Skip to content

Commit ff12ee0

Browse files
authored
Merge branch 'development' into Feature/Async
2 parents 1ad41ce + cf7efcd commit ff12ee0

31 files changed

+1040
-103
lines changed

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
- Added support for asyncio library
33
- BREAKING CHANGE: Minimum supported Python version is 3.7.16
44

5+
9.7.0 (May 15, 2024)
6+
- Added support for targeting rules based on semantic versions (https://semver.org/).
7+
- Added the logic to handle correctly when the SDK receives an unsupported Matcher type.
8+
59
9.6.2 (Apr 5, 2024)
610
- Fixed an issue when pushing unique keys tracker data to redis if no keys exist, i.e. get_treatment flavors are not called.
711

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This SDK is designed to work with Split, the platform for controlled rollouts, w
77
[![Twitter Follow](https://img.shields.io/twitter/follow/splitsoftware.svg?style=social&label=Follow&maxAge=1529000)](https://twitter.com/intent/follow?screen_name=splitsoftware)
88

99
## Compatibility
10-
This SDK is compatible with **Python 3 and higher**.
10+
This SDK is compatible with **Python 3.7 and higher**.
1111

1212
## Getting started
1313
Below is a simple example that describes the instantiation and most basic usage of our SDK:

setup.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,18 @@
88
'flake8',
99
'pytest==7.0.1',
1010
'pytest-mock==3.11.1',
11-
'coverage==6.2',
11+
'coverage==7.2,7',
1212
'pytest-cov==4.1.0',
13-
'importlib-metadata==4.2',
13+
'importlib-metadata==6.7',
1414
'tomli==1.2.3',
1515
'iniconfig==1.1.1',
1616
'attrs==22.1.0',
1717
'pytest-asyncio==0.21.0'
1818
]
1919

2020
INSTALL_REQUIRES = [
21-
'requests>=2.9.1',
22-
'pyyaml>=5.4',
21+
'requests',
22+
'pyyaml',
2323
'docopt>=0.6.2',
2424
'enum34;python_version<"3.4"',
2525
'bloom-filter2>=2.0.0',

splitio/api/auth.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import json
55

66
from splitio.api import APIException, headers_from_metadata
7+
from splitio.api.commons import headers_from_metadata, record_telemetry
8+
from splitio.spec import SPEC_VERSION
9+
from splitio.util.time import get_current_epoch_time_ms
710
from splitio.api.client import HttpClientException
811
from splitio.models.token import from_raw
912
from splitio.models.telemetry import HTTPExceptionsAndLatencies
@@ -41,7 +44,7 @@ def authenticate(self):
4144
try:
4245
response = self._client.get(
4346
'auth',
44-
'v2/auth',
47+
'/v2/auth?s=' + SPEC_VERSION,
4548
self._sdk_key,
4649
extra_headers=self._metadata,
4750
)

splitio/api/commons.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,63 @@
11
"""Commons module."""
2+
from splitio.util.time import get_current_epoch_time_ms
3+
from splitio.spec import SPEC_VERSION
24

35
_CACHE_CONTROL = 'Cache-Control'
46
_CACHE_CONTROL_NO_CACHE = 'no-cache'
57

8+
def headers_from_metadata(sdk_metadata, client_key=None):
9+
"""
10+
Generate a dict with headers required by data-recording API endpoints.
11+
12+
:param sdk_metadata: SDK Metadata object, generated at sdk initialization time.
13+
:type sdk_metadata: splitio.client.util.SdkMetadata
14+
15+
:param client_key: client key.
16+
:type client_key: str
17+
18+
:return: A dictionary with headers.
19+
:rtype: dict
20+
"""
21+
22+
metadata = {
23+
'SplitSDKVersion': sdk_metadata.sdk_version,
24+
'SplitSDKMachineIP': sdk_metadata.instance_ip,
25+
'SplitSDKMachineName': sdk_metadata.instance_name
26+
} if sdk_metadata.instance_ip != 'NA' and sdk_metadata.instance_ip != 'unknown' else {
27+
'SplitSDKVersion': sdk_metadata.sdk_version,
28+
}
29+
30+
if client_key is not None:
31+
metadata['SplitSDKClientKey'] = client_key
32+
33+
return metadata
34+
35+
def record_telemetry(status_code, elapsed, metric_name, telemetry_runtime_producer):
36+
"""
37+
Record Telemetry info
38+
39+
:param status_code: http request status code
40+
:type status_code: int
41+
42+
:param elapsed: response time elapsed.
43+
:type status_code: int
44+
45+
:param metric_name: metric name for telemetry
46+
:type metric_name: str
47+
48+
:param telemetry_runtime_producer: telemetry recording instance
49+
:type telemetry_runtime_producer: splitio.engine.telemetry.TelemetryRuntimeProducer
50+
"""
51+
telemetry_runtime_producer.record_sync_latency(metric_name, elapsed)
52+
if 200 <= status_code < 300:
53+
telemetry_runtime_producer.record_successful_sync(metric_name, get_current_epoch_time_ms())
54+
return
55+
telemetry_runtime_producer.record_sync_error(metric_name, status_code)
56+
657
class FetchOptions(object):
758
"""Fetch Options object."""
859

9-
def __init__(self, cache_control_headers=False, change_number=None, sets=None):
60+
def __init__(self, cache_control_headers=False, change_number=None, sets=None, spec=SPEC_VERSION):
1061
"""
1162
Class constructor.
1263
@@ -22,6 +73,7 @@ def __init__(self, cache_control_headers=False, change_number=None, sets=None):
2273
self._cache_control_headers = cache_control_headers
2374
self._change_number = change_number
2475
self._sets = sets
76+
self._spec = spec
2577

2678
@property
2779
def cache_control_headers(self):
@@ -38,6 +90,11 @@ def sets(self):
3890
"""Return sets."""
3991
return self._sets
4092

93+
@property
94+
def spec(self):
95+
"""Return sets."""
96+
return self._spec
97+
4198
def __eq__(self, other):
4299
"""Match between other options."""
43100
if self._cache_control_headers != other._cache_control_headers:
@@ -48,7 +105,8 @@ def __eq__(self, other):
48105

49106
if self._sets != other._sets:
50107
return False
51-
108+
if self._spec != other._spec:
109+
return False
52110
return True
53111

54112

@@ -68,15 +126,16 @@ def build_fetch(change_number, fetch_options, metadata):
68126
:return: Objects for fetch
69127
:rtype: dict, dict
70128
"""
71-
query = {'since': change_number}
129+
query = {'s': fetch_options.spec} if fetch_options.spec is not None else {}
130+
query['since'] = change_number
72131
extra_headers = metadata
73132
if fetch_options is None:
74133
return query, extra_headers
75134

76135
if fetch_options.cache_control_headers:
77136
extra_headers[_CACHE_CONTROL] = _CACHE_CONTROL_NO_CACHE
78-
if fetch_options.change_number is not None:
79-
query['till'] = fetch_options.change_number
80137
if fetch_options.sets is not None:
81138
query['sets'] = fetch_options.sets
139+
if fetch_options.change_number is not None:
140+
query['till'] = fetch_options.change_number
82141
return query, extra_headers

splitio/models/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class MatcherNotFoundException(Exception):
2+
"""Exception to raise when a matcher is not found."""
3+
4+
def __init__(self, custom_message):
5+
"""Constructor."""
6+
Exception.__init__(self, custom_message)

splitio/models/grammar/condition.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from enum import Enum
44

5+
from splitio.models import MatcherNotFoundException
56
from splitio.models.grammar import matchers
67
from splitio.models.grammar import partitions
78

@@ -124,6 +125,7 @@ def from_raw(raw_condition):
124125
]
125126

126127
matcher_objects = [matchers.from_raw(x) for x in raw_condition['matcherGroup']['matchers']]
128+
127129
combiner = _MATCHER_COMBINERS[raw_condition['matcherGroup']['combiner']]
128130
label = raw_condition.get('label')
129131

splitio/models/grammar/matchers/__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Matchers entrypoint module."""
2+
from splitio.models import MatcherNotFoundException
23
from splitio.models.grammar.matchers.keys import AllKeysMatcher, UserDefinedSegmentMatcher
34
from splitio.models.grammar.matchers.numeric import BetweenMatcher, EqualToMatcher, \
45
GreaterThanOrEqualMatcher, LessThanOrEqualMatcher
@@ -7,6 +8,8 @@
78
from splitio.models.grammar.matchers.string import ContainsStringMatcher, \
89
EndsWithMatcher, RegexMatcher, StartsWithMatcher, WhitelistMatcher
910
from splitio.models.grammar.matchers.misc import BooleanMatcher, DependencyMatcher
11+
from splitio.models.grammar.matchers.semver import EqualToSemverMatcher, GreaterThanOrEqualToSemverMatcher, LessThanOrEqualToSemverMatcher, \
12+
BetweenSemverMatcher, InListSemverMatcher
1013

1114

1215
MATCHER_TYPE_ALL_KEYS = 'ALL_KEYS'
@@ -26,6 +29,11 @@
2629
MATCHER_TYPE_IN_SPLIT_TREATMENT = 'IN_SPLIT_TREATMENT'
2730
MATCHER_TYPE_EQUAL_TO_BOOLEAN = 'EQUAL_TO_BOOLEAN'
2831
MATCHER_TYPE_MATCHES_STRING = 'MATCHES_STRING'
32+
MATCHER_TYPE_EQUAL_TO_SEMVER = 'EQUAL_TO_SEMVER'
33+
MATCHER_GREATER_THAN_OR_EQUAL_TO_SEMVER = 'GREATER_THAN_OR_EQUAL_TO_SEMVER'
34+
MATCHER_LESS_THAN_OR_EQUAL_TO_SEMVER = 'LESS_THAN_OR_EQUAL_TO_SEMVER'
35+
MATCHER_BETWEEN_SEMVER = 'BETWEEN_SEMVER'
36+
MATCHER_INLIST_SEMVER = 'IN_LIST_SEMVER'
2937

3038

3139
_MATCHER_BUILDERS = {
@@ -45,10 +53,14 @@
4553
MATCHER_TYPE_CONTAINS_STRING: ContainsStringMatcher,
4654
MATCHER_TYPE_IN_SPLIT_TREATMENT: DependencyMatcher,
4755
MATCHER_TYPE_EQUAL_TO_BOOLEAN: BooleanMatcher,
48-
MATCHER_TYPE_MATCHES_STRING: RegexMatcher
56+
MATCHER_TYPE_MATCHES_STRING: RegexMatcher,
57+
MATCHER_TYPE_EQUAL_TO_SEMVER: EqualToSemverMatcher,
58+
MATCHER_GREATER_THAN_OR_EQUAL_TO_SEMVER: GreaterThanOrEqualToSemverMatcher,
59+
MATCHER_LESS_THAN_OR_EQUAL_TO_SEMVER: LessThanOrEqualToSemverMatcher,
60+
MATCHER_BETWEEN_SEMVER: BetweenSemverMatcher,
61+
MATCHER_INLIST_SEMVER: InListSemverMatcher
4962
}
5063

51-
5264
def from_raw(raw_matcher):
5365
"""
5466
Parse a condition from a JSON portion of splitChanges.
@@ -63,5 +75,5 @@ def from_raw(raw_matcher):
6375
try:
6476
builder = _MATCHER_BUILDERS[matcher_type]
6577
except KeyError:
66-
raise ValueError('Invalid matcher type %s' % matcher_type)
78+
raise MatcherNotFoundException('Invalid matcher type %s' % matcher_type)
6779
return builder(raw_matcher)

0 commit comments

Comments
 (0)