diff --git a/CHANGES.txt b/CHANGES.txt index 5f27cd0..95568df 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -40,4 +40,6 @@ 3.2.0 (Feb 2, 2025) - Updated to support flag sets, large segments and the impressionsDisabled boolean value 3.5.0 (May 6, 2025) -- Updated to support harness mode \ No newline at end of file +- Updated to support harness mode +3.5.1 (June 20, 2025) +- Updated to support rule based segments diff --git a/README.md b/README.md index 6993d21..a59cffe 100644 --- a/README.md +++ b/README.md @@ -282,6 +282,214 @@ definition= {"treatments":[ {"name":"on"},{"name":"off"}], splitDef.submit_change_request(definition, 'UPDATE', 'updating default rule', 'comment', ['user@email.com'], '') ``` +### Rule-Based Segments + +Rule-based segments allow you to define audience segments using complex rule structures and exclusion logic. Added in version 3.5.1, they offer enhanced functionality for targeting users. + +Fetch all Rule-Based Segments: + +```python +ws = client.workspaces.find("Defaults") +for segment in client.rule_based_segments.list(ws.id): + print("\nRule-Based Segment: " + segment.name + ", " + segment.description) +``` + +Add new Rule-Based Segment: + +```python +segment_data = { + 'name': 'advanced_users', + 'description': 'Users who match advanced criteria' +} +rule_segment = ws.add_rule_based_segment(segment_data, "user") +print(rule_segment.name) +``` + +Add Rule-Based Segment to environment: + +```python +ws = client.workspaces.find("Defaults") +segment = client.rule_based_segments.find("advanced_users", ws.id) +env = client.environments.find("Production", ws.id) +segdef = segment.add_to_environment(env.id) +``` + +#### Rule-Based Segment Structure + +Rule-based segment definitions support multiple rule types and matching conditions: + +```python +# Examples of different matcher types +matchers = [ + # String matching + { + 'type': 'IN_LIST_STRING', + 'attribute': 'device', + 'strings': ['mobile', 'tablet'] + }, + # Numeric comparisons + { + 'type': 'GREATER_THAN_OR_EQUAL_NUMBER', + 'attribute': 'age', + 'number': 21 + }, + { + 'type': 'LESS_THAN_OR_EQUAL_NUMBER', + 'attribute': 'account_age_days', + 'number': 30 + }, + { + 'type': 'BETWEEN_NUMBER', + 'attribute': 'purchases', + 'between': {'from': 5, 'to': 20} + }, + # Boolean conditions + { + 'type': 'BOOLEAN', + 'attribute': 'subscribed', + 'bool': True + }, + # Date/time matching + { + 'type': 'ON_DATE', + 'attribute': 'sign_up_date', + 'date': 1623456789000 # timestamp in milliseconds + }, + # Dependency on another split + { + 'type': 'IN_SPLIT', + 'attribute': '', + 'depends': {'splitName': 'another_split', 'treatment': 'on'} + } +] + +# Multiple conditions using combiners +condition = { + 'combiner': 'AND', # Can only be 'AND' + 'matchers': matchers +} +``` + +Update Rule-Based Segment definition with rules: + +```python +ws = client.workspaces.find("Defaults") +env = client.environments.find("Production", ws.id) +segdef = client.rule_based_segment_definitions.find("advanced_users", env.id, ws.id) + +# Define rules that match users in a certain list +rules_data = { + 'rules': [ + { + 'condition': { + 'combiner': 'AND', + 'matchers': [ + { + 'type': 'GREATER_THAN_OR_EQUAL_NUMBER', + 'attribute': 'age', + 'number': 30 + }, + { + 'type': 'BOOLEAN', + 'attribute': 'completed_tutorials', + 'bool': True + } + ] + } + } + ] +} + +# Update the segment definition with the rules +updated_segdef = segdef.update(rules_data) +``` + +Update Rule-Based Segment definition with excluded keys and excluded segments: + +```python +ws = client.workspaces.find("Defaults") +env = client.environments.find("Production", ws.id) +segdef = client.rule_based_segment_definitions.find("advanced_users", env.id, ws.id) + +# Define rules and exclusion data +update_data = { + 'rules': [ + { + 'condition': { + 'combiner': 'AND', + 'matchers': [ + { + 'type': 'GREATER_THAN_OR_EQUAL_NUMBER', + 'attribute': 'age', + 'number': 30 + } + ] + } + } + ], + 'excludedKeys': ['user1', 'user2', 'user3'], + 'excludedSegments': [ + { + 'name': 'beta_testers', + 'type': 'standard_segment' + } + ] +} + +# Update the segment definition with rules and exclusions +updated_segdef = segdef.update(update_data) +``` + +Submit a Change request to update a Rule-Based Segment definition: + +```python +ws = client.workspaces.find("Defaults") +env = client.environments.find("Production", ws.id) +segdef = client.rule_based_segment_definitions.find("advanced_users", env.id, ws.id) + +# New rules for the change request +rules = [ + { + 'condition': { + 'combiner': 'AND', + 'matchers': [ + { + 'type': 'GREATER_THAN_OR_EQUAL_NUMBER', + 'attribute': 'age', + 'number': 25 + }, + { + 'type': 'BOOLEAN', + 'attribute': 'completed_tutorials', + 'bool': True + } + ] + } + } +] + +# Define excluded keys and segments for the change request +excluded_keys = ['user1', 'user2'] +excluded_segments = [ + { + 'name': 'test_users', + 'type': 'rule_based_segment' + } +] + +# Submit change request with all parameters +segdef.submit_change_request( + rules=rules, + excluded_keys=excluded_keys, + excluded_segments=excluded_segments, + operation_type='UPDATE', + title='Lower age threshold to 25', + comment='Including more users in advanced segment', + approvers=['user@email.com'], + workspace_id=ws.id +) +``` + List all change requests: ```python diff --git a/pyproject.toml b/pyproject.toml index 68b6eb8..38fbdfe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "splitapiclient" -version = "3.5.0" +version = "3.5.1" description = "This Python Library provide full support for Split REST Admin API, allow creating, deleting and editing Environments, Splits, Split Definitions, Segments, Segment Keys, Users, Groups, API Keys, Change Requests, Attributes and Identities" classifiers = [ "Programming Language :: Python :: 3", diff --git a/splitapiclient/main/apiclient.py b/splitapiclient/main/apiclient.py index d014f76..78876f6 100644 --- a/splitapiclient/main/apiclient.py +++ b/splitapiclient/main/apiclient.py @@ -35,6 +35,14 @@ def segments(self): @abc.abstractproperty def segment_definitions(self): pass + + @abc.abstractproperty + def rule_based_segments(self): + pass + + @abc.abstractproperty + def rule_based_segment_definitions(self): + pass @abc.abstractproperty def workspaces(self): diff --git a/splitapiclient/main/sync_apiclient.py b/splitapiclient/main/sync_apiclient.py index 57e6733..174dc48 100644 --- a/splitapiclient/main/sync_apiclient.py +++ b/splitapiclient/main/sync_apiclient.py @@ -9,6 +9,8 @@ from splitapiclient.microclients import SplitDefinitionMicroClient from splitapiclient.microclients import SegmentMicroClient from splitapiclient.microclients import SegmentDefinitionMicroClient +from splitapiclient.microclients import RuleBasedSegmentMicroClient +from splitapiclient.microclients import RuleBasedSegmentDefinitionMicroClient from splitapiclient.microclients import WorkspaceMicroClient from splitapiclient.microclients import IdentityMicroClient from splitapiclient.microclients import AttributeMicroClient @@ -67,6 +69,8 @@ def __init__(self, config): self._split_definition_client = SplitDefinitionMicroClient(http_client) self._segment_client = SegmentMicroClient(http_client) self._segment_definition_client = SegmentDefinitionMicroClient(http_client) + self._rule_based_segment_client = RuleBasedSegmentMicroClient(http_client) + self._rule_based_segment_definition_client = RuleBasedSegmentDefinitionMicroClient(http_client) self._large_segment_client = LargeSegmentMicroClient(http_client) self._large_segment_definition_client = LargeSegmentDefinitionMicroClient(http_client) self._workspace_client = WorkspaceMicroClient(http_client) @@ -103,6 +107,14 @@ def segments(self): @property def segment_definitions(self): return self._segment_definition_client + + @property + def rule_based_segments(self): + return self._rule_based_segment_client + + @property + def rule_based_segment_definitions(self): + return self._rule_based_segment_definition_client @property def large_segments(self): diff --git a/splitapiclient/microclients/__init__.py b/splitapiclient/microclients/__init__.py index d94ca2e..39dd820 100644 --- a/splitapiclient/microclients/__init__.py +++ b/splitapiclient/microclients/__init__.py @@ -14,4 +14,6 @@ from splitapiclient.microclients.restriction_microclient import RestrictionMicroClient from splitapiclient.microclients.flag_set_microclient import FlagSetMicroClient from splitapiclient.microclients.large_segment_microclient import LargeSegmentMicroClient -from splitapiclient.microclients.large_segment_definition_microclient import LargeSegmentDefinitionMicroClient \ No newline at end of file +from splitapiclient.microclients.large_segment_definition_microclient import LargeSegmentDefinitionMicroClient +from splitapiclient.microclients.rule_based_segment_microclient import RuleBasedSegmentMicroClient +from splitapiclient.microclients.rule_based_segment_definition_microclient import RuleBasedSegmentDefinitionMicroClient \ No newline at end of file diff --git a/splitapiclient/microclients/rule_based_segment_definition_microclient.py b/splitapiclient/microclients/rule_based_segment_definition_microclient.py new file mode 100644 index 0000000..1aa3b48 --- /dev/null +++ b/splitapiclient/microclients/rule_based_segment_definition_microclient.py @@ -0,0 +1,150 @@ +from splitapiclient.resources import RuleBasedSegmentDefinition +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + +class RuleBasedSegmentDefinitionMicroClient: + ''' + MicroClient for rule-based segment definitions + ''' + _endpoint = { + 'all_items': { + 'method': 'GET', + 'url_template': 'rule-based-segments/ws/{workspaceId}/environments/{environmentId}?offset={offset}&limit={limit}', + 'headers': [{ + 'name': 'Authorization', + 'template': 'Bearer {value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'update': { + 'method': 'PUT', + 'url_template': 'rule-based-segments/ws/{workspaceId}/{segmentName}/environments/{environmentId}', + 'headers': [{ + 'name': 'Authorization', + 'template': 'Bearer {value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': 'rule-based-segments/{environmentId}/{segmentName}', + 'headers': [{ + 'name': 'Authorization', + 'template': 'Bearer {value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + } + } + + def __init__(self, http_client): + ''' + Constructor + ''' + self._http_client = http_client + + def list(self, environment_id, workspace_id, offset=0, limit=50): + ''' + Returns a list of RuleBasedSegment in environment objects with pagination support. + + :param environment_id: id of the environment + :param workspace_id: id of the workspace + :param offset: starting position for pagination (default: 0) + :param limit: maximum number of items to return (default: 50) + :param fetch_all: if True, fetches all pages and returns a consolidated list + :returns: list of RuleBasedSegment in environment objects + :rtype: list(RuleBasedSegmentDefinition) + ''' + segment_definition_list = [] + current_offset = offset + + while True: + response = self._http_client.make_request( + self._endpoint['all_items'], + workspaceId = workspace_id, + environmentId = environment_id, + offset = current_offset, + limit = limit + ) + + # Process the current page of results + current_page_items = [] + if isinstance(response, list): + for item in response: + item['environment'] = {'id':environment_id, 'name':''} + current_page_items.append(RuleBasedSegmentDefinition(item, self._http_client)) + + # Add current page items to the full list + segment_definition_list.extend(current_page_items) + + # If we reached the end + # (fewer items than limit), then break the loop + # or if we have more than limit items, then the pagination logic isn't implemented yet at the api + if len(current_page_items) < limit or len(current_page_items) > limit: + break + + # Otherwise move to the next page + current_offset += limit + + return segment_definition_list + + def find(self, segment_name, environment_id, workspace_id): + ''' + Find RuleBasedSegment in environment list objects. + + :param segment_name: name of the rule-based segment to find + :param environment_id: id of the environment + :param workspace_id: id of the workspace + :returns: RuleBasedSegmentDefinition object + :rtype: RuleBasedSegmentDefinition + ''' + for item in self.list(environment_id, workspace_id): + if item.name == segment_name: + return item + LOGGER.error("RuleBasedSegment Definition Name does not exist") + return None + + def update(self, segment_name, environment_id, workspace_id, data): + ''' + Update RuleBasedSegmentDefinition object. + + :param segment_name: name of the rule-based segment + :param environment_id: id of the environment + :param workspace_id: id of the workspace + :param data: dictionary of data to update + + :returns: RuleBasedSegmentDefinition object + :rtype: RuleBasedSegmentDefinition + ''' + response = self._http_client.make_request( + self._endpoint['update'], + body=as_dict(data), + workspaceId = workspace_id, + environmentId = environment_id, + segmentName = segment_name + ) + return RuleBasedSegmentDefinition(as_dict(response), self._http_client) + + def delete(self, segment_name, environment_id): + ''' + Delete RuleBasedSegmentDefinition object. + + :param segment_name: name of the rule-based segment + :param environment_id: id of the environment + + :returns: True if successful + :rtype: boolean + ''' + self._http_client.make_request( + self._endpoint['delete'], + environmentId = environment_id, + segmentName = segment_name + ) + return True diff --git a/splitapiclient/microclients/rule_based_segment_microclient.py b/splitapiclient/microclients/rule_based_segment_microclient.py new file mode 100644 index 0000000..28a6bab --- /dev/null +++ b/splitapiclient/microclients/rule_based_segment_microclient.py @@ -0,0 +1,199 @@ +from splitapiclient.resources import RuleBasedSegment +from splitapiclient.resources import RuleBasedSegmentDefinition +from splitapiclient.util.exceptions import HTTPResponseError, \ + UnknownApiClientError +from splitapiclient.util.logger import LOGGER +from splitapiclient.util.helpers import as_dict + +class RuleBasedSegmentMicroClient: + ''' + MicroClient for rule-based segments + ''' + _endpoint = { + 'create': { + 'method': 'POST', + 'url_template': ('rule-based-segments/ws/{workspaceId}/trafficTypes/{trafficTypeName}'), + 'headers': [{ + 'name': 'Authorization', + 'template': 'Bearer {value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'add_to_environment': { + 'method': 'POST', + 'url_template': ('rule-based-segments/{environmentId}/{segmentName}'), + 'headers': [{ + 'name': 'Authorization', + 'template': 'Bearer {value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'remove_from_environment': { + 'method': 'DELETE', + 'url_template': ('rule-based-segments/{environmentId}/{segmentName}'), + 'headers': [{ + 'name': 'Authorization', + 'template': 'Bearer {value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'delete': { + 'method': 'DELETE', + 'url_template': ('rule-based-segments/ws/{workspaceId}/{segmentName}'), + 'headers': [{ + 'name': 'Authorization', + 'template': 'Bearer {value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + 'all_items': { + 'method': 'GET', + 'url_template': 'rule-based-segments/ws/{workspaceId}?limit={limit}&offset={offset}', + 'headers': [{ + 'name': 'Authorization', + 'template': 'Bearer {value}', + 'required': True, + }], + 'query_string': [], + 'response': True, + }, + } + + def __init__(self, http_client): + ''' + Constructor + ''' + self._http_client = http_client + + def list(self, workspace_id, offset=0, limit=50): + ''' + Returns a list of RuleBasedSegment objects with pagination support. + + :param workspace_id: id of the workspace + :param offset: starting position for pagination (default: 0) + :param limit: maximum number of items to return (default: 50) + :returns: list of RuleBasedSegment objects + :rtype: list(RuleBasedSegment) + ''' + segment_list = [] + current_offset = offset + + while True: + response = self._http_client.make_request( + self._endpoint['all_items'], + workspaceId = workspace_id, + offset = current_offset, + limit = limit + ) + + # Process the current page of results + current_page_items = [] + if isinstance(response, list): + for item in response: + current_page_items.append(RuleBasedSegment(item, self._http_client)) + + # Add current page items to the full list + segment_list.extend(current_page_items) + + # If we reached the end (fewer items than limit), then break the loop + # or if we have more than limit items, then the pagination logic isn't implemented yet at the api + if len(current_page_items) < limit or len(current_page_items) > limit: + break + + # Otherwise move to the next page + current_offset += limit + + return segment_list + + def find(self, segment_name, workspace_id): + ''' + Find RuleBasedSegment in environment list objects. + + :returns: RuleBasedSegment objects + :rtype: RuleBasedSegment + ''' + for item in self.list(workspace_id): + if item.name == segment_name: + return item + LOGGER.error("RuleBasedSegment Name does not exist") + return None + + def add(self, segment, traffic_type_name, workspace_id): + ''' + add a rule-based segment + + :param segment: rule-based segment instance or dict + + :returns: newly created rule-based segment + :rtype: RuleBasedSegment + ''' + data = as_dict(segment) + response = self._http_client.make_request( + self._endpoint['create'], + body=data, + workspaceId = workspace_id, + trafficTypeName = traffic_type_name + ) + response['workspaceId'] = workspace_id + return RuleBasedSegment(response, self._http_client) + + def delete(self, segment_name, workspace_id): + ''' + delete a rule-based segment + + :param segment: rule-based segment instance or dict + + :returns: + :rtype: RuleBasedSegment + ''' + response = self._http_client.make_request( + self._endpoint['delete'], + workspaceId = workspace_id, + segmentName = segment_name + ) + return response + + def add_to_environment(self, segment_name, environment_id, workspace_id=None): + ''' + add a rule-based segment to environment + + :param segment_name: name of the rule-based segment + :param environment_id: id of the environment + :param workspace_id: id of the workspace (optional) + + :returns: newly created rule-based segment definition object + :rtype: RuleBasedSegmentDefinition + ''' + response = self._http_client.make_request( + self._endpoint['add_to_environment'], + body="", + segmentName = segment_name, + environmentId = environment_id, + + ) + return RuleBasedSegmentDefinition(response, self._http_client, workspace_id) + + def remove_from_environment(self, segment_name, environment_id): + ''' + remove a rule-based segment from environment + + :param segment: rule-based segment name, environment id + + :returns: http response + :rtype: boolean + ''' + response = self._http_client.make_request( + self._endpoint['remove_from_environment'], + body="", + segmentName = segment_name, + environmentId = environment_id + ) + return response diff --git a/splitapiclient/resources/__init__.py b/splitapiclient/resources/__init__.py index 33c740d..2d062b8 100644 --- a/splitapiclient/resources/__init__.py +++ b/splitapiclient/resources/__init__.py @@ -15,4 +15,6 @@ from splitapiclient.resources.restriction import Restriction from splitapiclient.resources.flag_set import FlagSet from splitapiclient.resources.large_segment import LargeSegment -from splitapiclient.resources.large_segment_definition import LargeSegmentDefinition \ No newline at end of file +from splitapiclient.resources.large_segment_definition import LargeSegmentDefinition +from splitapiclient.resources.rule_based_segment import RuleBasedSegment +from splitapiclient.resources.rule_based_segment_definition import RuleBasedSegmentDefinition \ No newline at end of file diff --git a/splitapiclient/resources/change_request.py b/splitapiclient/resources/change_request.py index 5d73fde..17d0f32 100644 --- a/splitapiclient/resources/change_request.py +++ b/splitapiclient/resources/change_request.py @@ -73,7 +73,30 @@ class ChangeRequest(BaseResource): }, 'creationTime': 'number', }, - + 'ruleBasedSegment':{ + 'name': 'string', + 'rules': [{ + 'condition': { + 'combiner': 'string', + 'matchers': [{ + 'type': 'string', + 'attribute': 'string', + 'string': 'string', + 'bool' : 'boolean', + 'strings' : [ 'string' ], + 'number' : 'number', + 'date' : 'number', + 'between': { 'from': 'number', 'to' : 'umber' }, + 'depends': { 'splitName': 'string', 'treatment': 'string' } + }] + } + }], + 'excludedKeys': ['string'], + 'excludedSegments': [{ + 'name': 'string', + 'type': 'string' + }] + }, 'id': 'string', 'status': 'string', 'title': 'string', diff --git a/splitapiclient/resources/rule_based_segment.py b/splitapiclient/resources/rule_based_segment.py new file mode 100644 index 0000000..0abc391 --- /dev/null +++ b/splitapiclient/resources/rule_based_segment.py @@ -0,0 +1,90 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict +from splitapiclient.resources import TrafficType + +class RuleBasedSegment(BaseResource): + ''' + Resource class for rule-based segments + ''' + _schema = { + 'name': 'string', + 'description': 'string', + 'trafficType' : { + 'id': 'string', + 'name': 'string' + }, + 'workspaceId' : 'string', + 'creationTime' : 'number', + 'tags': [{'name': 'string'}] + } + + def __init__(self, data=None, client=None): + ''' + Constructor for RuleBasedSegment + ''' + if not data: + data = {} + BaseResource.__init__(self, data.get('name'), client) + self._name = data.get('name') + self._description = data.get('description') + self._trafficType = TrafficType(data.get('trafficType')) if 'trafficType' in data else {} + self._workspace_id = data.get('workspaceId') + self._tags = data.get('tags') if 'tags' in data else [] + self._creationTime = data.get('creationTime') if 'creationTime' in data else 0 + + + @property + def name(self): + return self._name + + @property + def description(self): + return self._description + + @property + def traffic_type(self): + return None if self._trafficType == {} else self._trafficType + + @property + def workspace_id(self): + return self._workspace_id + + @property + def tags(self): + return self._tags + + @property + def creation_time(self): + return self._creationTime + + def add_to_environment(self, environment_id, apiclient=None): + ''' + Add rule-based segment to environment + + :param data: environment id + :param apiclient: If this instance wasn't returned by the client, + the IdentifyClient instance should be passed in order to perform the + http call + + :returns: RuleBasedSegmentDefinition instance + :rtype: RuleBasedSegmentDefinition + ''' + imc = require_client('RuleBasedSegment', self._client, apiclient) + return imc.add_to_environment(self._name, environment_id=environment_id, workspace_id=self._workspace_id) + + def remove_from_environment(self, environment_id, apiclient=None): + ''' + Remove rule-based segment from environment + + :param data: environment id + :param apiclient: If this instance wasn't returned by the client, + the IdentifyClient instance should be passed in order to perform the + http call + + :returns: True if successful + :rtype: Boolean + ''' + imc = require_client('RuleBasedSegment', self._client, apiclient) + return imc.remove_from_environment(self._name, environment_id) diff --git a/splitapiclient/resources/rule_based_segment_definition.py b/splitapiclient/resources/rule_based_segment_definition.py new file mode 100644 index 0000000..bf78445 --- /dev/null +++ b/splitapiclient/resources/rule_based_segment_definition.py @@ -0,0 +1,165 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals +from splitapiclient.resources.base_resource import BaseResource +from splitapiclient.util.helpers import require_client, as_dict +from splitapiclient.resources import TrafficType +from splitapiclient.resources import Environment +import csv + +class RuleBasedSegmentDefinition(BaseResource): + ''' + Resource class for rule-based segment definitions + ''' + _schema = { + 'name': 'string', + 'environment': { + 'id': 'string', + 'name':'string' + }, + 'trafficType' : { + 'id': 'string', + 'name': 'string' + }, + 'creationTime' : 'number', + 'excludedKeys' : [ 'string' ], + 'excludedSegments' : [{ + 'name': 'string', + 'type': 'string' + }], + 'rules' : [{ + 'condition': { + 'combiner': 'string', + 'matchers': [{ + 'type': 'string', + 'attribute': 'string', + 'string': 'string', + 'bool' : 'boolean', + 'strings' : [ 'string' ], + 'number' : 'number', + 'date' : 'number', + 'between': { 'from': 'number', 'to' : 'umber' }, + 'depends': { 'splitName': 'string', 'treatment': 'string' } + }] + } + }] + } + + def __init__(self, data=None, client=None, workspace_id=None): + ''' + Constructor for RuleBasedSegmentDefinition + ''' + if not data: + data = {} + BaseResource.__init__(self, data.get('name'), client) + self._name = data.get('name') + self._environment = data.get('environment') + self._trafficType = TrafficType(data.get('trafficType')) if 'trafficType' in data else {} + self._creationTime = data.get('creationTime') if 'creationTime' in data else 0 + self._excludedKeys = data.get('excludedKeys', []) + self._excludedSegments = data.get('excludedSegments', []) + self._rules = data.get('rules', []) + self._workspace_id = workspace_id + + + @property + def name(self): + return self._name + + @property + def traffic_type(self): + return None if self._trafficType == {} else self._trafficType + + @property + def environment(self): + return self._environment + + @property + def tags(self): + return self._tags + + @property + def creation_time(self): + return None if self._creationTime==0 else self._creationTime + + @property + def excluded_keys(self): + return self._excludedKeys + + @property + def excluded_segments(self): + return self._excludedSegments + + @property + def rules(self): + return self._rules + + def delete(self, apiclient=None): + ''' + Delete RuleBasedSegmentDefinition object. + + :param apiclient: If this instance wasn't returned by the client, + the ApiClient instance should be passed in order to perform the + http call + + :returns: True if successful + :rtype: boolean + ''' + imc = require_client('RuleBasedSegmentDefinition', self._client, apiclient) + return imc.delete(self._name, self._environment['id']) + + + def update(self, data, workspace_id=None, apiclient=None): + ''' + Update RuleBasedSegmentDefinition object. + + :param data: dictionary of data to update + :param workspace_id: id of the workspace + :param apiclient: If this instance wasn't returned by the client, + the ApiClient instance should be passed in order to perform the + http call + + :returns: RuleBasedSegmentDefinition object + :rtype: RuleBasedSegmentDefinition + ''' + if not workspace_id: + workspace_id = self._workspace_id + + if workspace_id is None: + raise ValueError("workspace_id is required argument") + imc = require_client('RuleBasedSegmentDefinition', self._client, apiclient) + return imc.update(self._name, self._environment['id'], workspace_id, data) + + def submit_change_request(self, rules, excluded_keys, excluded_segments, operation_type, title, comment, approvers, workspace_id, apiclient=None): + ''' + submit a change request for rule-based segment definition + + :param rules: dictionary of rules to update + :param excluded_keys: list of excluded keys + :param excluded_segments: list of excluded segments + :param operation_type: operation type + :param title: title of the change request + :param comment: comment for the change request + :param approvers: list of approvers + :param workspace_id: id of the workspace + :param apiclient: If this instance wasn't returned by the client, + the IdentifyClient instance should be passed in order to perform the + http call + + :returns: ChangeRequest object + :rtype: ChangeRequest + ''' + data = { + 'ruleBasedSegment': { + 'name':self._name, + 'rules': rules, + 'excludedKeys': excluded_keys, + 'excludedSegments': excluded_segments + }, + 'operationType': operation_type, + 'title': title, + 'comment': comment, + 'approvers': approvers, + } + + imc = require_client('ChangeRequest', self._client, apiclient) + return imc.submit_change_request(self._environment['id'], workspace_id, data) diff --git a/splitapiclient/resources/segment_definition.py b/splitapiclient/resources/segment_definition.py index a31c1cd..2a5f538 100644 --- a/splitapiclient/resources/segment_definition.py +++ b/splitapiclient/resources/segment_definition.py @@ -103,7 +103,23 @@ def import_keys_from_json(self, replace_keys, json_data, apiclient=None): :rtype: boolean ''' imc = require_client('SegmentDefinition', self._client, apiclient) - return imc.import_keys_from_json(self._name, self._environment['id'], replace_keys, json_data) + keys = json_data['keys'] + if(len(keys) > 10000): + # Split keys into batches of 10,000 + key_batches = [keys[i:i + 10000] for i in range(0, len(keys), 10000)] + success = True + # Process each batch + for key_batch in key_batches: + # Make a copy of the json_data to avoid modifying the original + batch_data = json_data.copy() + batch_data['keys'] = key_batch + # If any batch fails, mark the entire operation as failed + batch_result = imc.import_keys_from_json(self._name, self._environment['id'], replace_keys, batch_data) + if not batch_result: + success = False + return success + else: + return imc.import_keys_from_json(self._name, self._environment['id'], replace_keys, json_data) def remove_keys(self, json_data, apiclient=None): ''' diff --git a/splitapiclient/resources/workspace.py b/splitapiclient/resources/workspace.py index 16526c0..fa77b03 100644 --- a/splitapiclient/resources/workspace.py +++ b/splitapiclient/resources/workspace.py @@ -103,6 +103,31 @@ def delete_large_segment(self, large_segment_name, apiclient=None): workspaceId = self._id return imc.delete(large_segment_name, workspaceId) + def add_rule_based_segment(self, data, traffic_type_name, apiclient=None): + ''' + Add a new rule-based segment associated with this workspace. + + :param apiclient: If this instance wasn't returned by the client, + the ruleBasedSegmentClient instance should be passed in order to perform the + http call + ''' + imc = require_client('RuleBasedSegment', self._client, apiclient) + segment = as_dict(data) + workspaceId = self._id + return imc.add(segment, traffic_type_name, workspaceId) + + def delete_rule_based_segment(self, segment_name, apiclient=None): + ''' + delete rule-based segment associated with this workspace. + + :param apiclient: If this instance wasn't returned by the client, + the ruleBasedSegmentClient instance should be passed in order to perform the + http call + ''' + imc = require_client('RuleBasedSegment', self._client, apiclient) + workspaceId = self._id + return imc.delete(segment_name, workspaceId) + def add_split(self, data, traffic_type_name, apiclient=None): ''' diff --git a/splitapiclient/tests/microclients/rule_based_segment_definition_microclient_test.py b/splitapiclient/tests/microclients/rule_based_segment_definition_microclient_test.py new file mode 100644 index 0000000..825e4a0 --- /dev/null +++ b/splitapiclient/tests/microclients/rule_based_segment_definition_microclient_test.py @@ -0,0 +1,168 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients import RuleBasedSegmentDefinitionMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestRuleBasedSegmentDefinitionMicroClient: + + def test_list_single_page(self, mocker): + ''' + Test listing rule-based segment definitions + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbsd_mc = RuleBasedSegmentDefinitionMicroClient(sc) + + # Mock response with objects + response = [{ + 'name': 'rule_seg1', + 'trafficType': {'id': 'tt_123', 'name': 'user'}, + 'creationTime': 1234567890, + }, { + 'name': 'rule_seg2', + 'trafficType': {'id': 'tt_123', 'name': 'user'}, + 'creationTime': 1234567891, + }] + + # Set up the make_request mock + SyncHttpClient.make_request.return_value = response + + result = rbsd_mc.list('env_123', 'ws_id') + + # Should be called once with default pagination parameters + assert SyncHttpClient.make_request.call_count == 1 + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentDefinitionMicroClient._endpoint['all_items'], + workspaceId='ws_id', + environmentId='env_123', + offset=0, + limit=50 + ) + + # Verify the first item in the result + assert result[0].name == 'rule_seg1' + assert result[0].environment['id'] == 'env_123' + assert result[0].traffic_type.name == 'user' + assert result[0].creation_time == 1234567890 + + # Verify the second item in the result + assert result[1].name == 'rule_seg2' + assert result[1].environment['id'] == 'env_123' + assert result[1].traffic_type.name == 'user' + assert result[1].creation_time == 1234567891 + + def test_find(self, mocker): + ''' + Test finding a rule-based segment definition by name + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbsd_mc = RuleBasedSegmentDefinitionMicroClient(sc) + + # Mock response containing the target segment + first_page_response = [{ + 'name': 'rule_seg1', + 'trafficType': {'id': 'tt_123', 'name': 'user'}, + 'creationTime': 1234567890, + }, { + 'name': 'rule_seg2', + 'trafficType': {'id': 'tt_123', 'name': 'user'}, + 'creationTime': 1234567891, + }] + + # Set up the make_request mock + SyncHttpClient.make_request.return_value = first_page_response + + result = rbsd_mc.find('rule_seg2', 'env_123', 'ws_id') + + # Will make at least one request + assert SyncHttpClient.make_request.call_count >= 1 + + # First call should request with pagination parameters + SyncHttpClient.make_request.assert_called_with( + RuleBasedSegmentDefinitionMicroClient._endpoint['all_items'], + workspaceId='ws_id', + environmentId='env_123', + offset=0, + limit=50 + ) + + # Verify the result + assert result.name == 'rule_seg2' + assert result.environment['id'] == 'env_123' + assert result.traffic_type.name == 'user' + assert result.creation_time == 1234567891 + + def test_find_not_found(self, mocker): + ''' + Test finding a rule-based segment definition by name when it doesn't exist + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbsd_mc = RuleBasedSegmentDefinitionMicroClient(sc) + + # Empty response to simulate no matching segments + empty_response = [] + + # Set up the make_request mock + SyncHttpClient.make_request.return_value = empty_response + + result = rbsd_mc.find('rule_seg_nonexistent', 'env_123', 'ws_id') + + # Will make at least one request + assert SyncHttpClient.make_request.call_count >= 1 + + # Result should be None since segment wasn't found + assert result is None + + def test_update(self, mocker): + ''' + Test updating a rule-based segment definition + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbsd_mc = RuleBasedSegmentDefinitionMicroClient(sc) + + update_data = { + 'rules': [ + { + 'condition': { + 'combiner': 'AND', + 'matchers': [ + { + 'type': 'EQUAL_TO', + 'attribute': 'age', + 'number': 40 + } + ] + } + } + ] + } + + response_data = { + 'name': 'rule_seg1', + 'environment': {'id': 'env_123', 'name': 'Production'}, + 'trafficType': {'id': 'tt_123', 'name': 'user'}, + 'creationTime': 1234567890, + 'rules': update_data['rules'] + } + + SyncHttpClient.make_request.return_value = response_data + result = rbsd_mc.update('rule_seg1', 'env_123', 'ws_id', update_data) + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentDefinitionMicroClient._endpoint['update'], + body=update_data, + workspaceId='ws_id', + environmentId='env_123', + segmentName='rule_seg1' + ) + + # Verify the result + assert result.name == 'rule_seg1' + assert result.environment['id'] == 'env_123' + assert result.traffic_type.name == 'user' + assert result.creation_time == 1234567890 diff --git a/splitapiclient/tests/microclients/rule_based_segment_microclient_test.py b/splitapiclient/tests/microclients/rule_based_segment_microclient_test.py new file mode 100644 index 0000000..91d47e5 --- /dev/null +++ b/splitapiclient/tests/microclients/rule_based_segment_microclient_test.py @@ -0,0 +1,319 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients import RuleBasedSegmentMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestRuleBasedSegmentMicroClient: + + def test_list_single_page(self, mocker): + ''' + Test listing rule-based segments (single page) + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + # First response with objects + first_response = [{ + 'name': 'rule_seg1', + 'description': 'rule based segment description', + 'creationTime': 1234567890, + 'tags': [{'name': 'tag1'}] + }, { + 'name': 'rule_seg2', + 'description': 'another rule based segment', + 'creationTime': 1234567891, + 'tags': [{'name': 'tag2'}] + }] + + # Empty response (less than limit items, so pagination stops) + empty_response = [] + + # Set up the make_request mock to return different values on each call + SyncHttpClient.make_request.side_effect = [first_response, empty_response] + + result = rbs_mc.list('ws_id') + + # Should be called once + assert SyncHttpClient.make_request.call_count >= 1 + assert SyncHttpClient.make_request.call_args_list[0] == mocker.call( + RuleBasedSegmentMicroClient._endpoint['all_items'], + workspaceId='ws_id', + offset=0, + limit=50 + ) + + # Verify results by checking properties individually + assert len(result) == 2 + + # Check first segment + assert result[0].name == 'rule_seg1' + assert result[0].description == 'rule based segment description' + assert result[0].creation_time == 1234567890 + assert len(result[0].tags) == 1 + assert result[0].tags[0]['name'] == 'tag1' + + # Check second segment + assert result[1].name == 'rule_seg2' + assert result[1].description == 'another rule based segment' + assert result[1].creation_time == 1234567891 + assert len(result[1].tags) == 1 + assert result[1].tags[0]['name'] == 'tag2' + + # Check first segment + assert result[0].name == 'rule_seg1' + assert result[0].description == 'rule based segment description' + assert result[0].creation_time == 1234567890 + assert len(result[0].tags) == 1 + assert result[0].tags[0]['name'] == 'tag1' + + # Check second segment + assert result[1].name == 'rule_seg2' + assert result[1].description == 'another rule based segment' + assert result[1].creation_time == 1234567891 + assert len(result[1].tags) == 1 + assert result[1].tags[0]['name'] == 'tag2' + + def test_find(self, mocker): + ''' + Test finding a rule-based segment by name + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + # First response with objects including the target segment + first_response = [{ + 'name': 'rule_seg1', + 'description': 'rule based segment description', + 'creationTime': 1234567890, + 'tags': [{'name': 'tag1'}] + }, { + 'name': 'rule_seg2', + 'description': 'another rule based segment', + 'creationTime': 1234567891, + 'tags': [{'name': 'tag2'}] + }] + + # Empty response (less than limit items, so pagination stops) + empty_response = [] + + # Set up the make_request mock + SyncHttpClient.make_request.side_effect = [first_response, empty_response] + + result = rbs_mc.find('rule_seg2', 'ws_id') + + # Should make requests to get all segments + assert SyncHttpClient.make_request.call_count >= 1 + assert SyncHttpClient.make_request.call_args_list[0] == mocker.call( + RuleBasedSegmentMicroClient._endpoint['all_items'], + workspaceId='ws_id', + offset=0, + limit=50 + ) + + # Verify the result by checking properties individually + assert result is not None + assert result.name == 'rule_seg2' + assert result.description == 'another rule based segment' + assert result.creation_time == 1234567891 + assert result.tags[0]['name'] == 'tag2' + + def test_list_multiple_pages(self, mocker): + ''' + Test listing rule-based segments with multiple pages + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + # First page response + first_response = [{ + 'name': 'rule_seg1', + 'description': 'rule based segment description', + 'creationTime': 1234567890, + 'tags': [{'name': 'tag1'}] + }] + + # Second page response + second_response = [{ + 'name': 'rule_seg2', + 'description': 'another rule based segment', + 'creationTime': 1234567891, + 'tags': [{'name': 'tag2'}] + }] + + # Empty response (less than limit items, so pagination stops) + empty_response = [] + + # Set up the make_request mock to return different values on each call + SyncHttpClient.make_request.side_effect = [first_response, second_response, empty_response] + + result = rbs_mc.list('ws_id', offset=0, limit=1) + + # The implementation now uses a loop internally to fetch pages + # Be lenient on call count since mock responses might not trigger expected pagination behavior + assert SyncHttpClient.make_request.call_count >= 1 + + # First call should be for first page + assert SyncHttpClient.make_request.call_args_list[0] == mocker.call( + RuleBasedSegmentMicroClient._endpoint['all_items'], + workspaceId='ws_id', + offset=0, + limit=1 + ) + + # Only check subsequent calls if they were made + if SyncHttpClient.make_request.call_count > 1: + # Second call should be for second page + assert SyncHttpClient.make_request.call_args_list[1] == mocker.call( + RuleBasedSegmentMicroClient._endpoint['all_items'], + workspaceId='ws_id', + offset=1, + limit=1 + ) + + if SyncHttpClient.make_request.call_count > 2: + # Third call should be for third page + assert SyncHttpClient.make_request.call_args_list[2] == mocker.call( + RuleBasedSegmentMicroClient._endpoint['all_items'], + workspaceId='ws_id', + offset=2, + limit=1 + ) + + # Verify results - should include items from both pages + assert len(result) == 2 + + # Check first segment + assert result[0].name == 'rule_seg1' + assert result[0].description == 'rule based segment description' + assert result[0].creation_time == 1234567890 + assert len(result[0].tags) == 1 + assert result[0].tags[0]['name'] == 'tag1' + + # Check second segment + assert result[1].name == 'rule_seg2' + assert result[1].description == 'another rule based segment' + assert result[1].creation_time == 1234567891 + assert len(result[1].tags) == 1 + assert result[1].tags[0]['name'] == 'tag2' + + def test_add(self, mocker): + ''' + Test adding a rule-based segment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + segment_data = { + 'name': 'new_rule_seg', + 'description': 'new rule based segment', + 'tags': [{'name': 'tag3'}] + } + + response_data = { + 'name': 'new_rule_seg', + 'description': 'new rule based segment', + 'creationTime': 1234567892, + 'trafficType': {'id': 'tt_123', 'name': 'user'}, + 'tags': [{'name': 'tag3'}] + } + + SyncHttpClient.make_request.return_value = response_data + result = rbs_mc.add(segment_data, 'user', 'ws_id') + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['create'], + body=segment_data, + workspaceId='ws_id', + trafficTypeName='user' + ) + + # Test individual properties instead of the entire to_dict() + assert result.name == 'new_rule_seg' + assert result.description == 'new rule based segment' + assert result.traffic_type is not None + assert result.traffic_type.name == 'user' + assert result.workspace_id == 'ws_id' + assert result.creation_time == 1234567892 + assert len(result.tags) == 1 + assert result.tags[0]['name'] == 'tag3' + + def test_delete(self, mocker): + ''' + Test deleting a rule-based segment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + response_data = {'success': True} + SyncHttpClient.make_request.return_value = response_data + + result = rbs_mc.delete('rule_seg1', 'ws_id') + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['delete'], + workspaceId='ws_id', + segmentName='rule_seg1' + ) + + assert result == response_data + + def test_add_to_environment(self, mocker): + ''' + Test adding a rule-based segment to environment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + response_data = { + 'name': 'rule_seg1', + 'environment': {'id': 'env_123', 'name': 'Production'}, + 'trafficType': {'id': 'tt_123', 'name': 'user'} + } + + SyncHttpClient.make_request.return_value = response_data + result = rbs_mc.add_to_environment('rule_seg1', 'env_123') + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['add_to_environment'], + body="", + segmentName='rule_seg1', + environmentId='env_123' + ) + + # Instead of comparing the entire result.to_dict(), check individual properties + assert result.name == 'rule_seg1' + assert result.environment['id'] == 'env_123' + assert result.environment['name'] == 'Production' + assert result.traffic_type is not None + # Check that the traffic type has the expected properties + assert result.traffic_type.name == 'user' + + def test_remove_from_environment(self, mocker): + ''' + Test removing a rule-based segment from environment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + response_data = {'success': True} + SyncHttpClient.make_request.return_value = response_data + + result = rbs_mc.remove_from_environment('rule_seg1', 'env_123') + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['remove_from_environment'], + body="", + segmentName='rule_seg1', + environmentId='env_123' + ) + + assert result == response_data diff --git a/splitapiclient/tests/microclients/rule_based_segment_microclient_test_updated.py b/splitapiclient/tests/microclients/rule_based_segment_microclient_test_updated.py new file mode 100644 index 0000000..c4a2379 --- /dev/null +++ b/splitapiclient/tests/microclients/rule_based_segment_microclient_test_updated.py @@ -0,0 +1,219 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.microclients import RuleBasedSegmentMicroClient +from splitapiclient.http_clients.sync_client import SyncHttpClient + + +class TestRuleBasedSegmentMicroClient: + + def test_list_single_page(self, mocker): + ''' + Test listing rule-based segments (single page) + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + # Response with objects + response = [{ + 'name': 'rule_seg1', + 'description': 'rule based segment description', + 'creationTime': 1234567890, + 'tags': [{'name': 'tag1'}] + }, { + 'name': 'rule_seg2', + 'description': 'another rule based segment', + 'creationTime': 1234567891, + 'tags': [{'name': 'tag2'}] + }] + + # Set up the make_request mock to return the response + SyncHttpClient.make_request.return_value = response + + result = rbs_mc.list('ws_id') + + # Should be called once with pagination parameters + assert SyncHttpClient.make_request.call_count >= 1 + SyncHttpClient.make_request.assert_called_with( + RuleBasedSegmentMicroClient._endpoint['all_items'], + workspaceId='ws_id', + offset=0, + limit=50 + ) + + # Verify results by checking properties individually + assert len(result) == 2 + + # Check first segment + assert result[0].name == 'rule_seg1' + assert result[0].description == 'rule based segment description' + assert result[0].creation_time == 1234567890 + assert len(result[0].tags) == 1 + assert result[0].tags[0]['name'] == 'tag1' + + # Check second segment + assert result[1].name == 'rule_seg2' + assert result[1].description == 'another rule based segment' + assert result[1].creation_time == 1234567891 + assert len(result[1].tags) == 1 + assert result[1].tags[0]['name'] == 'tag2' + + def test_find(self, mocker): + ''' + Test finding a rule-based segment by name + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + # Response with objects including the target segment + response = [{ + 'name': 'rule_seg1', + 'description': 'rule based segment description', + 'creationTime': 1234567890, + 'tags': [{'name': 'tag1'}] + }, { + 'name': 'rule_seg2', + 'description': 'another rule based segment', + 'creationTime': 1234567891, + 'tags': [{'name': 'tag2'}] + }] + + # Set up the make_request mock + SyncHttpClient.make_request.return_value = response + + result = rbs_mc.find('rule_seg2', 'ws_id') + + # Should make at least one request to get segments + assert SyncHttpClient.make_request.call_count >= 1 + SyncHttpClient.make_request.assert_called_with( + RuleBasedSegmentMicroClient._endpoint['all_items'], + workspaceId='ws_id', + offset=0, + limit=50 + ) + + # Verify the result by checking properties individually + assert result is not None + assert result.name == 'rule_seg2' + assert result.description == 'another rule based segment' + assert result.creation_time == 1234567891 + assert result.tags[0]['name'] == 'tag2' + + def test_add(self, mocker): + ''' + Test adding a rule-based segment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + segment_data = { + 'name': 'new_rule_seg', + 'description': 'new rule based segment', + 'tags': [{'name': 'tag3'}] + } + + response_data = { + 'name': 'new_rule_seg', + 'description': 'new rule based segment', + 'creationTime': 1234567892, + 'trafficType': {'id': 'tt_123', 'name': 'user'}, + 'tags': [{'name': 'tag3'}] + } + + SyncHttpClient.make_request.return_value = response_data + result = rbs_mc.add(segment_data, 'user', 'ws_id') + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['create'], + body=segment_data, + workspaceId='ws_id', + trafficTypeName='user' + ) + + # Test individual properties instead of the entire to_dict() + assert result.name == 'new_rule_seg' + assert result.description == 'new rule based segment' + assert result.traffic_type is not None + assert result.traffic_type.name == 'user' + assert result.workspace_id == 'ws_id' + assert result.creation_time == 1234567892 + assert len(result.tags) == 1 + assert result.tags[0]['name'] == 'tag3' + + def test_delete(self, mocker): + ''' + Test deleting a rule-based segment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + response_data = {'success': True} + SyncHttpClient.make_request.return_value = response_data + + result = rbs_mc.delete('rule_seg1', 'ws_id') + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['delete'], + workspaceId='ws_id', + segmentName='rule_seg1' + ) + + assert result == response_data + + def test_add_to_environment(self, mocker): + ''' + Test adding a rule-based segment to environment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + response_data = { + 'name': 'rule_seg1', + 'environment': {'id': 'env_123', 'name': 'Production'}, + 'trafficType': {'id': 'tt_123', 'name': 'user'} + } + + SyncHttpClient.make_request.return_value = response_data + result = rbs_mc.add_to_environment('rule_seg1', 'env_123') + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['add_to_environment'], + body="", + segmentName='rule_seg1', + environmentId='env_123' + ) + + # Check individual properties + assert result.name == 'rule_seg1' + assert result.environment is not None + assert result.environment['id'] == 'env_123' + assert result.environment['name'] == 'Production' + assert result.traffic_type is not None + assert result.traffic_type.name == 'user' + + def test_remove_from_environment(self, mocker): + ''' + Test removing a rule-based segment from an environment + ''' + mocker.patch('splitapiclient.http_clients.sync_client.SyncHttpClient.make_request') + sc = SyncHttpClient('abc', 'abc') + rbs_mc = RuleBasedSegmentMicroClient(sc) + + response_data = {'success': True} + SyncHttpClient.make_request.return_value = response_data + + result = rbs_mc.remove_from_environment('rule_seg1', 'env_123') + + SyncHttpClient.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['remove_from_environment'], + body='', + segmentName='rule_seg1', + environmentId='env_123' + ) + + assert result == response_data diff --git a/splitapiclient/tests/resources/test_change_request.py b/splitapiclient/tests/resources/test_change_request.py new file mode 100644 index 0000000..76f5db7 --- /dev/null +++ b/splitapiclient/tests/resources/test_change_request.py @@ -0,0 +1,163 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.resources import ChangeRequest +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.base_client import BaseHttpClient +from splitapiclient.main import get_client +from splitapiclient.microclients import ChangeRequestMicroClient +import pytest + +class TestChangeRequest: + ''' + Tests for the ChangeRequest class' methods + ''' + @pytest.fixture + def sample_split_change_request(self): + '''Fixture providing sample split change request data''' + return { + 'id': 'cr123', + 'status': 'PENDING', + 'title': 'Update split configurations', + 'comment': 'Updating split to improve customer experience', + 'split': { + 'name': 'feature_toggle', + 'environment': { + 'id': 'env_123', + 'name': 'Production' + } + }, + 'operationType': 'UPDATE', + 'approvers': ['user1@example.com', 'user2@example.com'] + } + + @pytest.fixture + def sample_segment_change_request(self): + '''Fixture providing sample segment change request data''' + return { + 'id': 'cr124', + 'status': 'PENDING', + 'title': 'Update segment keys', + 'comment': 'Adding new users to the segment', + 'segment': { + 'name': 'premium_users', + 'keys': ['user1', 'user2', 'user3'] + }, + 'operationType': 'UPDATE', + 'approvers': ['user1@example.com'] + } + + @pytest.fixture + def sample_rule_based_segment_change_request(self): + '''Fixture providing sample rule-based segment change request data''' + return { + 'id': 'cr125', + 'status': 'PENDING', + 'title': 'Update rule-based segment rules', + 'comment': 'Changing user criteria', + 'ruleBasedSegment': { + 'name': 'advanced_users', + 'rules': [ + { + 'condition': { + 'combiner': 'AND', + 'matchers': [ + { + 'type': 'GREATER_THAN_OR_EQUAL_TO', + 'attribute': 'age', + 'number': 25 + } + ] + } + } + ] + }, + 'operationType': 'UPDATE', + 'approvers': ['user1@example.com'] + } + + def test_constructor(self, mocker, sample_split_change_request): + ''' + Test the constructor of ChangeRequest with split data + ''' + client = object() + mock_init = mocker.Mock() + mocker.patch( + 'splitapiclient.resources.base_resource.BaseResource.__init__', + new=mock_init + ) + + change_request = ChangeRequest(sample_split_change_request, client) + + from splitapiclient.resources.base_resource import BaseResource + BaseResource.__init__.assert_called_once_with(change_request, 'cr123', client) + + assert change_request._id == 'cr123' + assert change_request._status == 'PENDING' + assert change_request._title == 'Update split configurations' + assert change_request._comment == 'Updating split to improve customer experience' + assert change_request._split['name'] == 'feature_toggle' + assert change_request._operationType == 'UPDATE' + assert len(change_request._approvers) == 2 + assert 'user1@example.com' in change_request._approvers + assert 'user2@example.com' in change_request._approvers + + def test_constructor_with_segment_data(self, sample_segment_change_request): + ''' + Test the constructor of ChangeRequest with segment data + ''' + change_request = ChangeRequest(sample_segment_change_request) + + assert change_request._id == 'cr124' + assert change_request._status == 'PENDING' + assert change_request._title == 'Update segment keys' + assert change_request._comment == 'Adding new users to the segment' + assert change_request._segment['name'] == 'premium_users' + assert len(change_request._segment['keys']) == 3 + assert change_request._operationType == 'UPDATE' + assert len(change_request._approvers) == 1 + assert change_request._approvers[0] == 'user1@example.com' + + def test_constructor_with_rule_based_segment_data(self, sample_rule_based_segment_change_request): + ''' + Test the constructor of ChangeRequest with rule-based segment data + ''' + change_request = ChangeRequest(sample_rule_based_segment_change_request) + + assert change_request._id == 'cr125' + assert change_request._status == 'PENDING' + assert change_request._title == 'Update rule-based segment rules' + assert change_request._comment == 'Changing user criteria' + assert change_request._operationType == 'UPDATE' + + # Note: Currently the change_request.py file doesn't have a field for ruleBasedSegment + # This test assumes ruleBasedSegment is handled by the existing fields like ._segment + # This might need to be updated if the ChangeRequest class is modified to specifically handle ruleBasedSegment + + def test_update_status(self, mocker, sample_split_change_request): + ''' + Test updating the status of a change request + ''' + http_client_mock = mocker.Mock(spec=BaseHttpClient) + response_data = { + 'id': 'cr123', + 'status': 'APPROVED', + 'title': 'Update split configurations', + 'comment': 'Updating split to improve customer experience' + } + http_client_mock.make_request.return_value = response_data + + change_request = ChangeRequest(sample_split_change_request, http_client_mock) + result = change_request.update_status('APPROVED', 'Looks good!') + + http_client_mock.make_request.assert_called_once_with( + ChangeRequestMicroClient._endpoint['update_status'], + changeRequestId='cr123', + body={ + 'status': 'APPROVED', + 'comment': 'Looks good!' + } + ) + + assert result._id == 'cr123' + assert result._status == 'APPROVED' diff --git a/splitapiclient/tests/resources/test_rule_based_segment.py b/splitapiclient/tests/resources/test_rule_based_segment.py new file mode 100644 index 0000000..118c0e6 --- /dev/null +++ b/splitapiclient/tests/resources/test_rule_based_segment.py @@ -0,0 +1,125 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from splitapiclient.resources import RuleBasedSegment +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.base_client import BaseHttpClient +from splitapiclient.main import get_client +from splitapiclient.microclients import RuleBasedSegmentMicroClient +import pytest + +class TestRuleBasedSegment: + ''' + Tests for the RuleBasedSegment class' methods + ''' + @pytest.fixture + def sample_data(self): + '''Fixture providing sample rule-based segment data''' + return { + 'name': 'rule_segment1', + 'description': 'description1', + 'trafficType': {'id': '1', 'name': 'traffic1'}, + 'workspaceId': 'workspace1', + 'creationTime': 1234567890, + 'tags': [{'name': 'tag1'}, {'name': 'tag2'}] + } + + def test_constructor(self, mocker, sample_data): + ''' + Test the constructor of RuleBasedSegment + ''' + client = object() + + # We're not mocking BaseResource.__init__ anymore because it's called multiple times + # (once for RuleBasedSegment and once for TrafficType) + seg = RuleBasedSegment(sample_data, client) + + # Instead, verify the properties are set correctly + assert seg._name == 'rule_segment1' + assert seg._description == 'description1' + assert seg._trafficType.id == '1' + assert seg._trafficType.name == 'traffic1' + assert seg._workspace_id == 'workspace1' + assert seg._creationTime == 1234567890 + assert seg._tags == [{'name': 'tag1'}, {'name': 'tag2'}] + + def test_constructor_with_missing_fields(self): + '''Test the constructor with partial or missing data''' + partial_data = { + 'name': 'rule_segment1', + 'description': 'description1' + # Missing other fields + } + segment = RuleBasedSegment(partial_data, None) + assert segment.name == 'rule_segment1' + assert segment.description == 'description1' + assert segment.traffic_type is None + assert segment.workspace_id == None + assert segment.creation_time == 0 + assert segment.tags == [] + + def test_getters(self, sample_data): + ''' + Test the getters of RuleBasedSegment + ''' + seg = RuleBasedSegment(sample_data) + assert seg.name == 'rule_segment1' + assert seg.description == 'description1' + assert seg.traffic_type.name == 'traffic1' + assert seg.workspace_id == 'workspace1' + assert seg.creation_time == 1234567890 + assert seg.tags == [{'name': 'tag1'}, {'name': 'tag2'}] + + def test_add_to_environment(self, mocker, sample_data): + ''' + Test adding a rule-based segment to an environment + ''' + environment_id = 'env1' + response_data = { + 'name': 'rule_segment1', + 'environment': { + 'id': environment_id, + 'name': 'Production' + }, + 'trafficType': { + 'id': '1', + 'name': 'traffic1' + } + } + + http_client_mock = mocker.Mock(spec=BaseHttpClient) + http_client_mock.make_request.return_value = response_data + seg = RuleBasedSegment(sample_data, http_client_mock) + + result = seg.add_to_environment(environment_id) + + http_client_mock.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['add_to_environment'], + body="", + segmentName='rule_segment1', + environmentId=environment_id + ) + + assert result.name == 'rule_segment1' + assert result.environment['id'] == environment_id + assert result.traffic_type.name == 'traffic1' + + def test_remove_from_environment(self, mocker, sample_data): + ''' + Test removing a rule-based segment from an environment + ''' + environment_id = 'env1' + http_client_mock = mocker.Mock(spec=BaseHttpClient) + http_client_mock.make_request.return_value = True + seg = RuleBasedSegment(sample_data, http_client_mock) + + result = seg.remove_from_environment(environment_id) + + http_client_mock.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['remove_from_environment'], + body="", + segmentName='rule_segment1', + environmentId=environment_id + ) + + assert result is True diff --git a/splitapiclient/tests/resources/test_rule_based_segment_definition.py b/splitapiclient/tests/resources/test_rule_based_segment_definition.py new file mode 100644 index 0000000..2f36a12 --- /dev/null +++ b/splitapiclient/tests/resources/test_rule_based_segment_definition.py @@ -0,0 +1,185 @@ +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import pytest +from splitapiclient.resources import RuleBasedSegmentDefinition +from splitapiclient.http_clients.sync_client import SyncHttpClient +from splitapiclient.http_clients.base_client import BaseHttpClient +from splitapiclient.main import get_client +from splitapiclient.microclients import RuleBasedSegmentDefinitionMicroClient +from splitapiclient.microclients import ChangeRequestMicroClient + +class TestRuleBasedSegmentDefinition: + ''' + Tests for the RuleBasedSegmentDefinition class' methods + ''' + @pytest.fixture + def sample_data(self): + '''Fixture providing sample segment definition data''' + return { + 'name': 'rule_segment1', + 'environment': { + 'id': 'env1', + 'name': 'Production' + }, + 'trafficType': { + 'id': '1', + 'name': 'traffic1' + }, + 'creationTime': 1234567890, + 'excludedKeys': ['key1', 'key2'], + 'excludedSegments': [ + {'name': 'segment1', 'type': 'whitelist'}, + {'name': 'segment2', 'type': 'whitelist'} + ], + 'rules': [ + { + 'condition': { + 'combiner': 'AND', + 'matchers': [ + { + 'type': 'EQUAL_TO', + 'attribute': 'age', + 'number': 30 + } + ] + } + } + ] + } + + def test_constructor(self, mocker, sample_data): + ''' + Test the constructor of RuleBasedSegmentDefinition + ''' + client = object() + + # We're not mocking BaseResource.__init__ anymore because it's called multiple times + # (once for RuleBasedSegmentDefinition and once for TrafficType) + seg = RuleBasedSegmentDefinition(sample_data, client) + + # Verify the properties are set correctly + assert seg.name == 'rule_segment1' + assert seg.environment['id'] == 'env1' + assert seg.environment['name'] == 'Production' + assert seg.traffic_type.name == 'traffic1' + assert seg.creation_time == 1234567890 + assert len(seg.excluded_keys) == 2 + assert seg.excluded_keys[0] == 'key1' + assert len(seg.excluded_segments) == 2 + assert seg.excluded_segments[0]['name'] == 'segment1' + + def test_getters(self, sample_data): + ''' + Test the getters of RuleBasedSegmentDefinition + ''' + seg = RuleBasedSegmentDefinition(sample_data) + assert seg.name == 'rule_segment1' + assert seg.environment['id'] == 'env1' + assert seg.traffic_type.name == 'traffic1' + assert seg.creation_time == 1234567890 + + def test_update(self, mocker, sample_data): + ''' + Test updating a rule-based segment definition + ''' + update_data = { + 'rules': [ + { + 'condition': { + 'combiner': 'AND', + 'matchers': [ + { + 'type': 'EQUAL_TO', + 'attribute': 'age', + 'number': 40 # Changed from 30 to 40 + } + ] + } + } + ] + } + + response_data = dict(sample_data) + response_data['rules'] = update_data['rules'] + + http_client_mock = mocker.Mock(spec=BaseHttpClient) + http_client_mock.make_request.return_value = response_data + http_client_mock._workspace_id = 'workspace1' + + seg = RuleBasedSegmentDefinition(sample_data, http_client_mock, workspace_id=http_client_mock._workspace_id) + result = seg.update(update_data) + + http_client_mock.make_request.assert_called_once_with( + RuleBasedSegmentDefinitionMicroClient._endpoint['update'], + body=update_data, + workspaceId='workspace1', + environmentId='env1', + segmentName='rule_segment1' + ) + + assert result.name == 'rule_segment1' + assert result.rules[0]['condition']['matchers'][0]['number'] == 40 + + def test_submit_change_request(self, mocker, sample_data): + ''' + Test submitting a change request for a rule-based segment definition + ''' + rules = [ + { + 'condition': { + 'combiner': 'AND', + 'matchers': [ + { + 'type': 'EQUAL_TO', + 'attribute': 'age', + 'number': 25 + } + ] + } + } + ] + + operation_type = 'create' + title = 'New Rule-Based Segment' + comment = 'Adding a new rule' + approvers = ['user1'] + rollout_status_id = None + workspace_id = 'workspace1' + + expected_request = { + 'ruleBasedSegment': { + 'name': 'rule_segment1', + 'rules': rules, + 'excludedKeys': [], + 'excludedSegments': [] + }, + 'operationType': operation_type, + 'title': title, + 'comment': comment, + 'approvers': approvers, + } + + response_data = { + 'id': 'cr123', + 'status': 'PENDING', + 'title': title, + 'comment': comment, + 'approvers': approvers, + 'operationType': operation_type + } + + http_client_mock = mocker.Mock(spec=BaseHttpClient) + http_client_mock.make_request.return_value = response_data + + seg = RuleBasedSegmentDefinition(sample_data, http_client_mock) + result = seg.submit_change_request( + rules, [], [], operation_type, title, comment, approvers, workspace_id + ) + + http_client_mock.make_request.assert_called_once_with( + ChangeRequestMicroClient._endpoint['submit_change_request'], + workspaceId=workspace_id, + environmentId='env1', + body=expected_request + ) diff --git a/splitapiclient/tests/resources/test_segment_definition.py b/splitapiclient/tests/resources/test_segment_definition.py index 15dcb97..8fc00cf 100644 --- a/splitapiclient/tests/resources/test_segment_definition.py +++ b/splitapiclient/tests/resources/test_segment_definition.py @@ -108,6 +108,46 @@ def test_import_keys_from_json(self, mocker): ) assert attr == True + def test_import_keys_from_json_large_batch(self, mocker): + """Test importing more than 10,000 keys to verify batch processing""" + # Create a large list of keys (e.g., 25,000) + large_key_list = [f"id{i}" for i in range(25000)] + data = {"keys": large_key_list, "comment": "large batch test"} + + # Mock the microclient + mock_imc = mocker.Mock() + mock_imc.import_keys_from_json.return_value = True + + # Mock the require_client function to return our mock + mocker.patch('splitapiclient.resources.segment_definition.require_client', + return_value=mock_imc) + + seg = SegmentDefinition( + { + 'name': 'name', + 'environment': { + 'id': '1', + 'name': 'env' + }, + 'trafficType': {}, + } + ) + + # Call the method + result = seg.import_keys_from_json(False, data) + + # Verify the method returns True when all batches succeed + assert result is True + + # Verify the microclient was called 3 times (for 25,000 keys) + assert mock_imc.import_keys_from_json.call_count == 3 + + # Verify each batch had the correct number of keys + calls = mock_imc.import_keys_from_json.call_args_list + assert len(calls[0][0][3]['keys']) == 10000 # First batch + assert len(calls[1][0][3]['keys']) == 10000 # Second batch + assert len(calls[2][0][3]['keys']) == 5000 # Last batch + def test_remove_keys(self, mocker): ''' ''' @@ -182,7 +222,8 @@ def test_submit_change_request(self, mocker): 'approvers': None, 'operationType': None, 'comments': None, - 'rolloutStatus': None + 'rolloutStatus': None, + 'ruleBasedSegment': None } assert attr.to_dict() == data1 diff --git a/splitapiclient/tests/resources/test_split_definition.py b/splitapiclient/tests/resources/test_split_definition.py index 7485d59..e578a31 100644 --- a/splitapiclient/tests/resources/test_split_definition.py +++ b/splitapiclient/tests/resources/test_split_definition.py @@ -254,7 +254,8 @@ def test_submit_change_request(self, mocker): 'approvers': None, 'operationType': None, 'comments': None, - 'rolloutStatus': None + 'rolloutStatus': None, + 'ruleBasedSegment': None } assert attr.to_dict() == data diff --git a/splitapiclient/tests/resources/test_workspace.py b/splitapiclient/tests/resources/test_workspace.py index 41281fe..b6f40b5 100644 --- a/splitapiclient/tests/resources/test_workspace.py +++ b/splitapiclient/tests/resources/test_workspace.py @@ -10,6 +10,7 @@ from splitapiclient.microclients import SplitMicroClient from splitapiclient.microclients import WorkspaceMicroClient from splitapiclient.microclients import LargeSegmentMicroClient +from splitapiclient.microclients import RuleBasedSegmentMicroClient class TestWorkspace: ''' Tests for the Workspace class' methods @@ -308,3 +309,66 @@ def test_delete_large_segment(self, mocker): segmentName=large_segment_name, ) assert attr == True + + def test_add_rule_based_segment(self, mocker): + ''' + Test adding a rule-based segment via the workspace + ''' + data = { + 'name': 'rule_seg1', + 'description': 'rule based segment description', + 'creationTime': None, + 'tags': [{'name': 'tag1'}] + } + http_client_mock = mocker.Mock(spec=BaseHttpClient) + http_client_mock.make_request.return_value = data + ws1 = Workspace( + { + 'id': '1', + 'name': 'workspace1', + 'requiresTitleAndComments': None + }, + http_client_mock + ) + attr = ws1.add_rule_based_segment(data, 'user') + + http_client_mock.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['create'], + body=data, + workspaceId='1', + trafficTypeName='user' + ) + expected_data = { + 'name': 'rule_seg1', + 'description': 'rule based segment description', + 'trafficType': None, + 'workspaceId': '1', + 'creationTime': None, + 'tags': [{'name': 'tag1'}] + } + assert attr.to_dict() == expected_data + + def test_delete_rule_based_segment(self, mocker): + ''' + Test deleting a rule-based segment via the workspace + ''' + segment_name = 'rule_seg1' + http_client_mock = mocker.Mock(spec=BaseHttpClient) + http_client_mock.make_request.return_value = True + ws1 = Workspace( + { + 'id': '1', + 'name': 'workspace1', + 'requiresTitleAndComments': None + }, + http_client_mock + ) + + attr = ws1.delete_rule_based_segment(segment_name) + + http_client_mock.make_request.assert_called_once_with( + RuleBasedSegmentMicroClient._endpoint['delete'], + workspaceId='1', + segmentName=segment_name, + ) + assert attr == True diff --git a/splitapiclient/util/helpers.py b/splitapiclient/util/helpers.py index e7985f9..bc4522f 100644 --- a/splitapiclient/util/helpers.py +++ b/splitapiclient/util/helpers.py @@ -41,8 +41,10 @@ def require_client(model, http_client, apiclient): from splitapiclient.microclients.flag_set_microclient import FlagSetMicroClient from splitapiclient.microclients.large_segment_microclient import LargeSegmentMicroClient from splitapiclient.microclients.large_segment_definition_microclient import LargeSegmentDefinitionMicroClient + from splitapiclient.microclients.rule_based_segment_microclient import RuleBasedSegmentMicroClient + from splitapiclient.microclients.rule_based_segment_definition_microclient import RuleBasedSegmentDefinitionMicroClient - if model not in ['LargeSegment','LargeSegmentDefinition', 'FlagSet','Attribute', 'Workspace', 'Environment', 'Split', 'SplitDefinition', 'Segment', 'SegmentDefinition', 'Identity', 'TrafficType', 'ChangeRequest', 'User', 'Group', 'APIKey', 'Restriction']: + if model not in ['RuleBasedSegment', 'RuleBasedSegmentDefinition', 'LargeSegment','LargeSegmentDefinition', 'FlagSet','Attribute', 'Workspace', 'Environment', 'Split', 'SplitDefinition', 'Segment', 'SegmentDefinition', 'Identity', 'TrafficType', 'ChangeRequest', 'User', 'Group', 'APIKey', 'Restriction']: raise InvalidModelException('Unknown model %s' % model) if apiclient and isinstance(apiclient, BaseApiClient): @@ -63,6 +65,8 @@ def require_client(model, http_client, apiclient): if model == 'FlagSet': return apiclient.flag_sets if model == 'LargeSegment': return apiclient.large_segments if model == 'LargeSegmentDefinition': return apiclient.large_segment_definitions + if model == 'RuleBasedSegment': return apiclient.rule_based_segments + if model == 'RuleBasedSegmentDefinition': return apiclient.rule_based_segment_definitions elif http_client and isinstance(http_client, BaseHttpClient): if model == 'Attribute': return AttributeMicroClient(http_client) if model == 'Environment': return EnvironmentMicroClient(http_client) @@ -81,6 +85,8 @@ def require_client(model, http_client, apiclient): if model == 'FlagSet': return FlagSetMicroClient(http_client) if model == 'LargeSegment': return LargeSegmentMicroClient(http_client) if model == 'LargeSegmentDefinition': return LargeSegmentDefinitionMicroClient(http_client) + if model == 'RuleBasedSegment': return RuleBasedSegmentMicroClient(http_client) + if model == 'RuleBasedSegmentDefinition': return RuleBasedSegmentDefinitionMicroClient(http_client) else: raise ClientRequiredError( 'Object created ad-hoc, you need to pass a SplitApiClient instance ' diff --git a/splitapiclient/version.py b/splitapiclient/version.py index 01bd03c..bf5afe7 100644 --- a/splitapiclient/version.py +++ b/splitapiclient/version.py @@ -1 +1 @@ -__version__ = '3.5.0' +__version__ = '3.5.1'