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
5 changes: 4 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -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"
}
28 changes: 16 additions & 12 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,41 @@ build-backend = "setuptools.build_meta"

[project]
name = "switcher_client"
dynamic = ["version", "readme", "dependencies"]
authors = [{name='Roger Floriano (petruki)', email='switcher.project@gmail.com'}]
description = "Switcher Client SDK for Python"
license = { text = "MIT" }
dynamic = ["version", "readme"]
dependencies = ["httpx[http2]>=0.28.1"]
authors = [{name='Roger Floriano (petruki)', email='switcher.project@gmail.com'}]
license = "MIT"
license-files = ["LICENSE"]
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*"]

[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"]
Expand All @@ -39,11 +47,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]
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 4 additions & 4 deletions switcher_client/__init__.py
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
47 changes: 33 additions & 14 deletions switcher_client/client.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,44 @@
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'
SNAPSHOT_AUTO_UPDATE_INTERVAL = 'snapshot_auto_update_interval'
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()
Expand Down Expand Up @@ -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)
Expand Down
13 changes: 9 additions & 4 deletions switcher_client/errors/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -35,5 +40,5 @@ def __init__(self, message):
'RemoteSwitcherError',
'LocalSwitcherError',
'LocalCriteriaError',
'SnapshpotNotFoundError'
'SnapshotNotFoundError'
]
4 changes: 4 additions & 0 deletions switcher_client/lib/globals/global_auth.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
3 changes: 1 addition & 2 deletions switcher_client/lib/globals/global_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions switcher_client/lib/globals/global_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion switcher_client/lib/globals/global_snapshot.py
Original file line number Diff line number Diff line change
@@ -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):
Expand All @@ -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
18 changes: 11 additions & 7 deletions switcher_client/lib/remote.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 9 additions & 4 deletions switcher_client/lib/remote_auth.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
16 changes: 11 additions & 5 deletions switcher_client/lib/resolver.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
16 changes: 10 additions & 6 deletions switcher_client/lib/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
Loading
Loading