kameleoon-client-python 3.22.0__tar.gz → 3.22.1__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 (205) hide show
  1. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/PKG-INFO +16 -16
  2. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/visitor_manager.py +11 -13
  3. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/multi_threading.py +34 -14
  4. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/scheduler.py +17 -3
  5. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/string_utils.py +2 -3
  6. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/tracking/tracking_manager.py +60 -17
  7. kameleoon_client_python-3.22.1/kameleoon/managers/tracking/visitor_tracking_registry.py +112 -0
  8. kameleoon_client_python-3.22.1/kameleoon/network/access_token_source.py +175 -0
  9. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/net_provider.py +2 -1
  10. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/service_impl.py +3 -1
  11. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/sdk_version.py +1 -1
  12. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon_client_python.egg-info/PKG-INFO +16 -16
  13. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon_client_python.egg-info/SOURCES.txt +1 -5
  14. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon_client_python.egg-info/requires.txt +15 -15
  15. kameleoon_client_python-3.22.1/tests/benchmarks/visitor_tracking_registry_benchmark.py +416 -0
  16. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/proj/settings.py +0 -2
  17. kameleoon_client_python-3.22.0/kameleoon/managers/tracking/visitor_tracking_registry.py +0 -89
  18. kameleoon_client_python-3.22.0/kameleoon/network/access_token_source.py +0 -138
  19. kameleoon_client_python-3.22.0/kameleoon/storage/cache.py +0 -67
  20. kameleoon_client_python-3.22.0/kameleoon/storage/cache_factory.py +0 -17
  21. kameleoon_client_python-3.22.0/kameleoon/storage/cache_factory_impl.py +0 -12
  22. kameleoon_client_python-3.22.0/kameleoon/storage/cache_impl.py +0 -36
  23. kameleoon_client_python-3.22.0/tests/integration/proj/proj/__init__.py +0 -0
  24. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/MANIFEST.in +0 -0
  25. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/README.md +0 -0
  26. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/__init__.py +0 -0
  27. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/client_readiness/__init__.py +0 -0
  28. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/client_readiness/async_readiness.py +0 -0
  29. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/client_readiness/threading_readiness.py +0 -0
  30. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/__init__.py +0 -0
  31. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/consent_blocking_behaviour.py +0 -0
  32. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/custom_data_info.py +0 -0
  33. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/data_file.py +0 -0
  34. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/experiment.py +0 -0
  35. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/feature_flag.py +0 -0
  36. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/me_group.py +0 -0
  37. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/rule.py +0 -0
  38. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/rule_type.py +0 -0
  39. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/settings.py +0 -0
  40. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/variable.py +0 -0
  41. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/variation.py +0 -0
  42. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/configuration/variation_by_exposition.py +0 -0
  43. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/__init__.py +0 -0
  44. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/application_version.py +0 -0
  45. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/browser.py +0 -0
  46. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/cbscores.py +0 -0
  47. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/conversion.py +0 -0
  48. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/cookie.py +0 -0
  49. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/custom_data.py +0 -0
  50. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/data.py +0 -0
  51. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/device.py +0 -0
  52. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/geolocation.py +0 -0
  53. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/kcs_heat.py +0 -0
  54. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/__init__.py +0 -0
  55. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/assigned_variation.py +0 -0
  56. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/forced_experiment_variation.py +0 -0
  57. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/forced_feature_variation.py +0 -0
  58. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/forced_variation.py +0 -0
  59. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/legal_consent.py +0 -0
  60. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/page_view_visit.py +0 -0
  61. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/visitor.py +0 -0
  62. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/manager/visitor_slot.py +0 -0
  63. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/mapping_identifier.py +0 -0
  64. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/operating_system.py +0 -0
  65. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/page_view.py +0 -0
  66. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/personalization.py +0 -0
  67. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/targeted_segment.py +0 -0
  68. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/unique_identifier.py +0 -0
  69. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/user_agent.py +0 -0
  70. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/data/visitor_visits.py +0 -0
  71. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/defaults.py +0 -0
  72. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/__init__.py +0 -0
  73. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/data_file_update_event.py +0 -0
  74. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/data_file_update_handler.py +0 -0
  75. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/event_handler.py +0 -0
  76. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/event_manager.py +0 -0
  77. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/event_type.py +0 -0
  78. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/http_request_failure.py +0 -0
  79. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/http_request_handler.py +0 -0
  80. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/events/request_type.py +0 -0
  81. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/exceptions.py +0 -0
  82. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/__init__.py +0 -0
  83. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/domain.py +0 -0
  84. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/functions.py +0 -0
  85. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/hasher.py +0 -0
  86. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/logger.py +0 -0
  87. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/nonce.py +0 -0
  88. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/repeat_timer.py +0 -0
  89. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/sem_version.py +0 -0
  90. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/time.py +0 -0
  91. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/helpers/visitor_code.py +0 -0
  92. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/hybrid/__init__.py +0 -0
  93. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/hybrid/hybrid_manager.py +0 -0
  94. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/hybrid/hybrid_manager_impl.py +0 -0
  95. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/kameleoon_client.py +0 -0
  96. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/kameleoon_client_config.py +0 -0
  97. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/kameleoon_client_factory.py +0 -0
  98. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/logging/__init__.py +0 -0
  99. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/logging/kameleoon_logger.py +0 -0
  100. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/logging/log_level.py +0 -0
  101. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/logging/logger.py +0 -0
  102. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/__init__.py +0 -0
  103. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/asyncexec/__init__.py +0 -0
  104. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/asyncexec/async_executor.py +0 -0
  105. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/data/__init__.py +0 -0
  106. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/data/data_manager.py +0 -0
  107. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/evaluation/__init__.py +0 -0
  108. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/evaluation/evaluated_experiment.py +0 -0
  109. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/remote_data/__init__.py +0 -0
  110. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/remote_data/remote_data_manager.py +0 -0
  111. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/remote_data/remote_visitor_data.py +0 -0
  112. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/tracking/__init__.py +0 -0
  113. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/tracking/tracking_builder.py +0 -0
  114. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/warehouse/__init__.py +0 -0
  115. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/managers/warehouse/warehouse_manager.py +0 -0
  116. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/middleware.py +0 -0
  117. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/__init__.py +0 -0
  118. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/access_token_source_factory.py +0 -0
  119. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/activity_event.py +0 -0
  120. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/cookie/__init__.py +0 -0
  121. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/cookie/cookie_manager.py +0 -0
  122. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/net_provider_impl.py +0 -0
  123. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/network_manager.py +0 -0
  124. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/network_manager_factory.py +0 -0
  125. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/network_manager_factory_impl.py +0 -0
  126. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/network_manager_impl.py +0 -0
  127. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/query_builder.py +0 -0
  128. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/query_encodable.py +0 -0
  129. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/sendable.py +0 -0
  130. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/service_initialize_context.py +0 -0
  131. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/service_initialize_context_impl.py +0 -0
  132. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/__init__.py +0 -0
  133. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/automation_service.py +0 -0
  134. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/automation_service_impl.py +0 -0
  135. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/configuration_service.py +0 -0
  136. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/configuration_service_impl.py +0 -0
  137. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/data_service.py +0 -0
  138. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/data_service_impl.py +0 -0
  139. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/services/service.py +0 -0
  140. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/uri_helper.py +0 -0
  141. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/network/url_provider.py +0 -0
  142. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/real_time/__init__.py +0 -0
  143. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/real_time/real_time_event.py +0 -0
  144. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/real_time/real_time_event_service.py +0 -0
  145. {kameleoon_client_python-3.22.0/kameleoon/storage → kameleoon_client_python-3.22.1/kameleoon/targeting}/__init__.py +0 -0
  146. {kameleoon_client_python-3.22.0/kameleoon/targeting → kameleoon_client_python-3.22.1/kameleoon/targeting/conditions}/__init__.py +0 -0
  147. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/browser_condition.py +0 -0
  148. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/constants.py +0 -0
  149. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/conversion_condition.py +0 -0
  150. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/cookie_condition.py +0 -0
  151. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/custom_datum.py +0 -0
  152. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/device_condition.py +0 -0
  153. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/exclusive_experiment_condition.py +0 -0
  154. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/geolocation_condition.py +0 -0
  155. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/kcs_heat_range_condition.py +0 -0
  156. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/number_condition.py +0 -0
  157. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/operating_system_condition.py +0 -0
  158. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/page_title_condition.py +0 -0
  159. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/page_url_condition.py +0 -0
  160. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/page_view_number_condition.py +0 -0
  161. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/previous_page_condition.py +0 -0
  162. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/sdk_language_condition.py +0 -0
  163. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/segment_condition.py +0 -0
  164. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/string_value_condition.py +0 -0
  165. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/target_experiment_condition.py +0 -0
  166. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/target_feature_flag_condition.py +0 -0
  167. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/target_personalization_condition.py +0 -0
  168. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/targeting_condition.py +0 -0
  169. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/time_elapsed_since_visit_condition.py +0 -0
  170. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/unknown_condition.py +0 -0
  171. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/version_condition.py +0 -0
  172. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/visit_number_today_condition.py +0 -0
  173. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/visit_number_total_condition.py +0 -0
  174. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/visitor_code_condition.py +0 -0
  175. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/visitor_new_return_condition.py +0 -0
  176. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/conditions/visitor_scope_condition.py +0 -0
  177. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/models.py +0 -0
  178. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/targeting_manager.py +0 -0
  179. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/tree_builder.py +0 -0
  180. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/targeting/tree_condition_factory.py +0 -0
  181. {kameleoon_client_python-3.22.0/kameleoon/targeting/conditions → kameleoon_client_python-3.22.1/kameleoon/types}/__init__.py +0 -0
  182. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/types/data_file.py +0 -0
  183. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/types/feature_flag.py +0 -0
  184. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/types/remote_visitor_data_filter.py +0 -0
  185. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/types/rule.py +0 -0
  186. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/types/variable.py +0 -0
  187. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon/types/variation.py +0 -0
  188. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon_client_python.egg-info/dependency_links.txt +0 -0
  189. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/kameleoon_client_python.egg-info/top_level.txt +0 -0
  190. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/setup.cfg +0 -0
  191. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/setup.py +0 -0
  192. {kameleoon_client_python-3.22.0/kameleoon/types → kameleoon_client_python-3.22.1/tests/integration/proj}/__init__.py +0 -0
  193. {kameleoon_client_python-3.22.0/tests/integration/proj → kameleoon_client_python-3.22.1/tests/integration/proj/kameleoon_app}/__init__.py +0 -0
  194. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/kameleoon_app/admin.py +0 -0
  195. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/kameleoon_app/apps.py +0 -0
  196. {kameleoon_client_python-3.22.0/tests/integration/proj/kameleoon_app → kameleoon_client_python-3.22.1/tests/integration/proj/kameleoon_app/migrations}/__init__.py +0 -0
  197. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/kameleoon_app/models.py +0 -0
  198. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/kameleoon_app/tests.py +0 -0
  199. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/kameleoon_app/views.py +0 -0
  200. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/manage.py +0 -0
  201. {kameleoon_client_python-3.22.0/tests/integration/proj/kameleoon_app/migrations → kameleoon_client_python-3.22.1/tests/integration/proj/proj}/__init__.py +0 -0
  202. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/proj/asgi.py +0 -0
  203. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/proj/urls.py +0 -0
  204. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/integration/proj/proj/wsgi.py +0 -0
  205. {kameleoon_client_python-3.22.0 → kameleoon_client_python-3.22.1}/tests/resources/config.yml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kameleoon-client-python
3
- Version: 3.22.0
3
+ Version: 3.22.1
4
4
  Summary: Kameleoon Client Python Software Development Kit.
5
5
  Home-page: https://developers.kameleoon.com/python-sdk.html
6
6
  Author: Kameleoon
@@ -20,27 +20,27 @@ Requires-Dist: requests<3.0.0,>=2.25
20
20
  Requires-Dist: sseclient-py<1.9.0,>=1.7.0
21
21
  Requires-Dist: aiohttp<4.0.0,>=3.8.0
22
22
  Provides-Extra: test
23
- Requires-Dist: types-requests==2.25.0; extra == "test"
24
- Requires-Dist: Django==3.2.4; extra == "test"
23
+ Requires-Dist: types-python-dateutil==2.8.2; extra == "test"
24
+ Requires-Dist: twine==3.4.1; extra == "test"
25
25
  Requires-Dist: sseclient-py<1.9.0,>=1.7.0; extra == "test"
26
- Requires-Dist: pytest-django==4.4.0; extra == "test"
27
- Requires-Dist: pytest-pythonpath==0.7.3; extra == "test"
28
- Requires-Dist: pytest-ordering==0.6; extra == "test"
26
+ Requires-Dist: types-PyYAML==5.4.3; extra == "test"
29
27
  Requires-Dist: aiohttp<4.0.0,>=3.8.0; extra == "test"
30
- Requires-Dist: flake8==4.0.1; extra == "test"
31
- Requires-Dist: pytest-asyncio==0.21.*; extra == "test"
32
- Requires-Dist: pytest-cov==2.12.1; extra == "test"
33
- Requires-Dist: mypy==0.991; extra == "test"
34
- Requires-Dist: pylint==2.12.*; extra == "test"
35
- Requires-Dist: Werkzeug==2.0.1; extra == "test"
36
28
  Requires-Dist: pyyaml<6.1.0,>=5.4.0; extra == "test"
37
- Requires-Dist: freezegun==1.1.0; extra == "test"
38
- Requires-Dist: twine==3.4.1; extra == "test"
39
- Requires-Dist: types-PyYAML==5.4.3; extra == "test"
40
29
  Requires-Dist: requests<3.0.0,>=2.25; extra == "test"
30
+ Requires-Dist: pylint==2.12.*; extra == "test"
31
+ Requires-Dist: pytest-pythonpath==0.7.3; extra == "test"
32
+ Requires-Dist: pytest-django==4.5.2; extra == "test"
33
+ Requires-Dist: flake8==4.0.1; extra == "test"
34
+ Requires-Dist: pytest-cov==2.12.1; extra == "test"
41
35
  Requires-Dist: wheel==0.36.2; extra == "test"
42
- Requires-Dist: types-python-dateutil==2.8.2; extra == "test"
36
+ Requires-Dist: freezegun==1.1.0; extra == "test"
37
+ Requires-Dist: types-requests==2.25.0; extra == "test"
38
+ Requires-Dist: pytest-ordering==0.6; extra == "test"
43
39
  Requires-Dist: aioresponses==0.7.*; extra == "test"
40
+ Requires-Dist: mypy==0.991; extra == "test"
41
+ Requires-Dist: Werkzeug==2.0.1; extra == "test"
42
+ Requires-Dist: pytest-asyncio==0.21.*; extra == "test"
43
+ Requires-Dist: Django==4.2.*; extra == "test"
44
44
  Dynamic: author
45
45
  Dynamic: author-email
46
46
  Dynamic: classifier
@@ -67,17 +67,15 @@ class VisitorManager:
67
67
  KameleoonLogger.debug("RETURN: VisitorManager.get_or_create_visitor(visitor_code: %s)", visitor_code)
68
68
 
69
69
  def peek_visitor(self, visitor_code: str) -> Optional[Visitor]:
70
- """Retrieves a visitor by their code if it exists, otherwise returns `None`."""
71
- KameleoonLogger.debug(f"CALL: VisitorManager.peekVisitor(visitor_code: {visitor_code})")
72
- if (slot := self.__try_acquire_slot(visitor_code)) is None:
73
- visitor = None
74
- else:
75
- try:
76
- visitor = slot.visitor
77
- finally:
78
- slot.lock.release()
70
+ """
71
+ Retrieves a visitor by their code if it exists, otherwise returns `None`. Unlike `get_visitor`, does not
72
+ count as visitor activity and takes no lock: a plain read needs no protection against the purge.
73
+ """
74
+ KameleoonLogger.debug("CALL: VisitorManager.peek_visitor(visitor_code: %s)", visitor_code)
75
+ slot = self._slots.get(visitor_code)
76
+ visitor = slot.visitor if slot is not None else None
79
77
  KameleoonLogger.debug(
80
- f"RETURN: VisitorManager.peekVisitor(visitor_code: {visitor_code}) -> " f"(visitor: {visitor})"
78
+ "RETURN: VisitorManager.peek_visitor(visitor_code: %s) -> (visitor: %s)", visitor_code, visitor
81
79
  )
82
80
  return visitor
83
81
 
@@ -180,7 +178,7 @@ class VisitorManager:
180
178
  while True:
181
179
  slot = self._slots[visitor_code]
182
180
  slot.lock.acquire()
183
- if slot == self._slots.get(visitor_code):
181
+ if slot is self._slots.get(visitor_code):
184
182
  return slot
185
183
  slot.lock.release()
186
184
 
@@ -190,7 +188,7 @@ class VisitorManager:
190
188
  if slot is None:
191
189
  return None
192
190
  slot.lock.acquire()
193
- if slot == self._slots.get(visitor_code):
191
+ if slot is self._slots.get(visitor_code):
194
192
  return slot
195
193
  slot.lock.release()
196
194
 
@@ -199,7 +197,7 @@ class VisitorManager:
199
197
  if slot is None:
200
198
  return None
201
199
  slot.lock.acquire()
202
- if slot == self._slots.get(visitor_code):
200
+ if slot is self._slots.get(visitor_code):
203
201
  return slot
204
202
  slot.lock.release()
205
203
  return None
@@ -12,6 +12,10 @@ class ThreadEventLoop:
12
12
  ThreadEventLoop is helper object to handle asynchronous tasks execution in a separate thread.
13
13
  """
14
14
 
15
+ # Upper bound for `stop` to wait for the running tasks and for the thread to exit; a task that never
16
+ # completes (or a loop thread blocked by a synchronous call) must not hang the disposal forever.
17
+ STOP_TIMEOUT = 10.0
18
+
15
19
  def __init__(self) -> None:
16
20
  self.loop: Optional[asyncio.AbstractEventLoop] = None
17
21
  self.thread: Optional[threading.Thread] = None
@@ -40,17 +44,38 @@ class ThreadEventLoop:
40
44
  return self.loop
41
45
 
42
46
  def stop(self) -> None:
43
- """Stops the thread and the event loop"""
47
+ """
48
+ Stops the thread and the event loop.
49
+
50
+ Waits (up to `STOP_TIMEOUT`) for the already scheduled tasks to complete before stopping the loop.
51
+ """
44
52
  if self.thread is None:
45
53
  return
46
54
  self.running_allowed = False
47
- time.sleep(0.001)
48
- while self.count_tasks() > 0:
49
- time.sleep(0.01)
50
55
  loop = self.get_loop()
56
+ if threading.current_thread() is self.thread:
57
+ # Called from the loop's own thread (e.g. a GC-triggered `__del__`): it cannot wait for or join itself.
58
+ loop.call_soon(loop.stop)
59
+ return
60
+ # The draining runs on the loop itself, after everything already queued (e.g. tasks being created by
61
+ # `run_coroutine_threadsafe`), so it observes the tasks a cross-thread `all_tasks` poll could miss.
62
+ drained = asyncio.run_coroutine_threadsafe(self.__wait_for_tasks(), loop)
63
+ try:
64
+ drained.result(timeout=self.STOP_TIMEOUT)
65
+ except Exception: # pylint: disable=W0703
66
+ drained.cancel()
51
67
  loop.call_soon_threadsafe(loop.stop)
52
- self.thread.join()
53
- loop.close()
68
+ self.thread.join(timeout=self.STOP_TIMEOUT)
69
+ if not self.thread.is_alive():
70
+ loop.close()
71
+
72
+ @staticmethod
73
+ async def __wait_for_tasks() -> None:
74
+ while True:
75
+ tasks = [task for task in asyncio.all_tasks() if task is not asyncio.current_task()]
76
+ if not tasks:
77
+ return
78
+ await asyncio.wait(tasks, timeout=ThreadEventLoop.STOP_TIMEOUT)
54
79
 
55
80
  def run_coro(self, coro: Coroutine[Any, Any, Any]) -> Optional[concurrent.futures.Future[Any]]:
56
81
  """
@@ -66,10 +91,6 @@ class ThreadEventLoop:
66
91
  self.start()
67
92
  return asyncio.run_coroutine_threadsafe(coro, self.get_loop())
68
93
 
69
- def count_tasks(self) -> int:
70
- """Returns the number of running tasks."""
71
- return len(asyncio.all_tasks(self.get_loop()))
72
-
73
94
 
74
95
  def run_in_thread(
75
96
  func: Callable[[], Any], thread_name: Optional[str] = None, with_event_loop=False
@@ -86,16 +107,15 @@ def run_in_thread(
86
107
  :rtype: threading.Thread
87
108
  """
88
109
  if with_event_loop:
110
+
89
111
  def target():
90
112
  loop = asyncio.new_event_loop()
91
113
  func()
92
114
  loop.close()
115
+
93
116
  else:
94
117
  target = func
95
- thread = threading.Thread(target=target)
96
- thread.daemon = True
97
- if thread_name:
98
- thread.setName(thread_name)
118
+ thread = threading.Thread(target=target, name=thread_name or None, daemon=True)
99
119
  thread.start()
100
120
  return thread
101
121
 
@@ -42,10 +42,14 @@ class Scheduler:
42
42
 
43
43
  def stop(self) -> None:
44
44
  """
45
- Starts all scheduled jobs.
45
+ Stops all scheduled jobs.
46
46
 
47
47
  Should not be called more than once.
48
48
  """
49
+ if self._loop.is_closed():
50
+ # The jobs died with the loop; nothing left to stop (e.g. disposal after the host app closed its loop).
51
+ KameleoonLogger.debug("Scheduler is not stopped: event loop is already closed")
52
+ return
49
53
  try:
50
54
  self._loop.call_soon_threadsafe(self._stop)
51
55
  except Exception as err: # pylint: disable=W0703
@@ -53,8 +57,7 @@ class Scheduler:
53
57
 
54
58
  def _stop(self) -> None:
55
59
  for job in self._jobs.values():
56
- if job.stop_signal is not None:
57
- job.stop_signal.set()
60
+ job.stop()
58
61
 
59
62
  class Job:
60
63
  """`Job` represents a scheduled job"""
@@ -65,13 +68,24 @@ class Scheduler:
65
68
  self.func = func
66
69
  self.stop_signal: Optional[asyncio.Event] = None
67
70
  self.future: Optional[concurrent.futures.Future[Any]] = None
71
+ self._stop_requested = False
68
72
 
69
73
  def run(self, loop: asyncio.AbstractEventLoop) -> None:
70
74
  """Starts the job execution in the given event loop."""
71
75
  if self.future is None:
72
76
  self.future = asyncio.run_coroutine_threadsafe(self._cycle(), loop)
73
77
 
78
+ def stop(self) -> None:
79
+ """Requests the job to stop. Must be called on the job's event loop."""
80
+ # `stop_signal` only exists once `_cycle` has started; a stop posted with `call_soon_threadsafe` right
81
+ # after `run` is processed before the task's first step, so the request has to be remembered as well.
82
+ self._stop_requested = True
83
+ if self.stop_signal is not None:
84
+ self.stop_signal.set()
85
+
74
86
  async def _cycle(self) -> None:
87
+ if self._stop_requested:
88
+ return
75
89
  self.stop_signal = asyncio.Event()
76
90
  interval = self.interval
77
91
  stop_task = asyncio.create_task(self.stop_signal.wait())
@@ -9,7 +9,7 @@ class StringUtils:
9
9
 
10
10
  @staticmethod
11
11
  def secret(secret: Optional[str]) -> str:
12
- """Hide part of the string, keeping only a few initial characters visible."""
12
+ """Hides the string behind a fixed-width mask, keeping only a few trailing characters visible."""
13
13
  hid_ch = '*'
14
14
  vis_count = 4
15
15
 
@@ -21,8 +21,7 @@ class StringUtils:
21
21
  if length <= vis_count:
22
22
  return hid_ch * length
23
23
 
24
- hidden_length = max(length - vis_count, vis_count)
25
- return secret[:length - hidden_length] + hid_ch * hidden_length
24
+ return hid_ch * vis_count + secret[max(length - vis_count, vis_count):]
26
25
 
27
26
  @staticmethod
28
27
  def bool_to_string_lower_case(data: bool) -> str:
@@ -1,5 +1,6 @@
1
1
  """Tracking"""
2
- from typing import Iterable, List
2
+ import asyncio
3
+ from typing import Iterable, List, Optional
3
4
  from kameleoon.data.manager.visitor_manager import VisitorManager
4
5
  from kameleoon.logging.kameleoon_logger import KameleoonLogger
5
6
  from kameleoon.managers.asyncexec.async_executor import AsyncExecutor
@@ -7,7 +8,7 @@ from kameleoon.managers.data.data_manager import DataManager
7
8
  from kameleoon.managers.tracking.tracking_builder import TrackingBuilder
8
9
  from kameleoon.managers.tracking.visitor_tracking_registry import (
9
10
  VisitorTrackingRegistry,
10
- LockVisitorTrackingRegistry,
11
+ ConcurrentVisitorTrackingRegistry,
11
12
  )
12
13
  from kameleoon.network.net_provider import Response
13
14
  from kameleoon.network.network_manager import NetworkManager
@@ -19,6 +20,7 @@ class TrackingManager:
19
20
  """Tracking manager"""
20
21
  LINES_DELIMITER = "\n"
21
22
  REQUEST_SIZE_LIMIT = 2560 * 1024 # 2.5 * 1024^2 characters
23
+ PROBE_MAX_VISITORS = 1
22
24
 
23
25
  # pylint: disable=R0913
24
26
  def __init__(
@@ -30,7 +32,8 @@ class TrackingManager:
30
32
  "CALL: TrackingManager(data_manager, network_manager. visitor_manager, "
31
33
  "track_interval_seconds: %s, scheduler)", track_interval_seconds
32
34
  )
33
- self._tracking_visitors: VisitorTrackingRegistry = LockVisitorTrackingRegistry(visitor_manager)
35
+ self._tracking_visitors: VisitorTrackingRegistry = ConcurrentVisitorTrackingRegistry(visitor_manager)
36
+ self._probe_mode = False
34
37
  self._data_manager = data_manager
35
38
  self._network_manager = network_manager
36
39
  self._visitor_manager = visitor_manager
@@ -41,6 +44,11 @@ class TrackingManager:
41
44
  "track_interval_seconds: %s, scheduler)", track_interval_seconds
42
45
  )
43
46
 
47
+ @property
48
+ def probe_mode(self) -> bool:
49
+ """Test purpose only"""
50
+ return self._probe_mode
51
+
44
52
  def add_visitor_code(self, visitor_code: str) -> None:
45
53
  """Adds a visitor code to the tracking registry."""
46
54
  KameleoonLogger.debug("CALL: TrackingManager.add_visitor_code(visitor_code: %s)", visitor_code)
@@ -54,7 +62,10 @@ class TrackingManager:
54
62
  `track_all` is triggered by the interval tracking timer.
55
63
  """
56
64
  KameleoonLogger.debug("CALL: TrackingManager.track_all()")
57
- self.__track(self._tracking_visitors.extract())
65
+ if self._probe_mode:
66
+ self.__track_probe()
67
+ else:
68
+ self.__track(self._tracking_visitors.extract())
58
69
  KameleoonLogger.debug("RETURN: TrackingManager.track_all()")
59
70
 
60
71
  def track_visitor(self, visitor_code: str) -> None:
@@ -63,7 +74,14 @@ class TrackingManager:
63
74
  self.__track([visitor_code])
64
75
  KameleoonLogger.debug("RETURN: TrackingManager.track_visitor(visitor_code: %s)", visitor_code)
65
76
 
66
- def __track(self, visitor_codes: Iterable[str]) -> None:
77
+ def __track_probe(self) -> None:
78
+ # Sends one visitor's data; visitors without data are dropped (as on the normal path) until one is found.
79
+ while visitor_codes := self._tracking_visitors.extract(self.PROBE_MAX_VISITORS):
80
+ if self.__track(visitor_codes):
81
+ return
82
+
83
+ def __track(self, visitor_codes: Iterable[str]) -> bool:
84
+ """Returns whether a request was sent."""
67
85
  builder = TrackingBuilder(
68
86
  visitor_codes, self._data_manager.data_file, self._visitor_manager, self.REQUEST_SIZE_LIMIT
69
87
  )
@@ -75,33 +93,58 @@ class TrackingManager:
75
93
  "If it is not caused by the peak load, decreasing the tracking interval is recommended."
76
94
  )
77
95
  self._tracking_visitors.add_all(builder.visitor_codes_to_keep)
78
- self.__perform_tracking_request(
96
+ return self.__perform_tracking_request(
79
97
  builder.visitor_codes_to_send, builder.unsent_visitor_data, builder.tracking_lines
80
98
  )
81
99
 
82
100
  def __perform_tracking_request(
83
101
  self, visitor_codes: List[str], visitors_data: List[Sendable], tracking_lines: List[str]
84
- ) -> None:
102
+ ) -> bool:
103
+ """Returns whether a request was sent."""
85
104
  if not tracking_lines:
86
- return
105
+ return False
87
106
  lines = self.LINES_DELIMITER.join(tracking_lines)
88
107
  # Mark unsent data as transmitted
89
108
  for sendable_data in visitors_data:
90
109
  sendable_data.mark_as_transmitting()
91
110
 
92
- async def call() -> Response:
93
- service = self._network_manager.get_service(DataService)
94
- response = await service.send_tracking_data(lines)
95
- if response.success:
111
+ async def call() -> Optional[Response]:
112
+ response: Optional[Response] = None
113
+ try:
114
+ service = self._network_manager.get_service(DataService)
115
+ response = await service.send_tracking_data(lines)
116
+ except asyncio.CancelledError:
117
+ # Not a Data API failure: roll the data back for the next request without entering probe mode.
118
+ self.__handle_failed_request(visitor_codes, visitors_data)
119
+ raise
120
+ except Exception as exception: # pylint: disable=W0703
121
+ KameleoonLogger.error("Tracking request failed: %s", exception)
122
+ if (response is not None) and response.success:
96
123
  KameleoonLogger.debug("Successful request for tracking visitors: %s, data: %s",
97
124
  visitor_codes, visitors_data)
98
125
  for sendable_data in visitors_data:
99
126
  sendable_data.mark_as_sent()
127
+ self.__leave_probe_mode()
100
128
  else:
101
- KameleoonLogger.debug("Failed request for tracking visitors: %s, data: %s",
102
- visitor_codes, visitors_data)
103
- for sendable_data in visitors_data:
104
- sendable_data.mark_as_unsent()
105
- self._tracking_visitors.add_all(visitor_codes)
129
+ self.__handle_failed_request(visitor_codes, visitors_data)
130
+ self.__enter_probe_mode()
106
131
  return response
107
132
  self._async_executor.run_coro(call(), "send_tracking_data")
133
+ return True
134
+
135
+ def __handle_failed_request(self, visitor_codes: List[str], visitors_data: List[Sendable]) -> None:
136
+ KameleoonLogger.debug("Failed request for tracking visitors: %s, data: %s", visitor_codes, visitors_data)
137
+ for sendable_data in visitors_data:
138
+ sendable_data.mark_as_unsent()
139
+ self._tracking_visitors.add_all(visitor_codes)
140
+
141
+ def __enter_probe_mode(self) -> None:
142
+ if not self._probe_mode:
143
+ self._probe_mode = True
144
+ KameleoonLogger.info("Tracking request failed: only one visitor's data per tracking request"
145
+ " will be sent until a request succeeds")
146
+
147
+ def __leave_probe_mode(self) -> None:
148
+ if self._probe_mode:
149
+ self._probe_mode = False
150
+ KameleoonLogger.info("Tracking request succeeded: full-size tracking requests are restored")
@@ -0,0 +1,112 @@
1
+ """Tracking"""
2
+
3
+ import sys
4
+ from threading import Lock
5
+ from typing import Dict, Iterable, List, Set
6
+ from kameleoon.data.manager.visitor_manager import VisitorManager
7
+
8
+
9
+ class VisitorTrackingRegistry:
10
+ """
11
+ Abstract visitor tracking registry.
12
+
13
+ Must be thread-safe
14
+ """
15
+
16
+ UNLIMITED = sys.maxsize
17
+ DEFAULT_STORAGE_LIMIT = 1_000_000
18
+ DEFAULT_EXTRACTION_LIMIT = 20_000
19
+ LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT = 2
20
+ REMOVAL_FACTOR = 0.8
21
+
22
+ def __init__(
23
+ self,
24
+ visitor_manager: VisitorManager,
25
+ storage_limit=DEFAULT_STORAGE_LIMIT,
26
+ extraction_limit=DEFAULT_EXTRACTION_LIMIT,
27
+ ) -> None:
28
+ self._visitor_manager = visitor_manager
29
+ self._storage_limit = storage_limit
30
+ self._extraction_limit = extraction_limit
31
+
32
+ @property
33
+ def visitors(self) -> Set[str]:
34
+ """Test purpose only"""
35
+ raise NotImplementedError()
36
+
37
+ def add(self, visitor_code: str) -> None:
38
+ """Adds a single visitor code to the registry."""
39
+ raise NotImplementedError()
40
+
41
+ def add_all(self, visitor_codes: Iterable[str]) -> None:
42
+ """Adds multiple visitor codes to the registry."""
43
+ raise NotImplementedError()
44
+
45
+ def extract(self, limit: int = UNLIMITED) -> List[str]:
46
+ """
47
+ Removes and returns visitor codes. A backlog under the doubled `extraction_limit` is returned at once
48
+ (unless `limit` is smaller); otherwise at most `min(limit, extraction_limit)` codes are returned.
49
+ Codes are extracted in no particular order.
50
+ """
51
+ raise NotImplementedError()
52
+
53
+ def _extraction_count(self, size: int, limit: int) -> int:
54
+ if (size <= limit) and (size < self._extraction_limit * self.LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT):
55
+ return size
56
+ return min(limit, self._extraction_limit)
57
+
58
+ def _storage_limit_after_removal(self) -> int:
59
+ return int(self._storage_limit * self.REMOVAL_FACTOR)
60
+
61
+
62
+ class ConcurrentVisitorTrackingRegistry(VisitorTrackingRegistry):
63
+ """
64
+ Lock-free on the hot path: `add` is a single `dict` operation, atomic under the GIL (CPython, PyPy) and under the
65
+ free-threaded build's per-object locking, so request threads never block on the registry - with the GIL a lock
66
+ there causes a convoy on every interpreter switch (~5x lower throughput, p99 of hundreds of microseconds at
67
+ 8 threads; see `tests/benchmarks/visitor_tracking_registry_benchmark.py`). The dict is never swapped out;
68
+ `extract` removes with `popitem()` (O(1)) under a lock that only serializes removers, so a concurrently added
69
+ code is either returned now or kept for the next extraction, never lost. Nothing here depends on the order in
70
+ which concurrent operations interleave, only on single operations not corrupting or losing entries.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ visitor_manager: VisitorManager,
76
+ storage_limit=VisitorTrackingRegistry.DEFAULT_STORAGE_LIMIT,
77
+ extraction_limit=VisitorTrackingRegistry.DEFAULT_EXTRACTION_LIMIT,
78
+ ) -> None:
79
+ super().__init__(visitor_manager, storage_limit, extraction_limit)
80
+ self._visitors: Dict[str, None] = {}
81
+ self._extract_lock = Lock()
82
+
83
+ @property
84
+ def visitors(self) -> Set[str]:
85
+ return set(self._visitors)
86
+
87
+ def add(self, visitor_code: str) -> None:
88
+ self._visitors[visitor_code] = None
89
+
90
+ def add_all(self, visitor_codes: Iterable[str]) -> None:
91
+ self._visitors.update(dict.fromkeys(visitor_codes))
92
+ if len(self._visitors) > self._storage_limit:
93
+ self.__erase_to_storage_limit()
94
+
95
+ def __erase_to_storage_limit(self) -> None:
96
+ with self._extract_lock:
97
+ # Drain with `popitem()` instead of a `for` loop over the live dict: request threads keep calling `add`
98
+ # meanwhile, and a loop over a dict that gets a new key fails with "dictionary changed size during
99
+ # iteration" - which would abort the eviction. Codes added while draining stay in the live dict.
100
+ drained = [self._visitors.popitem()[0] for _ in range(len(self._visitors))]
101
+ # `peek_visitor`: an eviction pass must not count as activity and extend the visitors' sessions.
102
+ self._visitors.update(
103
+ dict.fromkeys(vc for vc in drained if self._visitor_manager.peek_visitor(vc) is not None)
104
+ )
105
+ visitors_to_remove_count = len(self._visitors) - self._storage_limit_after_removal()
106
+ for _ in range(visitors_to_remove_count):
107
+ self._visitors.popitem()
108
+
109
+ def extract(self, limit: int = VisitorTrackingRegistry.UNLIMITED) -> List[str]:
110
+ with self._extract_lock:
111
+ count = self._extraction_count(len(self._visitors), limit)
112
+ return [self._visitors.popitem()[0] for _ in range(count)]