Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.13-alpine
FROM python:3.14-alpine

# Update pip and install dependencies and tools
RUN pip install --upgrade pip && \
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/master.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ jobs:
with:
fetch-depth: 0

- name: Set up Python 3.13
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "3.13"
python-version: "3.14"

- name: Install dependencies
run: |
Expand All @@ -31,7 +31,7 @@ jobs:
run: pipenv run pytest -v --cov=./switcher_client --cov-report xml

- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v6.0.0
uses: sonarsource/sonarqube-scan-action@v7.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
if: env.SONAR_TOKEN != ''
Expand All @@ -41,7 +41,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
os: [ ubuntu-latest, windows-latest ]
runs-on: ${{ matrix.os }}

Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.x"
python-version: "3.14"
- name: Install pypa/build
run: >-
python3 -m
Expand All @@ -41,7 +41,7 @@ jobs:

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/
Expand All @@ -60,7 +60,7 @@ jobs:

steps:
- name: Download all the dists
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: python-package-distributions
path: dist/
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/sonar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
steps:
- name: Get PR details
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const pr = await github.rest.pulls.get({
Expand All @@ -33,10 +33,10 @@ jobs:
ref: ${{ steps.pr.outputs.head_sha }}
fetch-depth: 0

- name: Set up Python 3.13
- name: Set up Python 3.14
uses: actions/setup-python@v6
with:
python-version: "3.13"
python-version: "3.14"

- name: Install dependencies
run: |
Expand All @@ -47,7 +47,7 @@ jobs:
run: pipenv run pytest -v --cov=./switcher_client --cov-report xml

- name: SonarCloud Scan
uses: sonarsource/sonarqube-scan-action@v6.0.0
uses: sonarsource/sonarqube-scan-action@v7.0.0
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
if: env.SONAR_TOKEN != ''
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
python:
description: 'Python version'
required: true
default: '3.13'
default: '3.14'
os:
description: 'Operating System (ubuntu-20.04, ubuntu-latest, windows-latest)'
required: true
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ pip install switcher-client
```

### System Requirements
- **Python**: 3.9+ (supports 3.9, 3.10, 3.11, 3.12, 3.13)
- **Python**: 3.9+ (supports 3.9, 3.10, 3.11, 3.12, 3.13, 3.14)
- **Operating System**: Cross-platform (Windows, macOS, Linux)

## Configuration
Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ classifiers = [
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
requires-python = ">=3.6"
requires-python = ">=3.9"

[project.urls]
Homepage = "https://github.com/switcherapi"
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ sonar.links.homepage=https://github.com/switcherapi/switcher-client-py
sonar.sources=switcher_client
sonar.tests=tests
sonar.python.coverage.reportPaths=coverage.xml
sonar.python.version=3.13
sonar.python.version=3.14
sonar.exclusions=**/tests/**, **/switcher_client/version.py
32 changes: 31 additions & 1 deletion switcher_client/lib/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@

class StrategiesType(Enum):
VALUE = "VALUE_VALIDATION"
NUMERIC = "NUMERIC_VALIDATION"

class OperationsType(Enum):
EXIST = "EXIST"
NOT_EXIST = "NOT_EXIST"
EQUAL = "EQUAL"
NOT_EQUAL = "NOT_EQUAL"
GREATER = "GREATER"
LOWER = "LOWER"
BETWEEN = "BETWEEN"

def process_operation(strategy_config: dict, input_value: str) -> Optional[bool]:
strategy = strategy_config.get('strategy')
Expand All @@ -19,6 +23,8 @@ def process_operation(strategy_config: dict, input_value: str) -> Optional[bool]
match strategy:
case StrategiesType.VALUE.value:
return process_value(operation, values, input_value)
case StrategiesType.NUMERIC.value:
return process_numeric(operation, values, input_value)

def process_value(operation: str, values: list, input_value: str) -> Optional[bool]:
match operation:
Expand All @@ -29,4 +35,28 @@ def process_value(operation: str, values: list, input_value: str) -> Optional[bo
case OperationsType.EQUAL.value:
return input_value in values
case OperationsType.NOT_EQUAL.value:
return input_value not in values
return input_value not in values

def process_numeric(operation: str, values: list, input_value: str) -> Optional[bool]:
try:
numeric_input = float(input_value)
except ValueError:
return None

numeric_values = [float(v) for v in values]

match operation:
case OperationsType.EXIST.value:
return numeric_input in numeric_values
case OperationsType.NOT_EXIST.value:
return numeric_input not in numeric_values
case OperationsType.EQUAL.value:
return numeric_input in numeric_values
case OperationsType.NOT_EQUAL.value:
return numeric_input not in numeric_values
case OperationsType.GREATER.value:
return any(numeric_input > v for v in numeric_values)
case OperationsType.LOWER.value:
return any(numeric_input < v for v in numeric_values)
case OperationsType.BETWEEN.value:
return numeric_input >= numeric_values[0] and numeric_input <= numeric_values[1]
138 changes: 138 additions & 0 deletions tests/strategy-operations/test_numeric.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import pytest
from typing import Dict, List, Any

from switcher_client.lib.snapshot import OperationsType, StrategiesType, process_operation

class TestNumericStrategy:
"""Test suite for Strategy [NUMERIC] tests."""

@pytest.fixture
def mock_values1(self) -> List[str]:
"""Single numeric value mock data."""
return ['1']

@pytest.fixture
def mock_values2(self) -> List[str]:
"""Multiple numeric values mock data."""
return ['1', '3']

@pytest.fixture
def mock_values3(self) -> List[str]:
"""Decimal numeric value mock data."""
return ['1.5']

def given_strategy_config(self, operation: str, values: List[str]) -> Dict[str, Any]:
return {
'strategy': StrategiesType.NUMERIC.value,
'operation': operation,
'values': values,
'activated': True
}

def test_should_agree_when_input_exist_in_values_string_type(self, mock_values2):
"""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."""

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."""

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."""

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."""

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."""

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."""

strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1)
result = process_operation(strategy_config, '2')
assert result is True

# test decimal
result = process_operation(strategy_config, '1.01')
assert result is True

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."""

strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values1)
result = process_operation(strategy_config, '0')
assert result is False

# test decimal
result = process_operation(strategy_config, '0.99')
assert result is False

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."""

strategy_config = self.given_strategy_config(OperationsType.LOWER.value, mock_values1)
result = process_operation(strategy_config, '0')
assert result is True

# test decimal
result = process_operation(strategy_config, '0.99')
assert result is True

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."""

strategy_config = self.given_strategy_config(OperationsType.BETWEEN.value, mock_values2)
result = process_operation(strategy_config, '1')
assert result is True

# test decimal
result = process_operation(strategy_config, '2.99')
assert result is True

result = process_operation(strategy_config, '1.001')
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."""

strategy_config = self.given_strategy_config(OperationsType.GREATER.value, mock_values2)
result = process_operation(strategy_config, 'ABC')
assert result is None
7 changes: 7 additions & 0 deletions tests/strategy-operations/test_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,42 +26,49 @@ def given_strategy_config(self, operation: str, values: List[str]) -> Dict[str,

def test_should_agree_when_input_exist(self, mock_values1):
"""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."""

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."""

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."""

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."""

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."""

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."""

strategy_config = self.given_strategy_config(OperationsType.NOT_EQUAL.value, mock_values2)
result = process_operation(strategy_config, 'USER_2')
assert result is False