From eaad78119f1f239bf506219ebc9c65ee17c93347 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:18:17 -0700 Subject: [PATCH 1/4] chore: updated imports to use absolute paths --- .vscode/settings.json | 5 +- pyproject.toml | 24 +++-- sonar-project.properties | 2 +- switcher_client/__init__.py | 8 +- switcher_client/client.py | 47 +++++++--- switcher_client/errors/__init__.py | 13 ++- switcher_client/lib/globals/global_auth.py | 4 + switcher_client/lib/globals/global_context.py | 3 +- switcher_client/lib/globals/global_retry.py | 5 ++ .../lib/globals/global_snapshot.py | 12 ++- switcher_client/lib/remote.py | 18 ++-- switcher_client/lib/remote_auth.py | 13 ++- switcher_client/lib/resolver.py | 16 ++-- switcher_client/lib/snapshot.py | 16 ++-- switcher_client/lib/snapshot_loader.py | 10 +-- switcher_client/lib/snapshot_watcher.py | 4 +- switcher_client/lib/types.py | 15 ++++ switcher_client/lib/utils/__init__.py | 4 +- switcher_client/lib/utils/execution_logger.py | 2 +- .../lib/utils/timed_match/timed_match.py | 4 +- switcher_client/switcher.py | 30 +++++-- switcher_client/switcher_data.py | 13 ++- switcher_client/version.py | 2 +- tests/playground/index.py | 6 +- tests/strategy-operations/test_date.py | 67 +++++++------- tests/strategy-operations/test_network.py | 61 ++++++------- tests/strategy-operations/test_numeric.py | 65 +++++++------- tests/strategy-operations/test_payload.py | 71 +++++++-------- tests/strategy-operations/test_regex.py | 87 ++++++++++--------- tests/strategy-operations/test_time.py | 51 +++++------ tests/strategy-operations/test_value.py | 47 +++++----- tests/test_client_check_switchers.py | 3 +- tests/test_switcher_logger.py | 17 ++-- 33 files changed, 426 insertions(+), 319 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 25cfc24..483635e 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,12 @@ { + "pylint.importStrategy": "fromEnvironment", + "pylint.ignorePatterns": ["**/tests/**"], "python.testing.pytestArgs": [ "tests", "-s", "-v" ], "python.testing.unittestEnabled": false, "python.testing.pytestEnabled": true, "editor.renderWhitespace": "trailing", - "files.trimTrailingWhitespace": true + "files.trimTrailingWhitespace": true, + "python-envs.defaultEnvManager": "ms-python.python:pipenv" } \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7d72af9..22e5ae4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,25 +4,35 @@ build-backend = "setuptools.build_meta" [project] name = "switcher_client" +version = "0.1.0" +description = "Switcher Client SDK for Python" dynamic = ["version", "readme", "dependencies"] authors = [{name='Roger Floriano (petruki)', email='switcher.project@gmail.com'}] -description = "Switcher Client SDK for Python" license = { text = "MIT" } +license-files = ["LICENSE"] +readme = "README.md" +requires-python = ">=3.9" classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", - "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ] -requires-python = ">=3.9" [project.urls] -Homepage = "https://github.com/switcherapi" +homepage = "https://github.com/switcherapi" +source = "https://github.com/switcherapi/switcher-client-py" +issue-tracker = "https://github.com/switcherapi/switcher-client-py/issues" +release-notes = "https://github.com/switcherapi/switcher-client-py/releases" [tool.setuptools.packages.find] include = ["switcher_client*"] @@ -39,11 +49,7 @@ source-roots = ["switcher_client"] max-line-length = 120 disable = [ "missing-module-docstring", - "missing-class-docstring", - "missing-function-docstring", - # Relative imports are valid within the installed package; pylint reports - # false positives when invoked from the project root without install. - "relative-beyond-top-level" + "missing-function-docstring" ] [tool.pylint.format] diff --git a/sonar-project.properties b/sonar-project.properties index 3badb1b..bbdafbb 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,7 +1,7 @@ sonar.projectKey=switcherapi_switcher-client-py sonar.projectName=switcher-client-py sonar.organization=switcherapi -sonar.projectVersion=0.0.1 +sonar.projectVersion=0.1.0 sonar.links.homepage=https://github.com/switcherapi/switcher-client-py sonar.sources=switcher_client diff --git a/switcher_client/__init__.py b/switcher_client/__init__.py index 5ad9299..e41eab8 100644 --- a/switcher_client/__init__.py +++ b/switcher_client/__init__.py @@ -1,7 +1,7 @@ -from .client import Client -from .switcher import Switcher -from .lib.globals.global_context import ContextOptions -from .lib.snapshot_watcher import WatchSnapshotCallback +from switcher_client.client import Client +from switcher_client.switcher import Switcher +from switcher_client.lib.globals.global_context import ContextOptions +from switcher_client.lib.snapshot_watcher import WatchSnapshotCallback __all__ = [ 'Client', diff --git a/switcher_client/client.py b/switcher_client/client.py index 2793617..3f61aac 100644 --- a/switcher_client/client.py +++ b/switcher_client/client.py @@ -1,18 +1,18 @@ 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 check_switchers, load_domain, validate_snapshot, save_snapshot -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 -from .errors import SnapshpotNotFoundError -from .switcher import Switcher +from switcher_client.lib.globals.global_auth import GlobalAuth +from switcher_client.lib.globals.global_snapshot import GlobalSnapshot, LoadSnapshotOptions +from switcher_client.lib.globals.global_context import Context, ContextOptions, DEFAULT_ENVIRONMENT +from switcher_client.lib.remote_auth import RemoteAuth +from switcher_client.lib.remote import Remote +from switcher_client.lib.snapshot_auto_updater import SnapshotAutoUpdater +from switcher_client.lib.snapshot_loader import check_switchers, load_domain, validate_snapshot, save_snapshot +from switcher_client.lib.snapshot_watcher import SnapshotWatcher, WatchSnapshotCallback +from switcher_client.lib.utils.execution_logger import ExecutionLogger +from switcher_client.lib.utils.timed_match.timed_match import TimedMatch +from switcher_client.lib.utils import get +from switcher_client.errors import SnapshotNotFoundError +from switcher_client.switcher import Switcher REGEX_MAX_BLACK_LIST = 'regex_max_black_list' REGEX_MAX_TIME_LIMIT = 'regex_max_time_limit' @@ -20,6 +20,25 @@ SILENT_MODE = 'silent_mode' class Client: + """ + Quick start with the following steps: + + 1. Use `Client.build_context()` to build the context for the client. + 2. Use `Client.get_switcher()` to create a new instance of Switcher. + 3. Use the instance created to evaluate criteria. + + Example: + Client.build_context( + domain='My Domain', # Your Switcher domain name + url='https://api.switcherapi.com', # Switcher-API endpoint (optional) + api_key='[YOUR_API_KEY]', # Your component's API key (optional) + component='MyApp', # Your application name (optional) + environment='default' # Environment ('default' for production) + ) + + switcher = Client.get_switcher() + result = switcher.is_on('my_criteria') + """ _context: Context = Context.empty() _switcher: dict[str, Switcher] = {} _snapshot_auto_updater: SnapshotAutoUpdater = SnapshotAutoUpdater() @@ -174,7 +193,7 @@ def watch_snapshot(callback: Optional[WatchSnapshotCallback] = None) -> None: snapshot_location = Client._context.options.snapshot_location if snapshot_location is None: - callback.reject(SnapshpotNotFoundError("Snapshot location is not defined in the context options")) + callback.reject(SnapshotNotFoundError("Snapshot location is not defined in the context options")) return environment = get(Client._context.environment, DEFAULT_ENVIRONMENT) diff --git a/switcher_client/errors/__init__.py b/switcher_client/errors/__init__.py index bc1f715..3bf68d3 100644 --- a/switcher_client/errors/__init__.py +++ b/switcher_client/errors/__init__.py @@ -1,29 +1,34 @@ class RemoteError(Exception): + """ Base class for remote errors """ def __init__(self, message): self.message = message super().__init__(self.message) class RemoteAuthError(RemoteError): - pass + """ Raised when there is an authentication error with the remote service """ class RemoteCriteriaError(RemoteError): - pass + """ Raised when there is a criteria error with the remote service """ class RemoteSwitcherError(RemoteError): + """ Raised when there is a switcher error with the remote service """ def __init__(self, not_found: list): super().__init__(f'{', '.join(not_found)} not found') class LocalSwitcherError(Exception): + """ Raised when there is a switcher error with the local service """ def __init__(self, not_found: list): self.message = f'{', '.join(not_found)} not found' super().__init__(self.message) class LocalCriteriaError(Exception): + """ Raised when there is a criteria error with the local service """ def __init__(self, message): self.message = message super().__init__(self.message) -class SnapshpotNotFoundError(Exception): +class SnapshotNotFoundError(Exception): + """ Raised when a snapshot is not found """ def __init__(self, message): self.message = message super().__init__(self.message) @@ -35,5 +40,5 @@ def __init__(self, message): 'RemoteSwitcherError', 'LocalSwitcherError', 'LocalCriteriaError', - 'SnapshpotNotFoundError' + 'SnapshotNotFoundError' ] diff --git a/switcher_client/lib/globals/global_auth.py b/switcher_client/lib/globals/global_auth.py index f01ca85..1887617 100644 --- a/switcher_client/lib/globals/global_auth.py +++ b/switcher_client/lib/globals/global_auth.py @@ -1,4 +1,8 @@ +from dataclasses import dataclass + +@dataclass class GlobalAuth: + """ GlobalAuth manages the global authentication state, including the token and its expiration time. """ __token = None __exp = None diff --git a/switcher_client/lib/globals/global_context.py b/switcher_client/lib/globals/global_context.py index 37dadfe..5dc46b6 100644 --- a/switcher_client/lib/globals/global_context.py +++ b/switcher_client/lib/globals/global_context.py @@ -37,8 +37,7 @@ class ContextOptions: :param cert_path: The path to the SSL certificate file for secure connections. If not set, it will use the default system certificates """ - # pylint: disable=too-many-arguments - # pylint: disable=too-many-instance-attributes + # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, *, local: bool = DEFAULT_LOCAL, logger: bool = DEFAULT_LOGGER, diff --git a/switcher_client/lib/globals/global_retry.py b/switcher_client/lib/globals/global_retry.py index 154fbde..32fb443 100644 --- a/switcher_client/lib/globals/global_retry.py +++ b/switcher_client/lib/globals/global_retry.py @@ -2,6 +2,11 @@ @dataclass class RetryOptions: + """ + RetryOptions holds the configuration for retrying authentication in silent mode, + including the retry time and the duration to wait between retries. + """ + def __init__(self, retry_time: int, retry_duration_in: str): """ :param retry_time: The maximum number of retries diff --git a/switcher_client/lib/globals/global_snapshot.py b/switcher_client/lib/globals/global_snapshot.py index c003528..b1296cb 100644 --- a/switcher_client/lib/globals/global_snapshot.py +++ b/switcher_client/lib/globals/global_snapshot.py @@ -1,8 +1,13 @@ from dataclasses import dataclass -from ...lib.types import Snapshot +from switcher_client.lib.types import Snapshot +@dataclass class GlobalSnapshot: + """ + GlobalSnapshot manages the global snapshot state, + allowing it to be set, cleared, and retrieved across the application. + """ @staticmethod def init(snapshot: Snapshot | None): @@ -18,6 +23,11 @@ def snapshot() -> Snapshot | None: @dataclass class LoadSnapshotOptions: + """ + LoadSnapshotOptions holds the options for loading a snapshot, + including whether to fetch it remotely and whether to watch for changes. + """ + def __init__(self, fetch_remote: bool = False, watch_snapshot: bool = False): self.fetch_remote = fetch_remote self.watch_snapshot = watch_snapshot diff --git a/switcher_client/lib/remote.py b/switcher_client/lib/remote.py index b54e042..c8b7699 100644 --- a/switcher_client/lib/remote.py +++ b/switcher_client/lib/remote.py @@ -1,16 +1,20 @@ -import json -import ssl from typing import Optional +import json +import ssl import httpx -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 -from ..switcher_data import SwitcherData +from switcher_client.errors import RemoteAuthError, RemoteError, RemoteCriteriaError, RemoteSwitcherError +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, get_entry +from switcher_client.switcher_data import SwitcherData class Remote: + """ + Remote handles all interactions with the remote switcher service, + including authentication, criteria checks, and snapshot resolution. + """ _client: Optional[httpx.Client] = None @staticmethod diff --git a/switcher_client/lib/remote_auth.py b/switcher_client/lib/remote_auth.py index 79d723c..7410d29 100644 --- a/switcher_client/lib/remote_auth.py +++ b/switcher_client/lib/remote_auth.py @@ -1,12 +1,17 @@ from time import time from datetime import datetime -from .remote import Remote -from .globals.global_context import Context -from .globals import GlobalAuth, RetryOptions -from .utils.date_moment import DateMoment +from switcher_client.lib.remote import Remote +from switcher_client.lib.globals.global_context import Context +from switcher_client.lib.globals import GlobalAuth, RetryOptions +from switcher_client.lib.utils.date_moment import DateMoment class RemoteAuth: + """ + RemoteAuth handles authentication with the remote switcher service. + It manages the authentication token, checks for token expiration, and handles silent mode token updates. + """ + __context: Context = Context.empty() __retry_options: RetryOptions diff --git a/switcher_client/lib/resolver.py b/switcher_client/lib/resolver.py index 6304085..d964012 100644 --- a/switcher_client/lib/resolver.py +++ b/switcher_client/lib/resolver.py @@ -1,13 +1,19 @@ from typing import Optional -from ..lib.types import Config, Domain, Entry, Group, ResultDetail, Snapshot, StrategyConfig -from ..lib.snapshot import process_operation -from ..lib.utils import get, get_entry -from ..errors import LocalCriteriaError -from ..switcher_data import SwitcherData +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.errors import LocalCriteriaError +from switcher_client.switcher_data import SwitcherData # pylint: disable=too-few-public-methods class Resolver: + """ + Resolver is responsible for resolving the criteria against the snapshot domain. + It provides methods to check the criteria for a given switcher request and return the result. + The resolution process involves checking the domain, groups, configs, and strategies in a hierarchical manner, + ensuring that all conditions are evaluated correctly to determine the final result. + """ @staticmethod def check_criteria(snapshot: Snapshot | None, switcher: SwitcherData) -> ResultDetail: diff --git a/switcher_client/lib/snapshot.py b/switcher_client/lib/snapshot.py index 429e72c..bc1d4b0 100644 --- a/switcher_client/lib/snapshot.py +++ b/switcher_client/lib/snapshot.py @@ -4,12 +4,13 @@ from typing import Optional from datetime import datetime -from ..lib.types import StrategyConfig -from .utils.payload_reader import parse_json, payload_reader -from .utils.ipcidr import IPCIDR -from .utils.timed_match import TimedMatch +from switcher_client.lib.types import StrategyConfig +from switcher_client.lib.utils.payload_reader import parse_json, payload_reader +from switcher_client.lib.utils.ipcidr import IPCIDR +from switcher_client.lib.utils.timed_match import TimedMatch class StrategiesType(Enum): + """ Enum for strategy types used in criteria evaluation. """ VALUE = "VALUE_VALIDATION" NUMERIC = "NUMERIC_VALIDATION" DATE = "DATE_VALIDATION" @@ -19,6 +20,7 @@ class StrategiesType(Enum): REGEX = "REGEX_VALIDATION" class OperationsType(Enum): + """ Enum for operation types used in strategy evaluation. """ EXIST = "EXIST" NOT_EXIST = "NOT_EXIST" EQUAL = "EQUAL" @@ -29,7 +31,8 @@ class OperationsType(Enum): HAS_ONE = "HAS_ONE" HAS_ALL = "HAS_ALL" -def process_operation(strategy_config: StrategyConfig, input_value: str) -> Optional[bool]: # pylint: disable=too-many-return-statements +# pylint: disable=too-many-return-statements +def process_operation(strategy_config: StrategyConfig, input_value: str) -> Optional[bool]: """Process the operation based on strategy configuration and input value.""" strategy = strategy_config.strategy @@ -65,7 +68,8 @@ def _process_value(operation: str, values: list, input_value: str) -> Optional[b case OperationsType.NOT_EQUAL.value: return input_value not in values -def _process_numeric(operation: str, values: list, input_value: str) -> Optional[bool]: # pylint: disable=too-many-return-statements +# pylint: disable=too-many-return-statements +def _process_numeric(operation: str, values: list, input_value: str) -> Optional[bool]: """ Process NUMERIC strategy operations.""" try: diff --git a/switcher_client/lib/snapshot_loader.py b/switcher_client/lib/snapshot_loader.py index 1ed98ed..f922f6c 100644 --- a/switcher_client/lib/snapshot_loader.py +++ b/switcher_client/lib/snapshot_loader.py @@ -1,11 +1,11 @@ import json import os -from .globals.global_auth import GlobalAuth -from .globals.global_context import Context -from .remote import Remote -from .types import Snapshot -from ..errors import LocalSwitcherError +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 switcher_client.errors import LocalSwitcherError def load_domain(snapshot_location: str, environment: str): """ Load Domain from snapshot file """ diff --git a/switcher_client/lib/snapshot_watcher.py b/switcher_client/lib/snapshot_watcher.py index e0ac65d..be4d0e4 100644 --- a/switcher_client/lib/snapshot_watcher.py +++ b/switcher_client/lib/snapshot_watcher.py @@ -4,8 +4,8 @@ from dataclasses import dataclass, field from typing import Callable -from .snapshot_loader import load_domain -from .globals.global_snapshot import GlobalSnapshot +from switcher_client.lib.snapshot_loader import load_domain +from switcher_client.lib.globals.global_snapshot import GlobalSnapshot _POLL_INTERVAL = 1 # seconds between file stat checks diff --git a/switcher_client/lib/types.py b/switcher_client/lib/types.py index e7f7563..bb27270 100644 --- a/switcher_client/lib/types.py +++ b/switcher_client/lib/types.py @@ -2,6 +2,11 @@ from typing import Optional class ResultDetail: + """ + ResultDetail holds the result of a criteria check, including the boolean result, + reason for the result, and any additional metadata. + It provides static methods to create instances for success, disabled, or custom results. + """ def __init__(self, result: bool, reason: Optional[str], metadata: Optional[dict] = None): self.result = result self.reason = reason @@ -28,6 +33,7 @@ def to_dict(self) -> dict: @dataclass class Domain: + """ Domain represents the top-level structure of the snapshot, containing groups and their configurations. """ def __init__(self): self.name: str self.version: int = 0 @@ -36,6 +42,7 @@ def __init__(self): @dataclass class Group: + """ Group represents a collection of configurations within a domain. """ def __init__(self): self.name: str self.activated: bool @@ -43,6 +50,7 @@ def __init__(self): @dataclass class Config: + """ Config represents a specific configuration within a group. """ def __init__(self): self.key: str self.activated: bool @@ -51,6 +59,7 @@ def __init__(self): @dataclass class StrategyConfig: + """ StrategyConfig represents a specific strategy within a configuration. """ def __init__(self): self.strategy: str self.activated: bool @@ -59,6 +68,7 @@ def __init__(self): @dataclass class Entry: + """ Entry represents an input entry for strategy evaluation, containing the strategy type and input value. """ # pylint: disable=redefined-builtin def __init__(self, strategy: str, input: str): self.strategy = strategy @@ -72,12 +82,17 @@ def to_dict(self) -> dict: @dataclass class Relay: + """ Relay represents a relay configuration within a configuration. """ def __init__(self): self.type: str self.activated: bool @dataclass class Snapshot: + """ + Snapshot represents the entire snapshot data structure, + including the domain and its nested groups and configurations. + """ def __init__(self, json_data: dict): self._original_data = json_data self.domain = self._parse_domain(json_data) diff --git a/switcher_client/lib/utils/__init__.py b/switcher_client/lib/utils/__init__.py index 08541eb..d7a25bd 100644 --- a/switcher_client/lib/utils/__init__.py +++ b/switcher_client/lib/utils/__init__.py @@ -1,5 +1,5 @@ -from ...lib.types import Entry -from .execution_logger import ExecutionLogger +from switcher_client.lib.types import Entry +from switcher_client.lib.utils.execution_logger import ExecutionLogger def get(value, default_value): """ Return value if not None, otherwise return default_value """ diff --git a/switcher_client/lib/utils/execution_logger.py b/switcher_client/lib/utils/execution_logger.py index 1962daa..b3aca70 100644 --- a/switcher_client/lib/utils/execution_logger.py +++ b/switcher_client/lib/utils/execution_logger.py @@ -1,7 +1,7 @@ # pylint: disable=redefined-builtin from typing import Optional, Callable, List -from ...lib.types import ResultDetail +from switcher_client.lib.types import ResultDetail # Global logger storage _logger: List['ExecutionLogger'] = [] diff --git a/switcher_client/lib/utils/timed_match/timed_match.py b/switcher_client/lib/utils/timed_match/timed_match.py index ed3ae1c..7870008 100644 --- a/switcher_client/lib/utils/timed_match/timed_match.py +++ b/switcher_client/lib/utils/timed_match/timed_match.py @@ -4,8 +4,8 @@ from typing import List, Optional, Any from dataclasses import dataclass -from ...globals.global_context import DEFAULT_REGEX_MAX_BLACKLISTED, DEFAULT_REGEX_MAX_TIME_LIMIT -from .worker import TaskType, WorkerResult, WorkerTask, persistent_regex_worker +from switcher_client.lib.globals.global_context import DEFAULT_REGEX_MAX_BLACKLISTED, DEFAULT_REGEX_MAX_TIME_LIMIT +from switcher_client.lib.utils.timed_match.worker import TaskType, WorkerResult, WorkerTask, persistent_regex_worker @dataclass class Blacklist: diff --git a/switcher_client/switcher.py b/switcher_client/switcher.py index 8191971..e2a9fb2 100644 --- a/switcher_client/switcher.py +++ b/switcher_client/switcher.py @@ -2,17 +2,29 @@ from datetime import datetime from typing import Optional -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 +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 class Switcher(SwitcherData): + """ + Switcher handles criteria execution and validations. + + The class provides methods to execute criteria with both boolean and detailed results, + and supports both synchronous and asynchronous execution modes. + + Example usage: + switcher = Client.get_switcher('FEATURE_LOGIN_V2') + switcher.is_on() # returns boolean result + switcher.is_on_with_details() # returns detailed result + + """ def __init__(self, context: Context, key: Optional[str] = None): super().__init__(context, key) self._context = context diff --git a/switcher_client/switcher_data.py b/switcher_client/switcher_data.py index ea1d645..136a369 100644 --- a/switcher_client/switcher_data.py +++ b/switcher_client/switcher_data.py @@ -1,15 +1,22 @@ # pylint: disable=redefined-builtin +from dataclasses import dataclass import json from datetime import datetime from abc import ABCMeta from typing import Optional, Self, Union -from .lib.utils import get -from .lib.globals.global_context import Context -from .lib.snapshot import StrategiesType +from switcher_client.lib.utils import get +from switcher_client.lib.globals.global_context import Context +from switcher_client.lib.snapshot import StrategiesType +@dataclass class SwitcherData(metaclass=ABCMeta): + """ + SwitcherData holds the data for a switcher execution. + It is used by the Switcher class to execute criteria and return results. + """ + # pylint: disable=too-many-instance-attributes def __init__(self, context: Context,key: Optional[str] = None): self._context = context diff --git a/switcher_client/version.py b/switcher_client/version.py index f102a9c..3dc1f76 100644 --- a/switcher_client/version.py +++ b/switcher_client/version.py @@ -1 +1 @@ -__version__ = "0.0.1" +__version__ = "0.1.0" diff --git a/tests/playground/index.py b/tests/playground/index.py index da0358b..5db13c6 100644 --- a/tests/playground/index.py +++ b/tests/playground/index.py @@ -1,7 +1,7 @@ import threading import time -from util import monitor_run +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, WatchSnapshotCallback @@ -28,7 +28,7 @@ def uc_simple_api_call(): )) switcher = Client.get_switcher(SWITCHER_KEY) - + monitor_thread = threading.Thread(target=monitor_run, args=(switcher,), daemon=True) monitor_thread.start() @@ -105,7 +105,7 @@ def uc_watch_snapshot(): 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() diff --git a/tests/strategy-operations/test_date.py b/tests/strategy-operations/test_date.py index ef7c625..9295b12 100644 --- a/tests/strategy-operations/test_date.py +++ b/tests/strategy-operations/test_date.py @@ -1,27 +1,28 @@ import pytest + from typing import List from switcher_client.lib.snapshot import OperationsType, StrategiesType, process_operation from switcher_client.lib.types import StrategyConfig class TestDateStrategy: - """Test suite for Strategy [DATE] tests.""" - + """ Test suite for Strategy [DATE] tests """ + @pytest.fixture def mock_values1(self) -> List[str]: - """Single date value mock data.""" + """ Single date value mock data """ return ['2019-12-01'] - - @pytest.fixture + + @pytest.fixture def mock_values2(self) -> List[str]: - """Multiple date values for BETWEEN operations.""" + """ Multiple date values for BETWEEN operations """ return ['2019-12-01', '2019-12-05'] - - @pytest.fixture + + @pytest.fixture def mock_values3(self) -> List[str]: - """Date with time component mock data.""" + """ Date with time component mock data """ return ['2019-12-01T08:30'] - + def given_strategy_config(self, operation: str, values: List[str]) -> StrategyConfig: strategy_config = StrategyConfig() strategy_config.strategy = StrategiesType.DATE.value @@ -29,86 +30,86 @@ def given_strategy_config(self, operation: str, values: List[str]) -> StrategyCo strategy_config.values = values strategy_config.activated = True return strategy_config - + def test_should_agree_when_input_is_lower(self, mock_values1): - """Should agree when input is LOWER.""" + """ Should agree when input is LOWER """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, '2019-11-26') assert result is True - + def test_should_agree_when_input_is_lower_or_same(self, mock_values1): - """Should agree when input is LOWER or SAME.""" + """ Should agree when input is LOWER or SAME """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, '2019-12-01') assert result is True - + def test_should_not_agree_when_input_is_not_lower(self, mock_values1): - """Should NOT agree when input is NOT LOWER.""" + """ Should NOT agree when input is NOT LOWER """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, '2019-12-02') assert result is False - + def test_should_agree_when_input_is_greater(self, mock_values1): - """Should agree when input is GREATER.""" + """ Should agree when input is GREATER """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, '2019-12-02') assert result is True - + def test_should_agree_when_input_is_greater_or_same(self, mock_values1): - """Should agree when input is GREATER or SAME.""" + """ Should agree when input is GREATER or SAME """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, '2019-12-01') assert result is True - + def test_should_not_agree_when_input_is_not_greater(self, mock_values1): - """Should NOT agree when input is NOT GREATER.""" + """ Should NOT agree when input is NOT GREATER """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, '2019-11-10') assert result is False - + def test_should_agree_when_input_is_in_between(self, mock_values2): - """Should agree when input is in BETWEEN.""" + """ Should agree when input is in BETWEEN """ strategy_config = self.given_strategy_config(OperationsType.BETWEEN.value, mock_values2) result = process_operation(strategy_config, '2019-12-03') assert result is True - + def test_should_not_agree_when_input_is_not_in_between(self, mock_values2): - """Should NOT agree when input is NOT in BETWEEN.""" + """ Should NOT agree when input is NOT in BETWEEN """ strategy_config = self.given_strategy_config(OperationsType.BETWEEN.value, mock_values2) result = process_operation(strategy_config, '2019-12-12') assert result is False - + def test_should_agree_when_input_is_lower_including_time(self, mock_values3): - """Should agree when input is LOWER including time.""" + """ Should agree when input is LOWER including time """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values3) result = process_operation(strategy_config, '2019-12-01T07:00') assert result is True - + def test_should_not_agree_when_input_is_not_lower_including_time(self, mock_values1): - """Should NOT agree when input is NOT LOWER including time.""" + """ Should NOT agree when input is NOT LOWER including time """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, '2019-12-01T07:00') assert result is False - + def test_should_agree_when_input_is_greater_including_time(self, mock_values3): - """Should agree when input is GREATER including time.""" + """ Should agree when input is GREATER including time """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values3) result = process_operation(strategy_config, '2019-12-01T08:40') assert result is True def test_should_not_agree_when_input_is_not_valid_date(self, mock_values1): - """Should NOT agree when input is NOT a valid date.""" + """ Should NOT agree when input is NOT a valid date """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, 'invalid-date') diff --git a/tests/strategy-operations/test_network.py b/tests/strategy-operations/test_network.py index acfb87f..6737e18 100644 --- a/tests/strategy-operations/test_network.py +++ b/tests/strategy-operations/test_network.py @@ -1,108 +1,109 @@ import pytest + from typing import List from switcher_client.lib.snapshot import OperationsType, StrategiesType, process_operation from switcher_client.lib.types import StrategyConfig class TestNetworkStrategy: - """Test suite for Strategy [NETWORK] tests.""" - + """ Test suite for Strategy [NETWORK] tests """ + @pytest.fixture def mock_values1(self) -> List[str]: - """Single CIDR range mock data.""" + """ Single CIDR range mock data """ return ['10.0.0.0/30'] - - @pytest.fixture + + @pytest.fixture def mock_values2(self) -> List[str]: - """Multiple CIDR ranges mock data.""" + """ Multiple CIDR ranges mock data """ return ['10.0.0.0/30', '192.168.0.0/30'] - + @pytest.fixture def mock_values3(self) -> List[str]: - """Multiple IP addresses mock data.""" + """ Multiple IP addresses mock data """ return ['192.168.56.56', '192.168.56.57', '192.168.56.58'] - + def given_strategy_config(self, operation: str, values: List[str]) -> StrategyConfig: - """Create a strategy configuration for NETWORK strategy.""" + """ Create a strategy configuration for NETWORK strategy """ strategy_config = StrategyConfig() strategy_config.strategy = StrategiesType.NETWORK.value strategy_config.operation = operation strategy_config.values = values strategy_config.activated = True return strategy_config - + def test_should_agree_when_input_range_exist(self, mock_values1): - """Should agree when input range EXIST.""" + """ Should agree when input range EXIST """ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values1) result = process_operation(strategy_config, '10.0.0.3') assert result is True - + def test_should_agree_when_input_range_exist_irregular_cidr(self): - """Should agree when input range EXIST - Irregular CIDR.""" + """ Should agree when input range EXIST - Irregular CIDR """ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, ['10.0.0.3/24']) result = process_operation(strategy_config, '10.0.0.3') assert result is True - + def test_should_not_agree_when_input_range_does_not_exist(self, mock_values1): - """Should NOT agree when input range DOES NOT EXIST.""" + """ Should NOT agree when input range DOES NOT EXIST """ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values1) result = process_operation(strategy_config, '10.0.0.4') assert result is False - + def test_should_agree_when_input_does_not_exist(self, mock_values1): - """Should agree when input DOES NOT EXIST.""" + """ Should agree when input DOES NOT EXIST """ strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values1) result = process_operation(strategy_config, '10.0.0.4') assert result is True - + def test_should_not_agree_when_input_exist_but_assumed_not_exist(self, mock_values1): - """Should NOT agree when input EXIST but assumed that it DOES NOT EXIST.""" + """ Should NOT agree when input EXIST but assumed that it DOES NOT EXIST """ strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values1) result = process_operation(strategy_config, '10.0.0.3') assert result is False - + def test_should_agree_when_input_ip_exist(self, mock_values3): - """Should agree when input IP EXIST.""" + """ Should agree when input IP EXIST """ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values3) result = process_operation(strategy_config, '192.168.56.58') assert result is True - + def test_should_agree_when_input_ip_does_not_exist(self, mock_values3): - """Should agree when input IP DOES NOT EXIST.""" + """ Should agree when input IP DOES NOT EXIST """ strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values3) result = process_operation(strategy_config, '192.168.56.50') assert result is True def test_should_not_agree_when_input_ip_does_exist_in_values(self, mock_values3): - """Should NOT agree when input IP DOES EXIST.""" + """ Should NOT agree when input IP DOES EXIST """ strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values3) result = process_operation(strategy_config, '192.168.56.58') assert result is False - + def test_should_agree_when_input_range_exist_for_multiple_ranges(self, mock_values2): - """Should agree when input range EXIST for multiple ranges.""" + """ Should agree when input range EXIST for multiple ranges """ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values2) result = process_operation(strategy_config, '192.168.0.3') assert result is True - + def test_should_agree_when_input_range_does_not_exist_for_multiple_ranges(self, mock_values2): - """Should NOT agree when input range DOES NOT EXIST for multiple ranges.""" + """ Should NOT agree when input range DOES NOT EXIST for multiple ranges """ strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values2) result = process_operation(strategy_config, '127.0.0.0') assert result is True def test_should_not_agree_when_operation_is_unknown(self, mock_values1): - """Should NOT agree when operation is unknown.""" + """ Should NOT agree when operation is unknown """ strategy_config = self.given_strategy_config('UNKNOWN_OPERATION', mock_values1) result = process_operation(strategy_config, '10.0.0.3') diff --git a/tests/strategy-operations/test_numeric.py b/tests/strategy-operations/test_numeric.py index fe6040a..48412a0 100644 --- a/tests/strategy-operations/test_numeric.py +++ b/tests/strategy-operations/test_numeric.py @@ -1,80 +1,81 @@ import pytest + from typing import List from switcher_client.lib.snapshot import OperationsType, StrategiesType, process_operation from switcher_client.lib.types import StrategyConfig class TestNumericStrategy: - """Test suite for Strategy [NUMERIC] tests.""" - + """ Test suite for Strategy [NUMERIC] tests """ + @pytest.fixture def mock_values1(self) -> List[str]: - """Single numeric value mock data.""" + """ Single numeric value mock data """ return ['1'] - - @pytest.fixture + + @pytest.fixture def mock_values2(self) -> List[str]: - """Multiple numeric values mock data.""" + """ Multiple numeric values mock data """ return ['1', '3'] - - @pytest.fixture + + @pytest.fixture def mock_values3(self) -> List[str]: - """Decimal numeric value mock data.""" + """ Decimal numeric value mock data """ return ['1.5'] - + def given_strategy_config(self, operation: str, values: List[str]) -> StrategyConfig: - """Create a strategy configuration for NUMERIC strategy.""" + """ Create a strategy configuration for NUMERIC strategy """ strategy_config = StrategyConfig() strategy_config.strategy = StrategiesType.NUMERIC.value strategy_config.operation = operation strategy_config.values = values strategy_config.activated = True return strategy_config - + def test_should_agree_when_input_exist_in_values_string_type(self, mock_values2): - """Should agree when input EXIST in values - String type.""" + """ Should agree when input EXIST in values - String type """ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values2) result = process_operation(strategy_config, '3') assert result is True - + def test_should_not_agree_when_input_exist_but_test_as_does_not_exist(self, mock_values2): - """Should NOT agree when input exist but test as DOES NOT EXIST.""" + """ Should NOT agree when input exist but test as DOES NOT EXIST """ strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values2) result = process_operation(strategy_config, '1') assert result is False - + def test_should_agree_when_input_does_not_exist_in_values(self, mock_values2): - """Should agree when input DOES NOT EXIST in values.""" + """ Should agree when input DOES NOT EXIST in values """ strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values2) result = process_operation(strategy_config, '2') assert result is True - + def test_should_agree_when_input_is_equal_to_value(self, mock_values1): - """Should agree when input is EQUAL to value.""" + """ Should agree when input is EQUAL to value """ strategy_config = self.given_strategy_config(OperationsType.EQUAL.value, mock_values1) result = process_operation(strategy_config, '1') assert result is True - + def test_should_not_agree_when_input_is_not_equal_but_test_as_equal(self, mock_values1): - """Should NOT agree when input is not equal but test as EQUAL.""" + """ Should NOT agree when input is not equal but test as EQUAL """ strategy_config = self.given_strategy_config(OperationsType.EQUAL.value, mock_values1) result = process_operation(strategy_config, '2') assert result is False - + def test_should_agree_when_input_is_not_equal_to_value(self, mock_values1): - """Should agree when input is NOT EQUAL to value.""" + """ Should agree when input is NOT EQUAL to value """ strategy_config = self.given_strategy_config(OperationsType.NOT_EQUAL.value, mock_values1) result = process_operation(strategy_config, '2') assert result is True - + def test_should_agree_when_input_is_greater_than_value(self, mock_values1, mock_values3): - """Should agree when input is GREATER than value.""" + """ Should agree when input is GREATER than value """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, '2') @@ -87,9 +88,9 @@ def test_should_agree_when_input_is_greater_than_value(self, mock_values1, mock_ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values3) result = process_operation(strategy_config, '1.55') assert result is True - + def test_should_not_agree_when_input_is_lower_but_tested_as_greater_than_value(self, mock_values1, mock_values3): - """Should NOT agree when input is lower but tested as GREATER than value.""" + """ Should NOT agree when input is lower but tested as GREATER than value """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, '0') @@ -102,9 +103,9 @@ def test_should_not_agree_when_input_is_lower_but_tested_as_greater_than_value(s strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values3) result = process_operation(strategy_config, '1.49') assert result is False - + def test_should_agree_when_input_is_lower_than_value(self, mock_values1, mock_values3): - """Should agree when input is LOWER than value.""" + """ Should agree when input is LOWER than value """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, '0') @@ -117,9 +118,9 @@ def test_should_agree_when_input_is_lower_than_value(self, mock_values1, mock_va strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values3) result = process_operation(strategy_config, '1.49') assert result is True - + def test_should_agree_when_input_is_between_values(self, mock_values2): - """Should agree when input is BETWEEN values.""" + """ Should agree when input is BETWEEN values """ strategy_config = self.given_strategy_config(OperationsType.BETWEEN.value, mock_values2) result = process_operation(strategy_config, '1') @@ -133,7 +134,7 @@ def test_should_agree_when_input_is_between_values(self, mock_values2): assert result is True def test_should_not_agree_when_input_is_not_number(self, mock_values2): - """Should NOT agree when input is NOT A NUMBER.""" + """ Should NOT agree when input is NOT A NUMBER """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values2) result = process_operation(strategy_config, 'ABC') diff --git a/tests/strategy-operations/test_payload.py b/tests/strategy-operations/test_payload.py index f501a04..cd104f1 100644 --- a/tests/strategy-operations/test_payload.py +++ b/tests/strategy-operations/test_payload.py @@ -1,5 +1,6 @@ import json import pytest + from typing import List from switcher_client.lib.types import StrategyConfig @@ -7,19 +8,19 @@ from switcher_client.lib.snapshot import OperationsType, StrategiesType, process_operation class TestPayloadStrategy: - """Test suite for Strategy [PAYLOAD] tests.""" - + """ Test suite for Strategy [PAYLOAD] tests """ + @pytest.fixture def fixture_1(self) -> str: - """Simple JSON payload fixture.""" + """ Simple JSON payload fixture """ return json.dumps({ 'id': '1', 'login': 'petruki' }) - - @pytest.fixture + + @pytest.fixture def fixture_values2(self) -> str: - """Complex nested JSON payload fixture.""" + """ Complex nested JSON payload fixture """ return json.dumps({ 'product': 'product-1', 'order': { @@ -40,10 +41,10 @@ def fixture_values2(self) -> str: } } }) - - @pytest.fixture + + @pytest.fixture def fixture_values3(self) -> str: - """Configuration-style JSON payload fixture.""" + """ Configuration-style JSON payload fixture """ return json.dumps({ 'description': 'Allowed IP address', 'strategy': 'NETWORK_VALIDATION', @@ -51,19 +52,19 @@ def fixture_values3(self) -> str: 'operation': 'EXIST', 'env': 'default' }) - + def given_strategy_config(self, operation: str, values: List[str]) -> StrategyConfig: - """Create a strategy configuration for PAYLOAD strategy.""" + """ Create a strategy configuration for PAYLOAD strategy """ strategy_config = StrategyConfig() strategy_config.strategy = StrategiesType.PAYLOAD.value strategy_config.operation = operation strategy_config.values = values strategy_config.activated = True return strategy_config - + def test_should_read_keys_from_payload_1(self, fixture_values2): - """Should read keys from payload #1.""" - + """ Should read keys from payload #1 """ + keys = payload_reader(json.loads(fixture_values2)) expected_keys = [ 'product', @@ -77,10 +78,10 @@ def test_should_read_keys_from_payload_1(self, fixture_values2): 'order.deliver.tracking.comments' ] assert all(key in keys for key in expected_keys) - + def test_should_read_keys_from_payload_2(self, fixture_values3): - """Should read keys from payload #2.""" - + """ Should read keys from payload #2 """ + keys = payload_reader(json.loads(fixture_values3)) expected_keys = [ 'description', @@ -90,10 +91,10 @@ def test_should_read_keys_from_payload_2(self, fixture_values3): 'env' ] assert all(key in keys for key in expected_keys) - + def test_should_read_keys_from_payload_with_array_values(self): - """Should read keys from payload with array values.""" - + """ Should read keys from payload with array values """ + test_payload = { 'order': { 'items': ['item_1', 'item_2'] @@ -105,41 +106,41 @@ def test_should_read_keys_from_payload_with_array_values(self): 'order.items' ] assert all(key in keys for key in expected_keys) - + def test_should_return_true_when_payload_has_field(self, fixture_1): - """Should return TRUE when payload has field.""" + """ Should return TRUE when payload has field """ strategy_config = self.given_strategy_config(OperationsType.HAS_ONE.value, ['login']) result = process_operation(strategy_config, fixture_1) assert result is True - + def test_should_return_false_when_payload_does_not_have_field(self, fixture_1): - """Should return FALSE when payload does not have field.""" + """ Should return FALSE when payload does not have field """ strategy_config = self.given_strategy_config(OperationsType.HAS_ONE.value, ['user']) result = process_operation(strategy_config, fixture_1) assert result is False - + def test_should_return_true_when_payload_has_nested_field(self, fixture_values2): - """Should return TRUE when payload has nested field.""" + """ Should return TRUE when payload has nested field """ strategy_config = self.given_strategy_config(OperationsType.HAS_ONE.value, [ 'order.qty', 'order.total' ]) result = process_operation(strategy_config, fixture_values2) assert result is True - + def test_should_return_true_when_payload_has_nested_field_with_arrays(self, fixture_values2): - """Should return TRUE when payload has nested field with arrays.""" + """ Should return TRUE when payload has nested field with arrays """ strategy_config = self.given_strategy_config(OperationsType.HAS_ONE.value, [ 'order.deliver.tracking.status' ]) result = process_operation(strategy_config, fixture_values2) assert result is True - + def test_should_return_true_when_payload_has_all(self, fixture_values2): - """Should return TRUE when payload has all.""" + """ Should return TRUE when payload has all """ strategy_config = self.given_strategy_config(OperationsType.HAS_ALL.value, [ 'product', @@ -153,9 +154,9 @@ def test_should_return_true_when_payload_has_all(self, fixture_values2): ]) result = process_operation(strategy_config, fixture_values2) assert result is True - + def test_should_return_false_when_payload_does_not_have_all(self, fixture_values2): - """Should return FALSE when payload does not have all.""" + """ Should return FALSE when payload does not have all """ strategy_config = self.given_strategy_config(OperationsType.HAS_ALL.value, [ 'product', @@ -164,10 +165,10 @@ def test_should_return_false_when_payload_does_not_have_all(self, fixture_values ]) result = process_operation(strategy_config, fixture_values2) assert result is False - + def test_should_return_false_when_payload_is_not_json_string(self): - """Should return FALSE when payload is not a JSON string.""" - + """ Should return FALSE when payload is not a JSON string """ + strategy_config = self.given_strategy_config(OperationsType.HAS_ALL.value, []) result = process_operation(strategy_config, 'NOT_JSON') assert result is False diff --git a/tests/strategy-operations/test_regex.py b/tests/strategy-operations/test_regex.py index 1121668..6a07fcf 100644 --- a/tests/strategy-operations/test_regex.py +++ b/tests/strategy-operations/test_regex.py @@ -1,4 +1,5 @@ import pytest + from typing import List from switcher_client.lib.snapshot import OperationsType, StrategiesType, process_operation @@ -6,50 +7,50 @@ from switcher_client.lib.utils.timed_match import TimedMatch class TestRegexStrategy: - """Test suite for Strategy [REGEX Safe] tests.""" - + """ Test suite for Strategy [REGEX Safe] tests """ + @classmethod def setup_class(cls): - """Set up TimedMatch before all tests in this class.""" + """ Set up TimedMatch before all tests in this class """ TimedMatch.set_max_time_limit(1000) # 1000ms = 1 second TimedMatch.initialize_worker() # Initialize persistent worker - + @classmethod def teardown_class(cls): - """Clean up TimedMatch after all tests in this class.""" + """ Clean up TimedMatch after all tests in this class """ TimedMatch.terminate_worker() # Terminate any running worker processes TimedMatch.clear_blacklist() - + @pytest.fixture def mock_values1(self) -> List[str]: - """Single regex pattern with word boundaries.""" + """ Single regex pattern with word boundaries """ return [r'\bUSER_[0-9]{1,2}\b'] - - @pytest.fixture + + @pytest.fixture def mock_values2(self) -> List[str]: - """Multiple regex patterns with word boundaries.""" + """ Multiple regex patterns with word boundaries """ return [ r'\bUSER_[0-9]{1,2}\b', r'\buser-[0-9]{1,2}\b' ] - - @pytest.fixture + + @pytest.fixture def mock_values3(self) -> List[str]: - """Simple regex pattern without word boundaries.""" + """ Simple regex pattern without word boundaries """ return ['USER_[0-9]{1,2}'] - + def given_strategy_config(self, operation: str, values: List[str]) -> StrategyConfig: - """Create a strategy configuration for REGEX strategy.""" + """ Create a strategy configuration for REGEX strategy """ strategy_config = StrategyConfig() strategy_config.strategy = StrategiesType.REGEX.value strategy_config.operation = operation strategy_config.values = values strategy_config.activated = True return strategy_config - + def test_should_agree_when_expect_to_exist_using_exist_operation(self, mock_values1, mock_values2): - """Should agree when expect to exist using EXIST operation.""" - + """ Should agree when expect to exist using EXIST operation """ + strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values1) result = process_operation(strategy_config, 'USER_1') assert result is True @@ -57,10 +58,10 @@ def test_should_agree_when_expect_to_exist_using_exist_operation(self, mock_valu strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values2) result = process_operation(strategy_config, 'user-01') assert result is True - + def test_should_not_agree_when_expect_to_exist_using_exist_operation(self, mock_values1, mock_values3): - """Should NOT agree when expect to exist using EXIST operation.""" - + """ Should NOT agree when expect to exist using EXIST operation """ + strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values1) result = process_operation(strategy_config, 'USER_123') assert result is False @@ -69,10 +70,10 @@ def test_should_not_agree_when_expect_to_exist_using_exist_operation(self, mock_ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values3) result = process_operation(strategy_config, 'USER_123') assert result is True - + def test_should_agree_when_expect_to_not_exist_using_not_exist_operation(self, mock_values1, mock_values2): - """Should agree when expect to not exist using NOT_EXIST operation.""" - + """ Should agree when expect to not exist using NOT_EXIST operation """ + strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values1) result = process_operation(strategy_config, 'USER_123') assert result is True @@ -80,47 +81,47 @@ def test_should_agree_when_expect_to_not_exist_using_not_exist_operation(self, m strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values2) result = process_operation(strategy_config, 'user-123') assert result is True - + def test_should_not_agree_when_expect_to_not_exist_using_not_exist_operation(self, mock_values1): - """Should NOT agree when expect to not exist using NOT_EXIST operation.""" - + """ Should NOT agree when expect to not exist using NOT_EXIST operation """ + strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values1) result = process_operation(strategy_config, 'USER_12') assert result is False - + def test_should_agree_when_expect_to_be_equal_using_equal_operation(self, mock_values3): - """Should agree when expect to be equal using EQUAL operation.""" - + """ Should agree when expect to be equal using EQUAL operation """ + strategy_config = self.given_strategy_config(OperationsType.EQUAL.value, mock_values3) result = process_operation(strategy_config, 'USER_11') assert result is True - + def test_should_not_agree_when_expect_to_be_equal_using_equal_operation(self, mock_values3): - """Should NOT agree when expect to be equal using EQUAL operation.""" - + """ Should NOT agree when expect to be equal using EQUAL operation """ + strategy_config = self.given_strategy_config(OperationsType.EQUAL.value, mock_values3) result = process_operation(strategy_config, 'user-11') assert result is False - + def test_should_agree_when_expect_to_not_be_equal_using_not_equal_operation(self, mock_values3): - """Should agree when expect to not be equal using NOT_EQUAL operation.""" - + """ Should agree when expect to not be equal using NOT_EQUAL operation """ + strategy_config = self.given_strategy_config(OperationsType.NOT_EQUAL.value, mock_values3) result = process_operation(strategy_config, 'USER_123') assert result is True - + def test_should_not_agree_when_expect_to_not_be_equal_using_not_equal_operation(self, mock_values3): - """Should NOT agree when expect to not be equal using NOT_EQUAL operation.""" - + """ Should NOT agree when expect to not be equal using NOT_EQUAL operation """ + strategy_config = self.given_strategy_config(OperationsType.NOT_EQUAL.value, mock_values3) result = process_operation(strategy_config, 'USER_1') assert result is False - + def test_should_not_agree_when_match_cannot_finish_redos_attempt(self): - """Should NOT agree when match cannot finish (reDoS attempt).""" - + """ Should NOT agree when match cannot finish (reDoS attempt) """ + strategy_config = self.given_strategy_config( - OperationsType.EQUAL.value, + OperationsType.EQUAL.value, ['^(([a-z])+.)+[A-Z]([a-z])+$'] ) result = process_operation(strategy_config, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa') diff --git a/tests/strategy-operations/test_time.py b/tests/strategy-operations/test_time.py index b69d95d..25f4b16 100644 --- a/tests/strategy-operations/test_time.py +++ b/tests/strategy-operations/test_time.py @@ -1,89 +1,90 @@ import pytest + from typing import List from switcher_client.lib.snapshot import OperationsType, StrategiesType, process_operation from switcher_client.lib.types import StrategyConfig class TestTimeStrategy: - """Test suite for Strategy [TIME] tests.""" - + """ Test suite for Strategy [TIME] tests """ + @pytest.fixture def mock_values1(self) -> List[str]: - """Single time value mock data.""" + """ Single time value mock data """ return ['08:00'] - - @pytest.fixture + + @pytest.fixture def mock_values2(self) -> List[str]: - """Multiple time values for BETWEEN operations.""" + """ Multiple time values for BETWEEN operations """ return ['08:00', '10:00'] - + def given_strategy_config(self, operation: str, values: List[str]) -> StrategyConfig: - """Create a strategy configuration for TIME strategy.""" + """ Create a strategy configuration for TIME strategy """ strategy_config = StrategyConfig() strategy_config.strategy = StrategiesType.TIME.value strategy_config.operation = operation strategy_config.values = values strategy_config.activated = True return strategy_config - + def test_should_agree_when_input_is_lower(self, mock_values1): - """Should agree when input is LOWER.""" + """ Should agree when input is LOWER """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, '06:00') assert result is True - + def test_should_agree_when_input_is_lower_or_same(self, mock_values1): - """Should agree when input is LOWER or SAME.""" + """ Should agree when input is LOWER or SAME """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, '08:00') assert result is True - + def test_should_not_agree_when_input_is_not_lower(self, mock_values1): - """Should NOT agree when input is NOT LOWER.""" + """ Should NOT agree when input is NOT LOWER """ strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1) result = process_operation(strategy_config, '10:00') assert result is False - + def test_should_agree_when_input_is_greater(self, mock_values1): - """Should agree when input is GREATER.""" + """ Should agree when input is GREATER """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, '10:00') assert result is True - + def test_should_agree_when_input_is_greater_or_same(self, mock_values1): - """Should agree when input is GREATER or SAME.""" + """ Should agree when input is GREATER or SAME """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, '08:00') assert result is True - + def test_should_not_agree_when_input_is_not_greater(self, mock_values1): - """Should NOT agree when input is NOT GREATER.""" + """ Should NOT agree when input is NOT GREATER """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, '06:00') assert result is False - + def test_should_agree_when_input_is_in_between(self, mock_values2): - """Should agree when input is in BETWEEN.""" + """ Should agree when input is in BETWEEN """ strategy_config = self.given_strategy_config(OperationsType.BETWEEN.value, mock_values2) result = process_operation(strategy_config, '09:00') assert result is True - + def test_should_not_agree_when_input_is_not_in_between(self, mock_values2): - """Should NOT agree when input is NOT in BETWEEN.""" + """ Should NOT agree when input is NOT in BETWEEN """ strategy_config = self.given_strategy_config(OperationsType.BETWEEN.value, mock_values2) result = process_operation(strategy_config, '07:00') assert result is False def test_should_not_agree_when_input_is_invalid_time_format(self, mock_values1): - """Should NOT agree when input is in INVALID time format.""" + """ Should NOT agree when input is in INVALID time format """ strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1) result = process_operation(strategy_config, 'invalid-time') diff --git a/tests/strategy-operations/test_value.py b/tests/strategy-operations/test_value.py index c8e8d5b..8bb4199 100644 --- a/tests/strategy-operations/test_value.py +++ b/tests/strategy-operations/test_value.py @@ -1,76 +1,77 @@ import pytest + from typing import List from switcher_client.lib.snapshot import OperationsType, StrategiesType, process_operation from switcher_client.lib.types import StrategyConfig class TestValueStrategy: - """Test suite for Strategy [VALUE] tests.""" - + """ Test suite for Strategy [VALUE] tests """ + @pytest.fixture def mock_values1(self) -> List[str]: - """Single user mock data.""" + """ Single user mock data """ return ['USER_1'] - - @pytest.fixture + + @pytest.fixture def mock_values2(self) -> List[str]: - """Multiple users mock data.""" + """ Multiple users mock data """ return ['USER_1', 'USER_2'] - + def given_strategy_config(self, operation: str, values: List[str]) -> StrategyConfig: - """Create a strategy configuration for VALUE strategy.""" + """ Create a strategy configuration for VALUE strategy """ strategy_config = StrategyConfig() strategy_config.strategy = StrategiesType.VALUE.value strategy_config.operation = operation strategy_config.values = values strategy_config.activated = True return strategy_config - + def test_should_agree_when_input_exist(self, mock_values1): - """Should agree when input EXIST.""" + """ Should agree when input EXIST """ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values1) result = process_operation(strategy_config, 'USER_1') assert result is True - + def test_should_not_agree_when_input_does_not_exist(self, mock_values1): - """Should NOT agree when input DOES NOT EXIST.""" + """ Should NOT agree when input DOES NOT EXIST """ strategy_config = self.given_strategy_config(OperationsType.EXIST.value, mock_values1) result = process_operation(strategy_config, 'USER_123') assert result is False - + def test_should_agree_when_input_does_not_exist_with_not_exist_operation(self, mock_values1): - """Should agree when input DOES NOT EXIST with NOT_EXIST operation.""" + """ Should agree when input DOES NOT EXIST with NOT_EXIST operation """ strategy_config = self.given_strategy_config(OperationsType.NOT_EXIST.value, mock_values1) result = process_operation(strategy_config, 'USER_123') assert result is True - + def test_should_agree_when_input_is_equal(self, mock_values1): - """Should agree when input is EQUAL.""" + """ Should agree when input is EQUAL """ strategy_config = self.given_strategy_config(OperationsType.EQUAL.value, mock_values1) result = process_operation(strategy_config, 'USER_1') assert result is True - + def test_should_not_agree_when_input_is_not_equal(self, mock_values1): - """Should NOT agree when input is NOT EQUAL.""" + """ Should NOT agree when input is NOT EQUAL """ strategy_config = self.given_strategy_config(OperationsType.EQUAL.value, mock_values1) result = process_operation(strategy_config, 'USER_2') assert result is False - + def test_should_agree_when_input_is_not_equal_with_not_equal_operation(self, mock_values2): - """Should agree when input is NOT EQUAL with NOT_EQUAL operation.""" + """ Should agree when input is NOT EQUAL with NOT_EQUAL operation """ strategy_config = self.given_strategy_config(OperationsType.NOT_EQUAL.value, mock_values2) result = process_operation(strategy_config, 'USER_123') assert result is True - + def test_should_not_agree_when_input_is_not_equal_but_value_exists(self, mock_values2): - """Should NOT agree when input is NOT EQUAL but value exists in list.""" - + """ Should NOT agree when input is NOT EQUAL but value exists in list """ + strategy_config = self.given_strategy_config(OperationsType.NOT_EQUAL.value, mock_values2) result = process_operation(strategy_config, 'USER_2') assert result is False diff --git a/tests/test_client_check_switchers.py b/tests/test_client_check_switchers.py index c890a73..13a243e 100644 --- a/tests/test_client_check_switchers.py +++ b/tests/test_client_check_switchers.py @@ -1,13 +1,12 @@ import time -from typing import Optional +from typing import Optional from pytest_httpx import HTTPXMock from switcher_client.client import Client from switcher_client.errors import LocalSwitcherError, RemoteSwitcherError from switcher_client.lib.globals.global_context import ContextOptions - def test_check_remote_switchers(httpx_mock): """ Should check remote switchers with success """ diff --git a/tests/test_switcher_logger.py b/tests/test_switcher_logger.py index 3c87f34..6480c29 100644 --- a/tests/test_switcher_logger.py +++ b/tests/test_switcher_logger.py @@ -1,12 +1,9 @@ -import pytest import time from typing import Optional from pytest_httpx import HTTPXMock -from switcher_client.errors import RemoteAuthError from switcher_client import Client -from switcher_client.lib.globals.global_auth import GlobalAuth from switcher_client.lib.globals.global_context import ContextOptions from switcher_client.lib.utils.execution_logger import ExecutionLogger @@ -22,7 +19,7 @@ def test_remote_with_logger(httpx_mock): # test assert switcher.is_on('MY_SWITCHER') - + logged = Client.get_execution(switcher) assert logged.key == 'MY_SWITCHER' assert logged.response.result is True @@ -45,7 +42,7 @@ def test_clear_logger(httpx_mock): # test assert switcher.is_on('MY_SWITCHER') - + # test clear Client.clear_logger() logged = Client.get_execution(switcher) @@ -71,7 +68,7 @@ def test_remote_with_input_and_logger(httpx_mock): assert switcher \ .check_value('user_id') \ .is_on('MY_SWITCHER') - + logged = Client.get_execution(switcher) assert logged.key == 'MY_SWITCHER' assert logged.response.result is True @@ -96,7 +93,7 @@ def test_remote_with_input_not_logged(httpx_mock): assert switcher \ .check_value('user_id') \ .is_on('MY_SWITCHER') - + logged = Client.get_execution(Client.get_switcher('MY_SWITCHER').check_value('other_id')) assert logged.key is None @@ -112,7 +109,7 @@ def test_remote_renew_logged_execution(httpx_mock): # test 1 assert switcher.is_on('MY_SWITCHER') - + logged = Client.get_execution(switcher) assert logged.key == 'MY_SWITCHER' assert logged.response.result is True @@ -120,7 +117,7 @@ def test_remote_renew_logged_execution(httpx_mock): # test 2 - change response given_check_criteria(httpx_mock, response={'result': False}) assert switcher.is_on('MY_SWITCHER') is False - + logged = Client.get_execution(switcher) assert logged.key == 'MY_SWITCHER' assert logged.response.result is False @@ -137,7 +134,7 @@ def test_execution_logger_not_found(httpx_mock): # test assert switcher.is_on('MY_SWITCHER') - + logged = Client.get_execution(Client.get_switcher('ANOTHER_SWITCHER')) assert logged.key is None From 384c9a0ac68c6c50ec586fe4d8013de4c3c61f48 Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:24:50 -0700 Subject: [PATCH 2/4] fix: pyproject.toml lincense value --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 22e5ae4..5414c21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ version = "0.1.0" description = "Switcher Client SDK for Python" dynamic = ["version", "readme", "dependencies"] authors = [{name='Roger Floriano (petruki)', email='switcher.project@gmail.com'}] -license = { text = "MIT" } +license = "MIT" license-files = ["LICENSE"] readme = "README.md" requires-python = ">=3.9" From 59d4f3215a9a8f22005c82fca72e995fcc8ebdce Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:27:14 -0700 Subject: [PATCH 3/4] fix: removed fixed version as resolved dynamically --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5414c21..923f374 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,6 @@ build-backend = "setuptools.build_meta" [project] name = "switcher_client" -version = "0.1.0" description = "Switcher Client SDK for Python" dynamic = ["version", "readme", "dependencies"] authors = [{name='Roger Floriano (petruki)', email='switcher.project@gmail.com'}] From c90ba5a7b8e91a69396738792662dda54431269c Mon Sep 17 00:00:00 2001 From: petruki <31597636+petruki@users.noreply.github.com> Date: Sun, 15 Mar 2026 13:30:23 -0700 Subject: [PATCH 4/4] fix: pyproject.toml dynamic values --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 923f374..44fce85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,11 +5,11 @@ build-backend = "setuptools.build_meta" [project] name = "switcher_client" description = "Switcher Client SDK for Python" -dynamic = ["version", "readme", "dependencies"] +dynamic = ["version", "readme"] +dependencies = ["httpx[http2]>=0.28.1"] authors = [{name='Roger Floriano (petruki)', email='switcher.project@gmail.com'}] license = "MIT" license-files = ["LICENSE"] -readme = "README.md" requires-python = ">=3.9" classifiers = [ "Development Status :: 4 - Beta", @@ -39,7 +39,6 @@ include = ["switcher_client*"] [tool.setuptools.dynamic] version = { attr = "switcher_client.version.__version__" } readme = { file = ["README.md"], content-type = "text/markdown" } -dependencies = { file = ["requirements.txt"] } [tool.pylint.main] source-roots = ["switcher_client"]