wingify-fme-python-sdk 1.55.0__tar.gz → 1.60.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. {wingify_fme_python_sdk-1.55.0/wingify_fme_python_sdk.egg-info → wingify_fme_python_sdk-1.60.0}/PKG-INFO +22 -1
  2. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/README.md +21 -0
  3. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/setup.py +1 -1
  4. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/constants/Constants.py +1 -1
  5. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/user/context_model.py +16 -2
  6. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/core/segmentation_manager.py +11 -0
  7. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/enums/segment_operator_value_enum.py +1 -0
  8. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/evaluators/segment_evaluator.py +4 -0
  9. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.py +24 -0
  10. wingify_fme_python_sdk-1.60.0/wingify/packages/segmentation_evaluator/utils/web_testing_segment_util.py +295 -0
  11. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/resources/error-messages.json +8 -1
  12. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0/wingify_fme_python_sdk.egg-info}/PKG-INFO +22 -1
  13. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify_fme_python_sdk.egg-info/SOURCES.txt +1 -0
  14. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/LICENSE +0 -0
  15. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/MANIFEST.in +0 -0
  16. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/NOTICE +0 -0
  17. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/requirements-dev.txt +0 -0
  18. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/requirements.txt +0 -0
  19. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/setup.cfg +0 -0
  20. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/vwo/__init__.py +0 -0
  21. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/vwo/vwo.py +0 -0
  22. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/vwo/vwo_builder.py +0 -0
  23. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/vwo/vwo_client.py +0 -0
  24. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/__init__.py +0 -0
  25. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/api/__init__.py +0 -0
  26. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/api/get_flag_api.py +0 -0
  27. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/api/set_attribute_api.py +0 -0
  28. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/api/track_api.py +0 -0
  29. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/constants/__init__.py +0 -0
  30. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/decorators/__init__.py +0 -0
  31. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/decorators/storage_decorator.py +0 -0
  32. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/__init__.py +0 -0
  33. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/api_enum.py +0 -0
  34. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/campaign_type_enum.py +0 -0
  35. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/debug_category_enum.py +0 -0
  36. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/event_enum.py +0 -0
  37. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/headers_enum.py +0 -0
  38. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/hooks_enum.py +0 -0
  39. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/status_enum.py +0 -0
  40. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/storage_enum.py +0 -0
  41. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/enums/url_enum.py +0 -0
  42. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/Alias.py +0 -0
  43. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/AliasSetResponse.py +0 -0
  44. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/__init__.py +0 -0
  45. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/batch_event_data.py +0 -0
  46. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/__init__.py +0 -0
  47. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/campaign_model.py +0 -0
  48. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/feature_model.py +0 -0
  49. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/holdout_model.py +0 -0
  50. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/impact_campaign_model.py +0 -0
  51. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/metric_model.py +0 -0
  52. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/rule_model.py +0 -0
  53. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/variable_model.py +0 -0
  54. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/campaign/variation_model.py +0 -0
  55. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/gateway_service_model.py +0 -0
  56. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/__init__.py +0 -0
  57. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/campaign_group_schema.py +0 -0
  58. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/campaign_metric_schema.py +0 -0
  59. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/campaign_schema.py +0 -0
  60. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/campaign_variation_schema.py +0 -0
  61. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/empty_object_schema.py +0 -0
  62. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/feature_schema.py +0 -0
  63. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/group_schema.py +0 -0
  64. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/holdout_schema.py +0 -0
  65. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/rule_schema.py +0 -0
  66. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/settings_schema.py +0 -0
  67. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/schemas/variable_schema.py +0 -0
  68. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/settings/__init__.py +0 -0
  69. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/settings/settings_model.py +0 -0
  70. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/storage/__init__.py +0 -0
  71. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/storage/storage_model.py +0 -0
  72. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/user/__init__.py +0 -0
  73. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/user/context_vwo_model.py +0 -0
  74. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/user/get_flag.py +0 -0
  75. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/vwo_options_model.py +0 -0
  76. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/models/wingify_options_model.py +0 -0
  77. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/__init__.py +0 -0
  78. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/decision_maker/__init__.py +0 -0
  79. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/decision_maker/decision_maker.py +0 -0
  80. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/__init__.py +0 -0
  81. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/core/__init__.py +0 -0
  82. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/core/log_manager.py +0 -0
  83. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/core/log_transport_manager.py +0 -0
  84. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/enums/__init__.py +0 -0
  85. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/enums/log_level_enum.py +0 -0
  86. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/enums/log_level_number_enum.py +0 -0
  87. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/log_message_builder.py +0 -0
  88. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/logger.py +0 -0
  89. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/transports/__init__.py +0 -0
  90. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/logger/transports/console_transport.py +0 -0
  91. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/__init__.py +0 -0
  92. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/client/__init__.py +0 -0
  93. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/client/network_client.py +0 -0
  94. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/handlers/__init__.py +0 -0
  95. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/handlers/request_handler.py +0 -0
  96. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/manager/__init__.py +0 -0
  97. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/manager/network_manager.py +0 -0
  98. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/models/__init__.py +0 -0
  99. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/models/global_request_model.py +0 -0
  100. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/models/request_model.py +0 -0
  101. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/network_layer/models/response_model.py +0 -0
  102. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/__init__.py +0 -0
  103. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/core/__init__.py +0 -0
  104. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/enums/__init__.py +0 -0
  105. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/enums/segment_operand_regex_enum.py +0 -0
  106. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/enums/segment_operand_value_enum.py +0 -0
  107. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/evaluators/__init__.py +0 -0
  108. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/utils/__init__.py +0 -0
  109. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/segmentation_evaluator/utils/segment_util.py +0 -0
  110. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/storage/__init__.py +0 -0
  111. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/storage/connector.py +0 -0
  112. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/packages/storage/storage.py +0 -0
  113. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/resources/__init__.py +0 -0
  114. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/resources/debug-messages.json +0 -0
  115. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/resources/info-message.json +0 -0
  116. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/resources/trace-messages.json +0 -0
  117. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/resources/warn-messages.json +0 -0
  118. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/services/__init__.py +0 -0
  119. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/services/batch_event_queue.py +0 -0
  120. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/services/campaign_decision_service.py +0 -0
  121. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/services/hooks_manager.py +0 -0
  122. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/services/settings_manager.py +0 -0
  123. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/services/storage_service.py +0 -0
  124. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/services/url_service.py +0 -0
  125. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/__init__.py +0 -0
  126. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/aliasing_util.py +0 -0
  127. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/brand_context.py +0 -0
  128. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/brand_util.py +0 -0
  129. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/campaign_util.py +0 -0
  130. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/data_type_util.py +0 -0
  131. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/debugger_service_util.py +0 -0
  132. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/decision_util.py +0 -0
  133. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/event_util.py +0 -0
  134. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/function_util.py +0 -0
  135. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/gateway_service_util.py +0 -0
  136. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/holdout_util.py +0 -0
  137. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/impression_util.py +0 -0
  138. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/log_message_util.py +0 -0
  139. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/meg_util.py +0 -0
  140. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/model_utils.py +0 -0
  141. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/network_util.py +0 -0
  142. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/rule_evaluation_util.py +0 -0
  143. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/settings_util.py +0 -0
  144. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/usage_stats_util.py +0 -0
  145. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/utils/uuid_util.py +0 -0
  146. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/vwo_client.py +0 -0
  147. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/wingify.py +0 -0
  148. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/wingify_builder.py +0 -0
  149. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify/wingify_client.py +0 -0
  150. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify_fme_python_sdk.egg-info/dependency_links.txt +0 -0
  151. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify_fme_python_sdk.egg-info/requires.txt +0 -0
  152. {wingify_fme_python_sdk-1.55.0 → wingify_fme_python_sdk-1.60.0}/wingify_fme_python_sdk.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wingify-fme-python-sdk
3
- Version: 1.55.0
3
+ Version: 1.60.0
4
4
  Summary: Wingify Feature Management and Experimentation SDK for Python
5
5
  Home-page: https://github.com/wingify/vwo-fme-python-sdk
6
6
  Author: Wingify
@@ -168,9 +168,30 @@ context = {
168
168
  'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
169
169
  'ip_address': '1.1.1.1',
170
170
  'session_id': 1697123456 # Optional: Custom session ID for web client integration
171
+ 'platformVariables': {
172
+ # Reference example only:.
173
+ # In production, fetch campaign assignments using script run in frontend and pass that object to backend as webTestingCampaigns.
174
+ 'webTestingCampaigns': '{"122":"1","130":"2"}'
175
+ },
176
+ 'session_id': 1697123456 # Optional: Custom session ID for web client integration
171
177
  }
172
178
  ```
173
179
 
180
+ ## Web testing pre-segmentation
181
+
182
+ Server-side flag decisions can align with **Web Testing** (browser) experiments. **Campaign assignments are typically read on the frontend** (for example, VWO cookies) and sent to your server; pass them in `platformVariables.webTestingCampaigns` as a map of **campaign ID -> variation ID** (strings), or as a **JSON string** of that object.
183
+
184
+ Pre-segment rules in the VWO dashboard can use the **`campaignVariation`** operator:
185
+
186
+ | Operand pattern | Meaning |
187
+ | --------------- | ------- |
188
+ | `C` | User is in campaign `C` (any variation). |
189
+ | `!C` | User is **not** in campaign `C`. |
190
+ | `C_V` | User is in campaign `C` with variation `V`. |
191
+ | `C_!V` | User is in campaign `C` and assigned variation is **not** `V`. |
192
+
193
+ Rollout rules evaluate pre-segments from the **first variation** only; AB/testing rules use **campaign-level** segments.
194
+
174
195
  ## Session Management
175
196
 
176
197
  The SDK provides automatic session management capabilities to enable seamless integration with VWO's web client testing campaigns. Session IDs are automatically generated and managed to connect server-side feature flag decisions with client-side user sessions.
@@ -141,9 +141,30 @@ context = {
141
141
  'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
142
142
  'ip_address': '1.1.1.1',
143
143
  'session_id': 1697123456 # Optional: Custom session ID for web client integration
144
+ 'platformVariables': {
145
+ # Reference example only:.
146
+ # In production, fetch campaign assignments using script run in frontend and pass that object to backend as webTestingCampaigns.
147
+ 'webTestingCampaigns': '{"122":"1","130":"2"}'
148
+ },
149
+ 'session_id': 1697123456 # Optional: Custom session ID for web client integration
144
150
  }
145
151
  ```
146
152
 
153
+ ## Web testing pre-segmentation
154
+
155
+ Server-side flag decisions can align with **Web Testing** (browser) experiments. **Campaign assignments are typically read on the frontend** (for example, VWO cookies) and sent to your server; pass them in `platformVariables.webTestingCampaigns` as a map of **campaign ID -> variation ID** (strings), or as a **JSON string** of that object.
156
+
157
+ Pre-segment rules in the VWO dashboard can use the **`campaignVariation`** operator:
158
+
159
+ | Operand pattern | Meaning |
160
+ | --------------- | ------- |
161
+ | `C` | User is in campaign `C` (any variation). |
162
+ | `!C` | User is **not** in campaign `C`. |
163
+ | `C_V` | User is in campaign `C` with variation `V`. |
164
+ | `C_!V` | User is in campaign `C` and assigned variation is **not** `V`. |
165
+
166
+ Rollout rules evaluate pre-segments from the **first variation** only; AB/testing rules use **campaign-level** segments.
167
+
147
168
  ## Session Management
148
169
 
149
170
  The SDK provides automatic session management capabilities to enable seamless integration with VWO's web client testing campaigns. Session IDs are automatically generated and managed to connect server-side feature flag decisions with client-side user sessions.
@@ -143,7 +143,7 @@ class PostDevelopCommand(develop):
143
143
 
144
144
  setup(
145
145
  name=_brand["name"],
146
- version="1.55.0",
146
+ version="1.60.0",
147
147
  description=_brand["description"],
148
148
  long_description=long_description,
149
149
  long_description_content_type="text/markdown",
@@ -17,7 +17,7 @@ class Constants:
17
17
  # TODO: read from setup.py
18
18
  sdk_meta = {
19
19
  "name": "vwo-fme-python-sdk",
20
- "version": "1.55.0",
20
+ "version": "1.60.0",
21
21
  }
22
22
 
23
23
  SDK_VERSION = sdk_meta["version"]
@@ -14,7 +14,8 @@
14
14
 
15
15
 
16
16
  from .context_vwo_model import ContextVWOModel
17
- from typing import Dict, List
17
+ from typing import Any, Dict, List, Optional
18
+ from ...utils.data_type_util import is_object
18
19
  from ...utils.uuid_util import get_uuid
19
20
  from ...services.settings_manager import SettingsManager
20
21
  from ...utils.function_util import get_current_unix_timestamp
@@ -33,6 +34,11 @@ class ContextModel:
33
34
  self._vwo = ContextVWOModel(vwo_context_data) if vwo_context_data else None
34
35
  self.post_segmentation_variables = context.get("post_segmentation_variables", [])
35
36
  self.bucketingSeed = context.get("bucketingSeed")
37
+ self.platform_variables: Optional[Dict[str, Any]] = (
38
+ dict(context.get("platform_variables"))
39
+ if is_object(context.get("platform_variables"))
40
+ else None
41
+ )
36
42
  self.session_id = context.get("session_id", None)
37
43
  if self.session_id is None:
38
44
  self.session_id = get_current_unix_timestamp()
@@ -96,4 +102,12 @@ class ContextModel:
96
102
  self.session_id = session_id
97
103
 
98
104
  def get_bucketing_seed(self) -> str:
99
- return self.bucketingSeed
105
+ return self.bucketingSeed
106
+
107
+ def get_platform_variables(self) -> Optional[Dict[str, Any]]:
108
+ return self.platform_variables
109
+
110
+ def set_platform_variables(self, platform_variables: Optional[Dict[str, Any]]) -> None:
111
+ self.platform_variables = (
112
+ dict(platform_variables) if is_object(platform_variables) else None
113
+ )
@@ -24,6 +24,10 @@ from ....models.user.context_model import ContextModel
24
24
  from ....utils.gateway_service_util import get_query_params, get_from_gateway_service
25
25
  from ....constants.Constants import Constants
26
26
  from ....enums.api_enum import ApiEnum
27
+ from ..utils.web_testing_segment_util import (
28
+ dsl_contains_campaign_variation_node,
29
+ is_web_testing_campaigns_absent_or_null,
30
+ )
27
31
 
28
32
 
29
33
  class SegmentationManager:
@@ -115,4 +119,11 @@ class SegmentationManager:
115
119
  :param properties: The properties to validate against.
116
120
  :return: True if segmentation is valid, otherwise False.
117
121
  """
122
+ # if the dsl contains a campaign variation node, then we need to check if the campaign variation is valid
123
+ if dsl_contains_campaign_variation_node(dsl):
124
+ evaluation_context = getattr(self.evaluator, "context", None)
125
+ if evaluation_context is None or is_web_testing_campaigns_absent_or_null(
126
+ evaluation_context
127
+ ):
128
+ return False
118
129
  return self.evaluator.is_segmentation_valid(dsl, properties)
@@ -34,3 +34,4 @@ class SegmentOperatorValueEnum(Enum):
34
34
  IP = "ip_address"
35
35
  BROWSER_VERSION = "browser_version"
36
36
  OS_VERSION = "os_version"
37
+ WEB_CAMPAIGN_VARIATION = "campaignVariation"
@@ -68,6 +68,10 @@ class SegmentEvaluator:
68
68
  return SegmentOperandEvaluator().evaluate_string_operand_dsl(
69
69
  sub_dsl, self.context, SegmentOperatorValueEnum.OS_VERSION.value
70
70
  )
71
+ elif operator == SegmentOperatorValueEnum.WEB_CAMPAIGN_VARIATION.value:
72
+ return SegmentOperandEvaluator().evaluate_campaign_variation_dsl(
73
+ sub_dsl, self.context
74
+ )
71
75
  else:
72
76
  return False
73
77
 
@@ -16,6 +16,11 @@
16
16
  import re
17
17
  from typing import Dict, Any
18
18
  from ..utils.segment_util import get_key_value, match_with_regex
19
+ from ..utils.web_testing_segment_util import (
20
+ coerce_campaign_variation_operand_to_str,
21
+ parse_web_testing_campaigns_from_context,
22
+ evaluate_web_testing_campaign_variation,
23
+ )
19
24
  from ..enums.segment_operand_regex_enum import SegmentOperandRegexEnum
20
25
  from ..enums.segment_operand_value_enum import SegmentOperandValueEnum
21
26
  from ..enums.segment_operator_value_enum import SegmentOperatorValueEnum
@@ -84,6 +89,25 @@ class SegmentOperandEvaluator:
84
89
  operand_type, processed_values["operand_value"], tag_value
85
90
  )
86
91
 
92
+ def evaluate_campaign_variation_dsl(
93
+ self, dsl_operand_value: Any, context: ContextModel
94
+ ) -> bool:
95
+ """
96
+ Evaluates a campaignVariation DSL operand against the user's web testing campaign assignments.
97
+
98
+ :param dsl_operand_value: The raw operand value from the DSL (e.g. "123_1", "!123", 100).
99
+ :param context: The context object containing platform_variables.web_testing_campaigns.
100
+ :return: True if the segment condition is satisfied, otherwise False.
101
+ """
102
+ operand_str = coerce_campaign_variation_operand_to_str(dsl_operand_value)
103
+ if operand_str is None:
104
+ return False
105
+
106
+ operand_str = operand_str.strip()
107
+ campaigns_map = parse_web_testing_campaigns_from_context(context)
108
+ result, _invalid = evaluate_web_testing_campaign_variation(operand_str, campaigns_map)
109
+ return result
110
+
87
111
  def evaluate_user_dsl(self, dsl_operand_value, properties):
88
112
  """
89
113
  Evaluates a user DSL expression to check if a user ID is in a specified List.
@@ -0,0 +1,295 @@
1
+ # Copyright 2024-2026 Wingify Software Pvt. Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import json
16
+ import math
17
+ import re
18
+ from typing import Any, Dict, Optional, Tuple
19
+
20
+ from ....models.user.context_model import ContextModel
21
+ from ....utils.data_type_util import (
22
+ get_type,
23
+ is_array,
24
+ is_boolean,
25
+ is_defined,
26
+ is_null,
27
+ is_number,
28
+ is_object,
29
+ is_string,
30
+ )
31
+ from ...logger.core.log_manager import LogManager
32
+ from ....enums.api_enum import ApiEnum
33
+ from ..enums.segment_operator_value_enum import SegmentOperatorValueEnum
34
+
35
+ _WEB_TESTING_JSON_STRING_KEY_REGEX = re.compile(r'"([^"\\]*)"\s*:')
36
+
37
+ def coerce_campaign_variation_operand_to_str(operand: Any) -> Optional[str]:
38
+ """
39
+ Coerces a campaignVariation operand to a string when the value is a valid string or finite number.
40
+
41
+ :param operand: Raw campaignVariation operand from the segmentation DSL.
42
+ :return: Coerced operand string, or None if the type cannot be coerced.
43
+ """
44
+ if is_null(operand):
45
+ return None
46
+ if is_boolean(operand):
47
+ return None
48
+ if is_string(operand):
49
+ return operand
50
+ if is_number(operand):
51
+ # bool excluded above; avoids treating True as 1 via int subclassing.
52
+ if type(operand) is float:
53
+ if not math.isfinite(operand):
54
+ return None
55
+ if operand.is_integer():
56
+ return str(int(operand))
57
+ return str(operand)
58
+ return str(operand)
59
+ if is_array(operand) or is_object(operand):
60
+ return None
61
+ return None
62
+
63
+
64
+ def normalize_web_testing_campaigns_map(web_testing_source: Dict) -> Dict[str, str]:
65
+ """
66
+ Normalizes a web_testing_campaigns map so campaign and variation IDs are non-empty string keys and values.
67
+
68
+ :param web_testing_source: Campaign ID to variation ID map from context or parsed JSON.
69
+ :return: Map with string campaign keys and string variation values.
70
+ """
71
+ normalized_campaigns_map: Dict[str, str] = {}
72
+ # iterate over the web testing source and normalize the campaign ids and variation ids
73
+ for campaign_key in web_testing_source:
74
+ variation_raw = web_testing_source[campaign_key]
75
+ # convert the campaign key to a string
76
+ campaign_key_as_str = str(campaign_key)
77
+ # if the variation raw is not None and the campaign key as string is not empty, then add the campaign key and variation to the normalized campaigns map
78
+ if is_defined(variation_raw) and len(campaign_key_as_str) > 0:
79
+ normalized_campaigns_map[campaign_key_as_str] = str(variation_raw)
80
+ return normalized_campaigns_map
81
+
82
+
83
+ def _log_first_duplicate_json_object_key_scan(
84
+ web_testing_campaigns_json: str,
85
+ context: ContextModel,
86
+ ) -> None:
87
+ """
88
+ Scans a web_testing_campaigns JSON string for duplicate object keys and logs the first duplicate found.
89
+
90
+ :param web_testing_campaigns_json: Trimmed JSON string of web_testing_campaigns.
91
+ :param context: User context used for debug metadata in the error log.
92
+ :return: None; parsing continues after logging (last duplicate value wins).
93
+ """
94
+ # set to track the keys seen so far
95
+ keys_seen_so_far = set()
96
+ # iterate over the web testing campaigns json and log the first duplicate json object key
97
+ for key_regex_match in _WEB_TESTING_JSON_STRING_KEY_REGEX.finditer(
98
+ web_testing_campaigns_json
99
+ ):
100
+ object_key_name = key_regex_match.group(1)
101
+ # if the object key name is already in the keys seen so far, then log an error
102
+ if object_key_name in keys_seen_so_far:
103
+ LogManager.get_instance().error_log(
104
+ "INVALID_WEB_TESTING_CAMPAIGNS_DUPLICATE_KEY",
105
+ {"duplicateKey": object_key_name},
106
+ {
107
+ "an": ApiEnum.GET_FLAG.value,
108
+ "uuid": context.get_vwo_uuid(),
109
+ "sId": context.get_session_id(),
110
+ },
111
+ )
112
+ return
113
+ # add the object key name to the keys seen so far
114
+ keys_seen_so_far.add(object_key_name)
115
+
116
+ def is_web_testing_campaigns_absent_or_null(context: ContextModel) -> bool:
117
+ """
118
+ Returns whether platform_variables.web_testing_campaigns is missing or explicitly null.
119
+
120
+ :param context: User context that may carry platform_variables.
121
+ :return: True if web_testing_campaigns is absent or null; otherwise False.
122
+ """
123
+ # get the platform variables
124
+ platform_variables = context.get_platform_variables()
125
+ # if the platform variables are not present, then return True
126
+ if is_null(platform_variables) or not platform_variables:
127
+ return True
128
+ if not is_object(platform_variables):
129
+ return True
130
+ if "web_testing_campaigns" not in platform_variables:
131
+ return True
132
+ # get the web testing campaigns input
133
+ web_testing_campaigns_input = platform_variables.get("web_testing_campaigns")
134
+ # if the web testing campaigns input is None, then return True
135
+ if is_null(web_testing_campaigns_input):
136
+ return True
137
+ return False
138
+
139
+ def dsl_contains_campaign_variation_node(segmentation_dsl: Any) -> bool:
140
+ """
141
+ Recursively checks whether a segmentation DSL tree contains a campaignVariation operator node.
142
+
143
+ :param segmentation_dsl: Segmentation DSL object to walk (and/or/not/campaignVariation nodes).
144
+ :return: True if any campaignVariation node exists in the tree; otherwise False.
145
+ """
146
+ # if the segmentation dsl is None or not a dictionary, then return False
147
+ if is_null(segmentation_dsl) or not is_object(segmentation_dsl):
148
+ return False
149
+
150
+ # iterate over the segmentation dsl and check if it contains a campaign variation node
151
+ for dsl_property_name in segmentation_dsl:
152
+ # if the dsl property name is the campaign variation node, then return True
153
+ if dsl_property_name == SegmentOperatorValueEnum.WEB_CAMPAIGN_VARIATION.value:
154
+ return True
155
+
156
+ nested_value = segmentation_dsl[dsl_property_name]
157
+ if is_array(nested_value):
158
+ for list_item in nested_value:
159
+ if dsl_contains_campaign_variation_node(list_item):
160
+ return True
161
+ elif is_object(nested_value):
162
+ if dsl_contains_campaign_variation_node(nested_value):
163
+ return True
164
+
165
+ return False
166
+
167
+
168
+ def parse_web_testing_campaigns_from_context(
169
+ context: ContextModel,
170
+ ) -> Optional[Dict[str, str]]:
171
+ """
172
+ Parses context.platform_variables.web_testing_campaigns from a dict or JSON string into a normalized map.
173
+
174
+ :param context: User context containing platform_variables.web_testing_campaigns.
175
+ :return: Normalized campaign-to-variation map, or None if missing, invalid, or unparseable.
176
+ """
177
+ platform_variables = context.get_platform_variables()
178
+ if is_null(platform_variables) or not platform_variables:
179
+ return None
180
+ if not is_object(platform_variables):
181
+ return None
182
+
183
+ web_testing_campaigns_raw = platform_variables.get("web_testing_campaigns")
184
+ if is_null(web_testing_campaigns_raw):
185
+ return None
186
+
187
+ # if the web testing campaigns raw is an object map, normalize it.
188
+ if is_object(web_testing_campaigns_raw):
189
+ return normalize_web_testing_campaigns_map(web_testing_campaigns_raw)
190
+
191
+ # if the web testing campaigns raw is a string, then parse the web testing campaigns
192
+ if is_string(web_testing_campaigns_raw):
193
+ trimmed_json_input = web_testing_campaigns_raw.strip()
194
+ if trimmed_json_input == "":
195
+ return None
196
+ _log_first_duplicate_json_object_key_scan(trimmed_json_input, context)
197
+ try:
198
+ # parse the web testing campaigns as a json object
199
+ parsed_json_object = json.loads(trimmed_json_input)
200
+ # if the parsed json object is an object map, normalize it
201
+ if is_object(parsed_json_object):
202
+ return normalize_web_testing_campaigns_map(parsed_json_object)
203
+ # if the parsed json object is not an object map, then log an error
204
+ LogManager.get_instance().error_log(
205
+ "INVALID_WEB_TESTING_CAMPAIGNS_JSON",
206
+ {},
207
+ {
208
+ "an": ApiEnum.GET_FLAG.value,
209
+ "uuid": context.get_vwo_uuid(),
210
+ "sId": context.get_session_id(),
211
+ },
212
+ )
213
+ except json.JSONDecodeError:
214
+ # if the json decode error is raised, then log an error
215
+ LogManager.get_instance().error_log(
216
+ "INVALID_WEB_TESTING_CAMPAIGNS_JSON",
217
+ {},
218
+ {
219
+ "an": ApiEnum.GET_FLAG.value,
220
+ "uuid": context.get_vwo_uuid(),
221
+ "sId": context.get_session_id(),
222
+ },
223
+ )
224
+ return None
225
+
226
+ # if the web testing campaigns raw is not an object or a string, then log an error
227
+ value_kind_label = get_type(web_testing_campaigns_raw)
228
+ LogManager.get_instance().error_log(
229
+ "INVALID_WEB_TESTING_CAMPAIGNS_TYPE",
230
+ {"kind": value_kind_label},
231
+ {
232
+ "an": ApiEnum.GET_FLAG.value,
233
+ "uuid": context.get_vwo_uuid(),
234
+ "sId": context.get_session_id(),
235
+ },
236
+ )
237
+ return None
238
+
239
+ def evaluate_web_testing_campaign_variation(
240
+ operand_encoding: str,
241
+ campaigns_map: Optional[Dict[str, str]],
242
+ ) -> Tuple[bool, bool]:
243
+ """
244
+ Evaluates a campaignVariation operand encoding against the user's web testing campaign assignments.
245
+
246
+ :param operand_encoding: Encoded operand (e.g. "123", "123_2", "123_!2", "!123").
247
+ :param campaigns_map: Normalized map of campaign ID to assigned variation ID, or None.
248
+ :return: Tuple of (segment matched, operand encoding invalid).
249
+ """
250
+ if not is_string(operand_encoding):
251
+ return (False, True)
252
+
253
+ assignments_by_campaign_id = (
254
+ campaigns_map if is_defined(campaigns_map) else {}
255
+ )
256
+
257
+ # e.g. "!123" — not in campaign 123 (no assignment for that campaign id).
258
+ not_in_campaign_regex_match = re.match(r"^!(\d+)$", operand_encoding)
259
+ if not_in_campaign_regex_match:
260
+ campaign_id_digits = not_in_campaign_regex_match.group(1)
261
+ exists_in_assignments_map = campaign_id_digits in assignments_by_campaign_id
262
+ return (not exists_in_assignments_map, False)
263
+
264
+ # e.g. "123_!3" — in campaign 123 but assigned variation is not 3.
265
+ in_campaign_but_not_variation_regex_match = re.match(r"^(\d+)_!(\d+)$", operand_encoding)
266
+ # if the operand encoding is a campaign and variation id, then check if the user is in the campaign but not the variation
267
+ if in_campaign_but_not_variation_regex_match:
268
+ campaign_id_digits = in_campaign_but_not_variation_regex_match.group(1)
269
+ variation_id_digits = in_campaign_but_not_variation_regex_match.group(2)
270
+ # if the campaign id is not in the assignments by campaign id, then return False and False
271
+ if campaign_id_digits not in assignments_by_campaign_id:
272
+ return (False, False)
273
+ assigned_variation_id = assignments_by_campaign_id[campaign_id_digits]
274
+ return (assigned_variation_id != variation_id_digits, False)
275
+
276
+ # e.g. "123_3" — in campaign 123 with variation exactly 3.
277
+ campaign_and_variation_regex_match = re.match(r"^(\d+)_(\d+)$", operand_encoding)
278
+ if campaign_and_variation_regex_match:
279
+ # if the campaign id is not in the assignments by campaign id, then return False
280
+ campaign_id_digits = campaign_and_variation_regex_match.group(1)
281
+ # get the variation id digits
282
+ variation_id_digits = campaign_and_variation_regex_match.group(2)
283
+ if campaign_id_digits not in assignments_by_campaign_id:
284
+ return (False, False)
285
+ assigned_variation_id = assignments_by_campaign_id[campaign_id_digits]
286
+ return (assigned_variation_id == variation_id_digits, False)
287
+
288
+ # e.g. "123" — in campaign 123 (any assigned variation counts).
289
+ campaign_id_only_regex_match = re.match(r"^(\d+)$", operand_encoding)
290
+ if campaign_id_only_regex_match:
291
+ campaign_id_digits = campaign_id_only_regex_match.group(1)
292
+ user_in_campaign = campaign_id_digits in assignments_by_campaign_id
293
+ return (user_in_campaign, False)
294
+
295
+ return (False, True)
@@ -49,5 +49,12 @@
49
49
 
50
50
  "ERROR_SENDING_DEBUG_EVENT": "Error occurred while sending debugger event. Error:{err}",
51
51
  "ERROR_SETTING_ALIAS": "Error while setting alias on gateway service. Error: {err}",
52
- "ERROR_GETTING_ALIAS": "Error while getting alias from gateway service. Error: {err}"
52
+ "ERROR_GETTING_ALIAS": "Error while getting alias from gateway service. Error: {err}",
53
+
54
+ "INVALID_WEB_TESTING_CAMPAIGNS_JSON": "webTestingCampaigns must be a JSON object (not an array)",
55
+ "INVALID_WEB_TESTING_CAMPAIGNS_DUPLICATE_KEY": "webTestingCampaigns JSON string contains duplicate object key: {duplicateKey}, only last occurrence is considered",
56
+ "INVALID_WEB_TESTING_CAMPAIGNS_TYPE": "webTestingCampaigns has invalid type: {kind}",
57
+ "INVALID_WEB_TESTING_CAMPAIGN_VARIATION_OPERAND_TYPE": "campaignVariation operand must be a string or finite number, got type: {type}",
58
+ "INVALID_WEB_TESTING_CAMPAIGN_VARIATION_OPERAND_EMPTY": "campaignVariation operand is empty",
59
+ "INVALID_WEB_TESTING_CAMPAIGN_VARIATION_OPERAND_FORMAT": "Invalid campaignVariation operand encoding: {operandEncoding}"
53
60
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: wingify-fme-python-sdk
3
- Version: 1.55.0
3
+ Version: 1.60.0
4
4
  Summary: Wingify Feature Management and Experimentation SDK for Python
5
5
  Home-page: https://github.com/wingify/vwo-fme-python-sdk
6
6
  Author: Wingify
@@ -168,9 +168,30 @@ context = {
168
168
  'user_agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36',
169
169
  'ip_address': '1.1.1.1',
170
170
  'session_id': 1697123456 # Optional: Custom session ID for web client integration
171
+ 'platformVariables': {
172
+ # Reference example only:.
173
+ # In production, fetch campaign assignments using script run in frontend and pass that object to backend as webTestingCampaigns.
174
+ 'webTestingCampaigns': '{"122":"1","130":"2"}'
175
+ },
176
+ 'session_id': 1697123456 # Optional: Custom session ID for web client integration
171
177
  }
172
178
  ```
173
179
 
180
+ ## Web testing pre-segmentation
181
+
182
+ Server-side flag decisions can align with **Web Testing** (browser) experiments. **Campaign assignments are typically read on the frontend** (for example, VWO cookies) and sent to your server; pass them in `platformVariables.webTestingCampaigns` as a map of **campaign ID -> variation ID** (strings), or as a **JSON string** of that object.
183
+
184
+ Pre-segment rules in the VWO dashboard can use the **`campaignVariation`** operator:
185
+
186
+ | Operand pattern | Meaning |
187
+ | --------------- | ------- |
188
+ | `C` | User is in campaign `C` (any variation). |
189
+ | `!C` | User is **not** in campaign `C`. |
190
+ | `C_V` | User is in campaign `C` with variation `V`. |
191
+ | `C_!V` | User is in campaign `C` and assigned variation is **not** `V`. |
192
+
193
+ Rollout rules evaluate pre-segments from the **first variation** only; AB/testing rules use **campaign-level** segments.
194
+
174
195
  ## Session Management
175
196
 
176
197
  The SDK provides automatic session management capabilities to enable seamless integration with VWO's web client testing campaigns. Session IDs are automatically generated and managed to connect server-side feature flag decisions with client-side user sessions.
@@ -105,6 +105,7 @@ wingify/packages/segmentation_evaluator/evaluators/segment_evaluator.py
105
105
  wingify/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.py
106
106
  wingify/packages/segmentation_evaluator/utils/__init__.py
107
107
  wingify/packages/segmentation_evaluator/utils/segment_util.py
108
+ wingify/packages/segmentation_evaluator/utils/web_testing_segment_util.py
108
109
  wingify/packages/storage/__init__.py
109
110
  wingify/packages/storage/connector.py
110
111
  wingify/packages/storage/storage.py