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
2 changes: 1 addition & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[run]
omit =
switcher_client/lib/utils/timed_match/worker.py
switcher_client/version.py
*/tests/*
*/test_*
*/__init__.py

[report]
exclude_lines =
Expand Down
27 changes: 18 additions & 9 deletions switcher_client/client.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from typing import Optional, Callable

from switcher_client.lib.globals.global_snapshot import GlobalSnapshot, LoadSnapshotOptions
from switcher_client.lib.remote_auth import RemoteAuth
from switcher_client.lib.globals.global_context import Context, ContextOptions
from switcher_client.lib.globals.global_context import DEFAULT_ENVIRONMENT
from switcher_client.lib.snapshot_auto_updater import SnapshotAutoUpdater
from switcher_client.lib.snapshot_loader import load_domain, validate_snapshot, save_snapshot
from switcher_client.lib.utils.execution_logger import ExecutionLogger
from switcher_client.lib.utils import get
from switcher_client.switcher import Switcher
from .lib.globals.global_snapshot import GlobalSnapshot, LoadSnapshotOptions
from .lib.remote_auth import RemoteAuth
from .lib.globals.global_context import Context, ContextOptions
from .lib.globals.global_context import DEFAULT_ENVIRONMENT
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
from .lib.utils.timed_match.timed_match import TimedMatch
from .lib.utils import get
from .switcher import Switcher

class SwitcherOptions:
SNAPSHOT_AUTO_UPDATE_INTERVAL = 'snapshot_auto_update_interval'
Expand Down Expand Up @@ -146,6 +147,14 @@ def get_execution(switcher: Switcher) -> ExecutionLogger:
def clear_logger() -> None:
"""Clear all logged executions"""
ExecutionLogger.clear_logger()

@staticmethod
def clear_resources() -> None:
""" Clear all resources used by the Client """
Client.terminate_snapshot_auto_update()
ExecutionLogger.clear_logger()
GlobalSnapshot.clear()
TimedMatch.terminate_worker()

@staticmethod
def _is_check_snapshot_available(fetch_remote = False) -> bool:
Expand Down
9 changes: 8 additions & 1 deletion switcher_client/errors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,11 @@ def __init__(self, message):
class LocalCriteriaError(Exception):
def __init__(self, message):
self.message = message
super().__init__(self.message)
super().__init__(self.message)

__all__ = [
'RemoteError',
'RemoteAuthError',
'RemoteCriteriaError',
'LocalCriteriaError',
]
2 changes: 0 additions & 2 deletions switcher_client/lib/globals/global_auth.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import Optional

class GlobalAuth:
__token = None
__exp = None
Expand Down
1 change: 0 additions & 1 deletion switcher_client/lib/globals/global_context.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Optional


DEFAULT_ENVIRONMENT = 'default'
DEFAULT_LOCAL = False

Expand Down
2 changes: 1 addition & 1 deletion switcher_client/lib/globals/global_snapshot.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from switcher_client.lib.types import Snapshot
from ...lib.types import Snapshot

class GlobalSnapshot:

Expand Down
31 changes: 8 additions & 23 deletions switcher_client/lib/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@

from typing import Optional

from switcher_client.errors import RemoteAuthError, RemoteError
from switcher_client.errors import RemoteCriteriaError
from switcher_client.lib.globals.global_context import DEFAULT_ENVIRONMENT, Context
from switcher_client.lib.types import ResultDetail
from switcher_client.lib.utils import get
from switcher_client.switcher_data import SwitcherData
from ..errors import RemoteAuthError, RemoteError, RemoteCriteriaError
from ..lib.globals.global_context import DEFAULT_ENVIRONMENT, Context
from ..lib.types import ResultDetail
from ..lib.utils import get, get_entry
from ..switcher_data import SwitcherData

class Remote:
_client: Optional[httpx.Client] = None
Expand All @@ -33,8 +32,8 @@ def auth(context: Context):
@staticmethod
def check_criteria(token: Optional[str], context: Context, switcher: SwitcherData) -> ResultDetail:
url = f'{context.url}/criteria?showReason={str(switcher._show_details).lower()}&key={switcher._key}'
entry = Remote._get_entry(switcher._input)
response = Remote._do_post(url, entry, Remote._get_header(token))
entry = get_entry(switcher._input)
response = Remote._do_post(url, { 'entry': [e.to_dict() for e in entry] }, Remote._get_header(token))

if response.status_code == 200:
json_response = response.json()
Expand Down Expand Up @@ -115,18 +114,4 @@ def _get_header(token: Optional[str]):
return {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json',
}

@staticmethod
def _get_entry(input: list):
entry = []
for strategy_type, input_value in input:
entry.append({
'strategy': strategy_type,
'input': input_value
})

if not entry:
return None

return {'entry': entry}
}
8 changes: 3 additions & 5 deletions switcher_client/lib/remote_auth.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from time import time

from switcher_client.lib.remote import Remote
from switcher_client.lib.globals.global_context import Context
from switcher_client.lib.globals import GlobalAuth
from .remote import Remote
from .globals.global_context import Context
from .globals import GlobalAuth

class RemoteAuth:
__context: Context = Context.empty()
Expand Down Expand Up @@ -42,5 +42,3 @@ def is_valid():
errors = [name for name, value in required_fields if not value]
if errors:
raise ValueError(f"Something went wrong: Missing or empty required fields ({', '.join(errors)})")


48 changes: 46 additions & 2 deletions switcher_client/lib/resolver.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,33 @@
from typing import Optional

from switcher_client.lib.types import Config, Domain, Entry, Group, ResultDetail, Snapshot, StrategyConfig
from switcher_client.lib.snapshot import process_operation
from switcher_client.lib.utils import get, get_entry
from switcher_client.lib.utils import get_entry
from switcher_client.errors import LocalCriteriaError
from switcher_client.lib.types import Config, Domain, Group, ResultDetail, Snapshot
from switcher_client.switcher_data import SwitcherData

class Resolver:

@staticmethod
def check_criteria(snapshot: Snapshot | None, switcher: SwitcherData) -> ResultDetail:
""" Resolves the criteria for a given switcher request against the snapshot domain. """
if not snapshot:
raise LocalCriteriaError("Snapshot not loaded. Try to use 'Client.load_snapshot()'")

return Resolver._check_domain(snapshot.domain, switcher)

@staticmethod
def _check_domain(domain: Domain, switcher: SwitcherData) -> ResultDetail:
""" Checks if the domain is activated and proceeds to check groups. """
if domain.activated is False:
return ResultDetail.disabled("Domain is disabled")

return Resolver._check_group(domain.group, switcher)

@staticmethod
def _check_group(groups: list[Group], switcher: SwitcherData) -> ResultDetail:
""" Finds the correct config in the groups and checks it. """
key = switcher._key

for group in groups:
Expand All @@ -35,7 +43,43 @@ def _check_group(groups: list[Group], switcher: SwitcherData) -> ResultDetail:

@staticmethod
def _check_config(config: Config, switcher: SwitcherData) -> ResultDetail:
""" Checks if the config is activated and proceeds to check strategies. """
if config.activated is False:
return ResultDetail.disabled("Config disabled")

return ResultDetail.success()
if config.strategies is not None and len(config.strategies) > 0:
return Resolver._check_strategy(config.strategies, switcher._input)

return ResultDetail.success()

@staticmethod
def _check_strategy(strategy_configs: list[StrategyConfig], inputs: list[list[str]]) -> ResultDetail:
""" Checks each strategy configuration against the provided inputs. """
entry = get_entry(get(inputs, []))

for strategy_config in strategy_configs:
if not strategy_config.activated:
continue

strategy_result = Resolver._check_strategy_config(strategy_config, entry)
if strategy_result is not None:
return strategy_result

return ResultDetail.success()

@staticmethod
def _check_strategy_config(strategy_config: StrategyConfig, entry: list[Entry]) -> Optional[ResultDetail]:
""" Checks a single strategy configuration against the provided entries. """
if len(entry) == 0:
return ResultDetail.disabled(f"Strategy '{strategy_config.strategy}' did not receive any input")

strategy_entry = [e for e in entry if e.strategy == strategy_config.strategy]
if not Resolver._is_strategy_fulfilled(strategy_entry, strategy_config):
return ResultDetail.disabled(f"Strategy '{strategy_config.strategy}' does not agree")

return None

@staticmethod
def _is_strategy_fulfilled(strategy_entry: list[Entry], strategy_config: StrategyConfig) -> bool:
""" Determines if the strategy conditions are fulfilled based on the entries and configuration. """
return len(strategy_entry) > 0 and process_operation(strategy_config, strategy_entry[0].input) is True
16 changes: 9 additions & 7 deletions switcher_client/lib/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import Optional
from datetime import datetime

from switcher_client.lib.types import StrategyConfig

from .utils.payload_reader import parse_json, payload_reader
from .utils.ipcidr import IPCIDR
from .utils.timed_match import TimedMatch
Expand All @@ -13,9 +15,9 @@ class StrategiesType(Enum):
NUMERIC = "NUMERIC_VALIDATION"
DATE = "DATE_VALIDATION"
TIME = "TIME_VALIDATION"
PAYLOAD = "PAYLOAD"
NETWORK = "NETWORK"
REGEX = "REGEX"
PAYLOAD = "PAYLOAD_VALIDATION"
NETWORK = "NETWORK_VALIDATION"
REGEX = "REGEX_VALIDATION"

class OperationsType(Enum):
EXIST = "EXIST"
Expand All @@ -28,12 +30,12 @@ class OperationsType(Enum):
HAS_ONE = "HAS_ONE"
HAS_ALL = "HAS_ALL"

def process_operation(strategy_config: dict, input_value: str) -> Optional[bool]:
def process_operation(strategy_config: StrategyConfig, input_value: str) -> Optional[bool]:
"""Process the operation based on strategy configuration and input value."""

strategy = strategy_config.get('strategy')
operation = strategy_config.get('operation', '')
values = strategy_config.get('values', [])
strategy = strategy_config.strategy
operation = strategy_config.operation
values = strategy_config.values

match strategy:
case StrategiesType.VALUE.value:
Expand Down
1 change: 1 addition & 0 deletions switcher_client/lib/snapshot_auto_updater.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import threading
import time

from typing import Callable, Optional

class SnapshotAutoUpdater:
Expand Down
8 changes: 4 additions & 4 deletions switcher_client/lib/snapshot_loader.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import json
import os

from switcher_client.lib.globals.global_auth import GlobalAuth
from switcher_client.lib.globals.global_context import Context
from switcher_client.lib.remote import Remote
from switcher_client.lib.types import Snapshot
from .globals.global_auth import GlobalAuth
from .globals.global_context import Context
from .remote import Remote
from .types import Snapshot

def load_domain(snapshot_location: str, environment: str):
""" Load Domain from snapshot file """
Expand Down
11 changes: 11 additions & 0 deletions switcher_client/lib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ def __init__(self):
self.operation: str
self.values: list[str]

class Entry:
def __init__(self, strategy: str, input: str):
self.strategy = strategy
self.input = input

def to_dict(self) -> dict:
return {
'strategy': self.strategy,
'input': self.input
}

class Relay:
def __init__(self):
self.type: str
Expand Down
12 changes: 12 additions & 0 deletions switcher_client/lib/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,22 @@
from typing import Optional

from switcher_client.lib.types import Entry
from .execution_logger import ExecutionLogger

def get(value, default_value):
""" Return value if not None, otherwise return default_value """
return value if value is not None else default_value

def get_entry(input: list) -> list[Entry]:
""" Prepare entry dictionary from input strategy handling """
entry: list[Entry] = []
for strategy_type, input_value in input:
entry.append(Entry(strategy_type, input_value))

return entry

__all__ = [
'ExecutionLogger',
'get_entry',
'get',
]
3 changes: 2 additions & 1 deletion switcher_client/lib/utils/execution_logger.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Optional, Callable, List
from switcher_client.lib.types import ResultDetail

from ...lib.types import ResultDetail

# Global logger storage
_logger: List['ExecutionLogger'] = []
Expand Down
2 changes: 1 addition & 1 deletion switcher_client/lib/utils/payload_reader.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from typing import Any, List, Union

from typing import Any, List, Union

def payload_reader(payload: Any) -> List[str]:
"""Extract all field keys from a JSON payload structure.
Expand Down
4 changes: 1 addition & 3 deletions switcher_client/lib/utils/timed_match/timed_match.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import multiprocessing
import signal
import os
import time

from typing import List, Optional, Any
from dataclasses import dataclass

from switcher_client.lib.utils.timed_match.worker import TaskType, WorkerResult, WorkerTask, persistent_regex_worker
from .worker import TaskType, WorkerResult, WorkerTask, persistent_regex_worker

# Default constants
DEFAULT_REGEX_MAX_TIME_LIMIT = 3000 # 3 seconds in milliseconds
Expand Down
18 changes: 9 additions & 9 deletions switcher_client/switcher.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
from typing import Optional

from switcher_client.lib.globals.global_context import Context
from switcher_client.lib.globals.global_snapshot import GlobalSnapshot
from switcher_client.lib.remote_auth import RemoteAuth
from switcher_client.lib.globals import GlobalAuth
from switcher_client.lib.remote import Remote
from switcher_client.lib.resolver import Resolver
from switcher_client.lib.types import ResultDetail
from switcher_client.lib.utils.execution_logger import ExecutionLogger
from switcher_client.switcher_data import SwitcherData
from .lib.globals.global_context import Context
from .lib.globals.global_snapshot import GlobalSnapshot
from .lib.remote_auth import RemoteAuth
from .lib.globals import GlobalAuth
from .lib.remote import Remote
from .lib.resolver import Resolver
from .lib.types import ResultDetail
from .lib.utils.execution_logger import ExecutionLogger
from .switcher_data import SwitcherData

class Switcher(SwitcherData):
def __init__(self, context: Context, key: Optional[str] = None):
Expand Down
Loading