Skip to content

Commit 538272a

Browse files
Abhi591rohitesh-wingify
authored andcommitted
feat: support for multiple attributes in setAttribute call
1 parent d9e4dd2 commit 538272a

File tree

6 files changed

+130
-69
lines changed

6 files changed

+130
-69
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [1.6.0] - 2025-04-04
8+
9+
### Added
10+
11+
- Added support for sending multiple attributes at once using `set_attribute` method by passing a dictionary of key-value pairs to update user attributes in a single API call.
12+
713
## [1.5.0] - 2025-03-12
814

915
### Added
@@ -120,4 +126,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
120126
}
121127

122128
vwo_client = init(options)
123-
```
129+
```

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def run(self):
121121

122122
setup(
123123
name="vwo-fme-python-sdk",
124-
version="1.5.0",
124+
version="1.6.0",
125125
description="VWO Feature Management and Experimentation SDK for Python",
126126
long_description=long_description,
127127
long_description_content_type="text/markdown",

vwo/api/set_attribute_api.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,37 @@
1919
from typing import Dict, Any
2020
from ..models.settings.settings_model import SettingsModel
2121
from ..models.user.context_model import ContextModel
22-
from ..utils.network_util import get_events_base_properties, get_attribute_payload_data, send_post_api_request
22+
from ..utils.network_util import (
23+
get_events_base_properties,
24+
get_attribute_payload_data,
25+
send_post_api_request,
26+
)
2327
from ..enums.event_enum import EventEnum
2428

29+
2530
class SetAttributeApi:
26-
def set_attribute(self, settings: SettingsModel, attribute_key: str, attribute_value: Any, context: ContextModel):
27-
self.create_and_send_impression_for_attribute(settings, attribute_key, attribute_value, context)
28-
31+
def set_attribute(
32+
self, settings: SettingsModel, attribute_map: Dict, context: ContextModel
33+
):
34+
self.create_and_send_impression_for_attribute(settings, attribute_map, context)
35+
2936
def create_and_send_impression_for_attribute(
30-
self,
31-
settings: SettingsModel,
32-
attribute_key: str,
33-
attribute_value: Any,
34-
context: ContextModel
37+
self, settings: SettingsModel, attribute_map: Dict, context: ContextModel
3538
):
3639
properties = get_events_base_properties(
3740
EventEnum.VWO_SYNC_VISITOR_PROP.value,
3841
visitor_user_agent=context.get_user_agent(),
39-
ip_address=context.get_ip_address()
42+
ip_address=context.get_ip_address(),
4043
)
4144
# Construct payload data for tracking the goal
4245
payload = get_attribute_payload_data(
4346
settings,
4447
context.get_id(),
4548
EventEnum.VWO_SYNC_VISITOR_PROP.value,
46-
attribute_key,
47-
attribute_value,
49+
attribute_map,
4850
visitor_user_agent=context.get_user_agent(),
49-
ip_address=context.get_ip_address()
51+
ip_address=context.get_ip_address(),
5052
)
5153

5254
# Send the constructed properties and payload as a POST request
5355
send_post_api_request(properties, payload)
54-

vwo/constants/Constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ class Constants:
1717
# Mock package_file equivalent
1818
package_file = {
1919
"name": "vwo-fme-python-sdk", # Replace with actual package name
20-
"version": "1.5.0", # Replace with actual package version
20+
"version": "1.6.0", # Replace with actual package version
2121
}
2222

2323
# Constants

vwo/utils/network_util.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
from ..enums.headers_enum import HeadersEnum
3131
import asyncio
3232
import threading
33+
from ..utils.function_util import (
34+
get_current_unix_timestamp,
35+
get_current_unix_timestamp_in_millis,
36+
get_random_number,
37+
)
3338

3439
def get_settings_path(sdk_key: str, account_id: str) -> Dict[str, Any]:
3540
path = {
@@ -181,23 +186,26 @@ def get_attribute_payload_data(
181186
settings: SettingsModel,
182187
user_id: str,
183188
event_name: str,
184-
attribute_key: str,
185-
attribute_value: Any,
186-
visitor_user_agent: str = '',
187-
ip_address: str = ''
189+
attribute_map: Dict,
190+
visitor_user_agent: str = "",
191+
ip_address: str = "",
188192
) -> Dict[str, Any]:
189-
properties = _get_event_base_payload(settings, user_id, event_name, visitor_user_agent, ip_address)
190-
191-
properties['d']['event']['props']['isCustomEvent'] = True
192-
properties['d']['event']['props'][Constants.VWO_FS_ENVIRONMENT] = settings.get_sdk_key()
193-
properties['d']['visitor']['props'][attribute_key] = attribute_value
194-
195-
LogManager.get_instance().debug(debug_messages.get('IMPRESSION_FOR_SYNC_VISITOR_PROP').format(
196-
eventName = event_name,
197-
accountId = settings.get_account_id(),
198-
userId = user_id
199-
))
200-
193+
properties = _get_event_base_payload(
194+
settings, user_id, event_name, visitor_user_agent, ip_address
195+
)
196+
197+
properties["d"]["event"]["props"]["isCustomEvent"] = True
198+
properties["d"]["event"]["props"][
199+
Constants.VWO_FS_ENVIRONMENT
200+
] = settings.get_sdk_key()
201+
properties["d"]["visitor"]["props"].update(attribute_map)
202+
203+
LogManager.get_instance().debug(
204+
debug_messages.get("IMPRESSION_FOR_SYNC_VISITOR_PROP").format(
205+
eventName=event_name, accountId=settings.get_account_id(), userId=user_id
206+
)
207+
)
208+
201209
return properties
202210

203211

vwo/vwo_client.py

Lines changed: 82 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -200,51 +200,99 @@ def track_event(
200200
)
201201
return {event_name: False}
202202

203-
def set_attribute(self, attribute_key: str, attribute_value: Any, context: Dict):
203+
def set_attribute(
204+
self, key_or_map: Any, value_or_context: Any, context: Dict = None
205+
):
204206
"""
205207
Sets an attribute for a user in the context provided.
206-
This method validates the types of the inputs before proceeding with the API call.
208+
This method can be called in two ways:
209+
1. set_attribute(key, value, context) - Sets a single attribute
210+
2. set_attribute(attribute_map, context) - Sets multiple attributes
207211
208-
:param attribute_key: The key of the attribute to set.
209-
:param attribute_value: The value of the attribute to set.
210-
:param context: The context in which the attribute is being set.
212+
The attribute values must be either strings, integers, or booleans.
213+
214+
:param key_or_map: Either the attribute key (str) or a map of attributes (Dict)
215+
:param value_or_context: Either the attribute value or the context (if key_or_map is a Dict)
216+
:param context: The context in which the attribute is being set (only used when setting single attribute)
211217
"""
212218
api_name = "set_attribute"
213219

214220
try:
215-
216221
LogManager.get_instance().debug(
217222
debug_messages.get("API_CALLED").format(apiName=api_name)
218223
)
219224

220-
# Validate featureKey is a string
221-
if not is_string(attribute_key):
222-
LogManager.get_instance().error(
223-
error_messages.get("API_INVALID_PARAM").format(
224-
apiName=api_name,
225-
key="attribute_key",
226-
type=type(attribute_key).__name__,
227-
correctType="string",
225+
# Determine which calling pattern is being used
226+
if context is not None:
227+
# Single attribute pattern: (key, value, context)
228+
if not is_string(key_or_map):
229+
LogManager.get_instance().error(
230+
error_messages.get("API_INVALID_PARAM").format(
231+
apiName=api_name,
232+
key="key",
233+
type=type(key_or_map).__name__,
234+
correctType="string",
235+
)
236+
)
237+
raise TypeError("TypeError: key should be a string")
238+
239+
if not isinstance(value_or_context, (str, int, bool, float)):
240+
LogManager.get_instance().error(
241+
error_messages.get("API_INVALID_PARAM").format(
242+
apiName=api_name,
243+
key="value",
244+
type=type(value_or_context).__name__,
245+
correctType="string, integer, float, or boolean",
246+
)
247+
)
248+
raise TypeError(
249+
"TypeError: value should be a string, integer, float, or boolean"
228250
)
229-
)
230-
raise TypeError("TypeError: attribute_key should be a string")
231251

232-
if (
233-
not is_string(attribute_value)
234-
and not isinstance(attribute_value, int)
235-
and not isinstance(attribute_value, bool)
236-
):
237-
LogManager.get_instance().error(
238-
error_messages.get("API_INVALID_PARAM").format(
239-
apiName=api_name,
240-
key="attribute_value",
241-
type=type(attribute_value).__name__,
242-
correctType="string or int or bool",
252+
attribute_map = {key_or_map: value_or_context}
253+
user_context = context
254+
else:
255+
# Multiple attributes pattern: (attribute_map, context)
256+
if not is_object(key_or_map) or not key_or_map:
257+
LogManager.get_instance().error(
258+
error_messages.get("API_INVALID_PARAM").format(
259+
apiName=api_name,
260+
key="attribute_map",
261+
type=type(key_or_map).__name__,
262+
correctType="object",
263+
)
264+
)
265+
raise TypeError(
266+
"TypeError: attribute_map should be a non-empty object"
243267
)
244-
)
245-
raise TypeError(
246-
"TypeError: attribute_value should be an string or int or bool"
247-
)
268+
269+
# Validate all keys and values in the attribute map
270+
for key, value in key_or_map.items():
271+
if not is_string(key):
272+
LogManager.get_instance().error(
273+
error_messages.get("API_INVALID_PARAM").format(
274+
apiName=api_name,
275+
key="key",
276+
type=type(key).__name__,
277+
correctType="string",
278+
)
279+
)
280+
raise TypeError("TypeError: key should be a string")
281+
if not isinstance(value, (str, int, bool, float)):
282+
LogManager.get_instance().error(
283+
error_messages.get("API_INVALID_PARAM").format(
284+
apiName=api_name,
285+
key=f"value for key '{key}'",
286+
type=type(value).__name__,
287+
correctType="string, integer, float, or boolean",
288+
)
289+
)
290+
raise TypeError(
291+
f"TypeError: value for key '{key}' should be a string, integer, float or boolean"
292+
)
293+
294+
attribute_map = key_or_map
295+
user_context = value_or_context
248296

249297
# Validate settings are loaded and valid
250298
if not SettingsManager.is_settings_valid(self.original_settings):
@@ -253,19 +301,17 @@ def set_attribute(self, attribute_key: str, attribute_value: Any, context: Dict)
253301
)
254302
raise ValueError("Invalid Settings")
255303

256-
# Validate user ID is present in context
257-
if not context or "id" not in context:
304+
if not user_context or "id" not in user_context:
258305
LogManager.get_instance().error(
259306
error_messages.get("API_CONTEXT_INVALID")
260307
)
261308
raise ValueError("Invalid context")
262309

263-
context_model = ContextModel(context)
310+
context_model = ContextModel(user_context)
264311

265-
# Fetch the feature flag value using FlagApi
266312
set_attribute_api = SetAttributeApi()
267313
set_attribute_api.set_attribute(
268-
self._settings, attribute_key, attribute_value, context_model
314+
self._settings, attribute_map, context_model
269315
)
270316
return
271317

0 commit comments

Comments
 (0)