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
4 changes: 2 additions & 2 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:

steps:
- name: Git checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

Expand Down Expand Up @@ -47,7 +47,7 @@ jobs:

steps:
- name: Git checkout
uses: actions/checkout@v5
uses: actions/checkout@v6

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
core.setOutput('base_ref', pr.data.base.ref);
core.setOutput('head_sha', pr.data.head.sha);

- uses: actions/checkout@v5
- uses: actions/checkout@v6
with:
ref: ${{ steps.pr.outputs.head_sha }}
fetch-depth: 0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:

steps:
- name: Git checkout
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

Expand Down
4 changes: 2 additions & 2 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ name = "pypi"
httpx = {extras = ["http2"], version = "==0.28.1"}

[dev-packages]
pytest = "==8.4.2"
pytest = "==9.0.2"
pytest-cov = "==7.0.0"
pytest-httpx = "==0.35.0"
pytest-httpx = "==0.36.0"
typing-extensions = "4.15.0"
switcher-client = {file = ".", editable = true}
266 changes: 127 additions & 139 deletions Pipfile.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ Client.build_context(
environment='default',
options=ContextOptions(
local=True, # Enable local mode
logger=True, # 🚧 TODO: Enable logging
logger=True, # Enable logging
snapshot_location='./snapshot/', # Snapshot files location
snapshot_auto_update_interval=3, # Auto-update interval (seconds)
silent_mode='5m', # 🚧 TODO: Silent mode retry time
Expand Down
11 changes: 11 additions & 0 deletions switcher_client/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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

Expand Down Expand Up @@ -135,6 +136,16 @@ def snapshot_version() -> int:
return 0

return snapshot.domain.version

@staticmethod
def get_execution(switcher: Switcher) -> ExecutionLogger:
"""Retrieve execution log given a switcher"""
return ExecutionLogger.get_execution(switcher._key, switcher._input)

@staticmethod
def clear_logger() -> None:
"""Clear all logged executions"""
ExecutionLogger.clear_logger()

@staticmethod
def __is_check_snapshot_available(fetch_remote = False) -> bool:
Expand Down
4 changes: 3 additions & 1 deletion switcher_client/lib/globals/global_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@

class ContextOptions:
def __init__(self,
local = DEFAULT_LOCAL,
local = DEFAULT_LOCAL,
logger = False,
snapshot_location: Optional[str] = None,
snapshot_auto_update_interval: Optional[int] = None):
self.local = local
self.logger = logger
self.snapshot_location = snapshot_location
self.snapshot_auto_update_interval = snapshot_auto_update_interval

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
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

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

# Global logger storage
_logger: List['ExecutionLogger'] = []

class ExecutionLogger:
"""It keeps track of latest execution results."""

_callback_error: Optional[Callable[[Exception], None]] = None

def __init__(self):
self.key: Optional[str] = None
self.input: Optional[List[List[str]]] = None
self.response: ResultDetail = ResultDetail(result=False, reason=None, metadata=None)

@staticmethod
def add(response: ResultDetail, key: str, input: Optional[List[List[str]]] = None) -> None:
"""Add new execution result"""
global _logger

# Remove existing execution with same key and input
for index in range(len(_logger)):
log = _logger[index]
if ExecutionLogger._has_execution(log, key, input):
_logger.pop(index)
break

# Create new execution log entry
new_log = ExecutionLogger()
new_log.key = key
new_log.input = input
new_log.response = ResultDetail(
result=response.result,
reason=response.reason,
metadata={**(response.metadata or {}), 'cached': True}
)

_logger.append(new_log)

@staticmethod
def get_execution(key: str, input: Optional[List[List[str]]] = None) -> 'ExecutionLogger':
"""Retrieve a specific result given a key and an input"""
global _logger

for log in _logger:
if ExecutionLogger._has_execution(log, key, input):
return log

return ExecutionLogger()

@staticmethod
def get_by_key(key: str) -> List['ExecutionLogger']:
"""Retrieve results given a switcher key"""
global _logger

return [log for log in _logger if log.key == key]

@staticmethod
def clear_logger() -> None:
"""Clear all results"""
global _logger
_logger.clear()

@staticmethod
def _has_execution(log: 'ExecutionLogger', key: str, input: Optional[List[List[str]]]) -> bool:
"""Check if log matches the given key and input"""
return log.key == key and ExecutionLogger._check_strategy_inputs(log.input, input)

@staticmethod
def _check_strategy_inputs(logger_inputs: Optional[List[List[str]]], inputs: Optional[List[List[str]]]) -> bool:
"""Check if strategy inputs match between logger and current inputs"""
if not logger_inputs:
return not inputs or len(inputs) == 0

if not inputs:
return False

for strategy_input in logger_inputs:
if len(strategy_input) >= 2:
strategy, input_value = strategy_input[0], strategy_input[1]
# Find matching strategy and input in current inputs
found = any(
len(current_input) >= 2 and
current_input[0] == strategy and
current_input[1] == input_value
for current_input in inputs
)
if not found:
return False

return True
14 changes: 12 additions & 2 deletions switcher_client/switcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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

class Switcher(SwitcherData):
Expand Down Expand Up @@ -75,8 +76,17 @@ def __execute_api_checks(self):
def __execute_remote_criteria(self):
""" Execute remote criteria """
token = GlobalAuth.get_token()
return Remote.check_criteria(token, self._context, self)
response = Remote.check_criteria(token, self._context, self)

if self.__can_log():
ExecutionLogger.add(response, self._key, self._input)

return response

def __execute_local_criteria(self):
""" Execute local criteria """
return Resolver.check_criteria(GlobalSnapshot.snapshot(), self)
return Resolver.check_criteria(GlobalSnapshot.snapshot(), self)

def __can_log(self) -> bool:
""" Check if logging is enabled """
return self._context.options.logger and self._key is not None
Loading