kameleoon-client-python 3.22.0__py2.py3-none-any.whl → 3.22.2__py2.py3-none-any.whl
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.
- kameleoon/data/manager/visitor_manager.py +11 -13
- kameleoon/helpers/multi_threading.py +34 -14
- kameleoon/helpers/scheduler.py +17 -3
- kameleoon/helpers/string_utils.py +2 -3
- kameleoon/managers/tracking/tracking_manager.py +60 -17
- kameleoon/managers/tracking/visitor_tracking_registry.py +83 -60
- kameleoon/network/access_token_source.py +96 -59
- kameleoon/network/net_provider.py +2 -1
- kameleoon/network/services/service_impl.py +3 -1
- kameleoon/sdk_version.py +1 -1
- kameleoon/targeting/conditions/browser_condition.py +6 -1
- kameleoon/targeting/conditions/constants.py +13 -0
- kameleoon/targeting/conditions/version_condition.py +6 -5
- {kameleoon_client_python-3.22.0.dist-info → kameleoon_client_python-3.22.2.dist-info}/METADATA +16 -16
- {kameleoon_client_python-3.22.0.dist-info → kameleoon_client_python-3.22.2.dist-info}/RECORD +17 -22
- kameleoon/storage/__init__.py +0 -0
- kameleoon/storage/cache.py +0 -67
- kameleoon/storage/cache_factory.py +0 -17
- kameleoon/storage/cache_factory_impl.py +0 -12
- kameleoon/storage/cache_impl.py +0 -36
- {kameleoon_client_python-3.22.0.dist-info → kameleoon_client_python-3.22.2.dist-info}/WHEEL +0 -0
- {kameleoon_client_python-3.22.0.dist-info → kameleoon_client_python-3.22.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
|
kameleoon/helpers/scheduler.py
CHANGED
|
@@ -42,10 +42,14 @@ class Scheduler:
|
|
|
42
42
|
|
|
43
43
|
def stop(self) -> None:
|
|
44
44
|
"""
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
-
) ->
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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")
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Tracking"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
2
4
|
from threading import Lock
|
|
3
|
-
from typing import Iterable, List, Set
|
|
5
|
+
from typing import Dict, Iterable, List, Set
|
|
4
6
|
from kameleoon.data.manager.visitor_manager import VisitorManager
|
|
5
7
|
|
|
6
8
|
|
|
@@ -10,6 +12,28 @@ class VisitorTrackingRegistry:
|
|
|
10
12
|
|
|
11
13
|
Must be thread-safe
|
|
12
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
|
+
|
|
13
37
|
def add(self, visitor_code: str) -> None:
|
|
14
38
|
"""Adds a single visitor code to the registry."""
|
|
15
39
|
raise NotImplementedError()
|
|
@@ -18,72 +42,71 @@ class VisitorTrackingRegistry:
|
|
|
18
42
|
"""Adds multiple visitor codes to the registry."""
|
|
19
43
|
raise NotImplementedError()
|
|
20
44
|
|
|
21
|
-
def extract(self) ->
|
|
22
|
-
"""
|
|
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
|
+
"""
|
|
23
51
|
raise NotImplementedError()
|
|
24
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)
|
|
25
57
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
LIMITED_EXTRACTION_THRESHOLD_COEFFICIENT = 2
|
|
29
|
-
REMOVAL_FACTOR = 0.8
|
|
58
|
+
def _storage_limit_after_removal(self) -> int:
|
|
59
|
+
return int(self._storage_limit * self.REMOVAL_FACTOR)
|
|
30
60
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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)
|
|
37
86
|
|
|
38
87
|
def add(self, visitor_code: str) -> None:
|
|
39
|
-
|
|
40
|
-
with self._lock:
|
|
41
|
-
self._visitors.add(visitor_code)
|
|
88
|
+
self._visitors[visitor_code] = None
|
|
42
89
|
|
|
43
90
|
def add_all(self, visitor_codes: Iterable[str]) -> None:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
self.
|
|
47
|
-
if len(self._visitors) > self._storage_limit:
|
|
48
|
-
self.__erase_nonexistent_visitors()
|
|
49
|
-
self.__erase_to_storage_limit()
|
|
50
|
-
|
|
51
|
-
def __erase_nonexistent_visitors(self) -> None:
|
|
52
|
-
"""Not thread-safe"""
|
|
53
|
-
visitors_to_remove = [vc for vc in self._visitors if self._visitor_manager.get_visitor(vc) is None]
|
|
54
|
-
self._visitors.difference_update(visitors_to_remove)
|
|
91
|
+
self._visitors.update(dict.fromkeys(visitor_codes))
|
|
92
|
+
if len(self._visitors) > self._storage_limit:
|
|
93
|
+
self.__erase_to_storage_limit()
|
|
55
94
|
|
|
56
95
|
def __erase_to_storage_limit(self) -> None:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
with self._lock:
|
|
75
|
-
old_visitors = self._visitors
|
|
76
|
-
self._visitors = new_visitors
|
|
77
|
-
return old_visitors
|
|
78
|
-
|
|
79
|
-
def __extract_limited(self) -> Iterable[str]:
|
|
80
|
-
with self._lock:
|
|
81
|
-
if self.__should_extract_all_be_used():
|
|
82
|
-
return self.__extract_all()
|
|
83
|
-
extracted: List[str] = [""] * self._extraction_limit
|
|
84
|
-
for i, visitor_code in enumerate(self._visitors):
|
|
85
|
-
if i >= self._extraction_limit:
|
|
86
|
-
break
|
|
87
|
-
extracted[i] = visitor_code
|
|
88
|
-
self._visitors.difference_update(extracted)
|
|
89
|
-
return extracted
|
|
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)]
|
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
"""Network"""
|
|
2
|
+
|
|
3
|
+
# Postponed evaluation: `concurrent.futures.Future[...]` is not subscriptable at runtime before Python 3.9.
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
2
6
|
import asyncio
|
|
3
7
|
import base64
|
|
8
|
+
import concurrent.futures
|
|
9
|
+
import threading
|
|
4
10
|
import time
|
|
5
|
-
from typing import Optional
|
|
11
|
+
from typing import Optional, Tuple
|
|
6
12
|
|
|
7
13
|
from kameleoon.helpers.string_utils import StringUtils
|
|
8
14
|
from kameleoon.logging.kameleoon_logger import KameleoonLogger
|
|
15
|
+
from kameleoon.network.net_provider import Response
|
|
9
16
|
from kameleoon.network.services.automation_service import AutomationService
|
|
10
17
|
|
|
11
18
|
|
|
12
19
|
class AccessTokenSource:
|
|
13
20
|
"""Access Token Source"""
|
|
21
|
+
|
|
22
|
+
_SILENCE_PERIOD = 300.0 # in seconds
|
|
14
23
|
_TOKEN_EXPIRATION_GAP = 60.0 # in seconds
|
|
15
24
|
_TOKEN_OBSOLESCENCE_GAP = 1800.0 # in seconds
|
|
16
25
|
_JWT_ACCESS_TOKEN_FIELD = "access_token"
|
|
@@ -19,17 +28,22 @@ class AccessTokenSource:
|
|
|
19
28
|
def __init__(self, network_manager, client_id: str, client_secret: str) -> None:
|
|
20
29
|
KameleoonLogger.debug(
|
|
21
30
|
lambda: f"CALL: AccessTokenSource(network_manager, client_id: '{StringUtils.secret(client_id)}', "
|
|
22
|
-
|
|
31
|
+
f"client_secret: '{StringUtils.secret(client_secret)}')"
|
|
23
32
|
)
|
|
24
33
|
self._network_manager = network_manager
|
|
25
34
|
self._client_id = client_id
|
|
26
35
|
self._client_secret = client_secret
|
|
27
|
-
self._cached_token: Optional[AccessTokenSource.ExpiringToken] = None
|
|
28
|
-
self._fetching = False
|
|
29
36
|
self._basic_authorization_token = AccessTokenSource.construct_basic_token(client_id, client_secret)
|
|
37
|
+
# Written by the fetching coroutine, read by callers on any thread / event loop.
|
|
38
|
+
self._cached_token: Optional[AccessTokenSource.ExpiringToken] = None
|
|
39
|
+
self._silent_after_fetch_failure_until = 0.0 # 0 = never silenced
|
|
40
|
+
# In-flight fetch shared by all concurrent callers; `None` when idle. A `concurrent.futures.Future` (not an
|
|
41
|
+
# asyncio one) because callers may run on different event loops. Guarded by `_lock`.
|
|
42
|
+
self._fetching: Optional[concurrent.futures.Future[Optional[str]]] = None
|
|
43
|
+
self._lock = threading.Lock()
|
|
30
44
|
KameleoonLogger.debug(
|
|
31
45
|
lambda: f"RETURN: AccessTokenSource(network_manager, client_id: '{StringUtils.secret(client_id)}', "
|
|
32
|
-
|
|
46
|
+
f"client_secret: '{StringUtils.secret(client_secret)}')"
|
|
33
47
|
)
|
|
34
48
|
|
|
35
49
|
@staticmethod
|
|
@@ -39,73 +53,97 @@ class AccessTokenSource:
|
|
|
39
53
|
return "Basic " + base64.b64encode(basic_token_content.encode("utf-8")).decode("utf-8")
|
|
40
54
|
|
|
41
55
|
async def get_token(self, timeout: Optional[float] = None) -> Optional[str]:
|
|
42
|
-
"""
|
|
56
|
+
"""
|
|
57
|
+
Returns the access token. Fetches a new token if the cached one is expired or obsolete.
|
|
58
|
+
|
|
59
|
+
Returns `None` without a request during the silence period which follows a fetch failure.
|
|
60
|
+
"""
|
|
43
61
|
KameleoonLogger.debug("CALL: AccessTokenSource.get_token(timeout: %s)", timeout)
|
|
44
62
|
now = time.time()
|
|
45
63
|
token = self._cached_token
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
64
|
+
silent = now < self._silent_after_fetch_failure_until
|
|
65
|
+
if (token is not None) and not token.is_expired(now):
|
|
66
|
+
if token.is_obsolete(now) and not silent:
|
|
67
|
+
self._fetch_token(timeout) # refresh in background, keep serving the current token
|
|
68
|
+
result = token.value
|
|
69
|
+
elif silent:
|
|
70
|
+
result = None
|
|
71
|
+
else:
|
|
72
|
+
result = await asyncio.wrap_future(self._fetch_token(timeout))
|
|
73
|
+
KameleoonLogger.debug(lambda: f"RETURN: AccessTokenSource.get_token(timeout: {timeout}) -> "
|
|
74
|
+
f"(token: '{StringUtils.secret(result)}')")
|
|
75
|
+
return result
|
|
54
76
|
|
|
55
77
|
def discard_token(self, token: str) -> None:
|
|
56
78
|
"""Discards the specified token, if it matches the cached token."""
|
|
57
|
-
KameleoonLogger.debug("CALL: AccessTokenSource.discard_token(token:
|
|
79
|
+
KameleoonLogger.debug(lambda: f"CALL: AccessTokenSource.discard_token(token: '{StringUtils.secret(token)}')")
|
|
58
80
|
cached_token = self._cached_token
|
|
59
81
|
if cached_token and (cached_token.value == token):
|
|
60
82
|
self._cached_token = None
|
|
61
|
-
KameleoonLogger.debug("RETURN: AccessTokenSource.discard_token(token:
|
|
83
|
+
KameleoonLogger.debug(lambda: f"RETURN: AccessTokenSource.discard_token(token: '{StringUtils.secret(token)}')")
|
|
62
84
|
|
|
63
|
-
|
|
85
|
+
def _fetch_token(self, timeout: Optional[float]) -> concurrent.futures.Future[Optional[str]]:
|
|
86
|
+
"""Starts a fetch unless one is already in flight; returns the future shared by all waiters."""
|
|
87
|
+
with self._lock:
|
|
88
|
+
if self._fetching is not None:
|
|
89
|
+
return self._fetching
|
|
90
|
+
future: concurrent.futures.Future[Optional[str]] = concurrent.futures.Future()
|
|
91
|
+
# A waiter that is cancelled (`asyncio.wrap_future` forwards cancellation) must not cancel the shared
|
|
92
|
+
# fetch: a running future ignores `cancel()`, so the other waiters and `set_result()` are unaffected.
|
|
93
|
+
future.set_running_or_notify_cancel()
|
|
94
|
+
self._fetching = future
|
|
64
95
|
try:
|
|
65
|
-
self.
|
|
66
|
-
return await self._fetch_token(timeout)
|
|
96
|
+
asyncio.get_running_loop().create_task(self._perform_fetch(future, timeout))
|
|
67
97
|
except Exception as exception: # pylint: disable=W0703
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
return None
|
|
98
|
+
self._on_fetch_done(future, None, exception)
|
|
99
|
+
return future
|
|
71
100
|
|
|
72
|
-
def
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
except Exception as exception: # pylint: disable=W0703
|
|
77
|
-
KameleoonLogger.error("Failed to run access token fetching: %s", exception)
|
|
78
|
-
self._fetching = False
|
|
79
|
-
|
|
80
|
-
async def _fetch_token(self, timeout: Optional[float] = None) -> Optional[str]:
|
|
81
|
-
KameleoonLogger.debug("CALL: AccessTokenSource._fetch_token(timeout: %s)", timeout)
|
|
101
|
+
async def _perform_fetch(self, future: concurrent.futures.Future[Optional[str]], timeout: Optional[float]) -> None:
|
|
102
|
+
KameleoonLogger.debug("CALL: AccessTokenSource._perform_fetch(timeout: %s)", timeout)
|
|
103
|
+
token: Optional[str] = None
|
|
104
|
+
error: Optional[Exception] = None
|
|
82
105
|
try:
|
|
83
106
|
service: AutomationService = self._network_manager.get_service(AutomationService)
|
|
84
107
|
response = await service.fetch_access_jwtoken(self._basic_authorization_token, timeout)
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return None
|
|
91
|
-
try:
|
|
92
|
-
jwt = response.content
|
|
93
|
-
token = jwt[self._JWT_ACCESS_TOKEN_FIELD]
|
|
94
|
-
expires_in = jwt[self._JWT_EXPIRES_IN_FIELD]
|
|
95
|
-
except Exception as exception: # pylint: disable=W0703
|
|
96
|
-
KameleoonLogger.error("Failed to parse access JWT: %s", exception)
|
|
97
|
-
return None
|
|
98
|
-
if not (isinstance(token, str) and token and isinstance(expires_in, int) and expires_in):
|
|
99
|
-
KameleoonLogger.error("Failed to read access JWT")
|
|
100
|
-
return None
|
|
101
|
-
self._handle_fetched_token(token, expires_in)
|
|
102
|
-
KameleoonLogger.debug("RETURN: AccessTokenSource._fetch_token(timeout: %s) -> (token: %s)",
|
|
103
|
-
timeout, token)
|
|
104
|
-
return token
|
|
108
|
+
token, expires_in = self._parse_response(response)
|
|
109
|
+
self._cached_token = self._new_expiring_token(token, expires_in)
|
|
110
|
+
KameleoonLogger.info("Fetched access token")
|
|
111
|
+
except Exception as exception: # pylint: disable=W0703
|
|
112
|
+
error = exception
|
|
105
113
|
finally:
|
|
106
|
-
|
|
114
|
+
# Also runs on cancellation (e.g. the event loop shutting down), so waiters are never left pending.
|
|
115
|
+
self._on_fetch_done(future, token, error)
|
|
116
|
+
KameleoonLogger.debug(lambda: f"RETURN: AccessTokenSource._perform_fetch(timeout: {timeout}) -> "
|
|
117
|
+
f"(token: '{StringUtils.secret(token)}')")
|
|
118
|
+
|
|
119
|
+
def _on_fetch_done(
|
|
120
|
+
self, future: concurrent.futures.Future[Optional[str]], token: Optional[str], error: Optional[Exception]
|
|
121
|
+
) -> None:
|
|
122
|
+
if error is not None:
|
|
123
|
+
self._silent_after_fetch_failure_until = time.time() + self._SILENCE_PERIOD
|
|
124
|
+
KameleoonLogger.warning(
|
|
125
|
+
"Failed to fetch access token (%s); it will not be requested for %ds", error, int(self._SILENCE_PERIOD)
|
|
126
|
+
)
|
|
127
|
+
# Clear before completing so waiters that re-enter get_token() can start a new fetch.
|
|
128
|
+
with self._lock:
|
|
129
|
+
self._fetching = None
|
|
130
|
+
future.set_result(token)
|
|
107
131
|
|
|
108
|
-
def
|
|
132
|
+
def _parse_response(self, response: Response) -> Tuple[str, int]:
|
|
133
|
+
if not response.success:
|
|
134
|
+
raise ValueError(f"access token request failed: {response}")
|
|
135
|
+
jwt = response.content
|
|
136
|
+
if not isinstance(jwt, dict):
|
|
137
|
+
raise ValueError(f"access token response is not a JSON object: {response}")
|
|
138
|
+
expires_in = jwt.get(self._JWT_EXPIRES_IN_FIELD)
|
|
139
|
+
if not (isinstance(expires_in, int) and (expires_in > 0)):
|
|
140
|
+
raise ValueError(f"access token response has no valid '{self._JWT_EXPIRES_IN_FIELD}' field")
|
|
141
|
+
token = jwt.get(self._JWT_ACCESS_TOKEN_FIELD)
|
|
142
|
+
if not (isinstance(token, str) and token):
|
|
143
|
+
raise ValueError(f"access token response has no '{self._JWT_ACCESS_TOKEN_FIELD}' field")
|
|
144
|
+
return token, expires_in
|
|
145
|
+
|
|
146
|
+
def _new_expiring_token(self, token: str, expires_in: int) -> AccessTokenSource.ExpiringToken:
|
|
109
147
|
now = time.time()
|
|
110
148
|
exp_time = now + expires_in - self._TOKEN_EXPIRATION_GAP
|
|
111
149
|
if expires_in > self._TOKEN_OBSOLESCENCE_GAP:
|
|
@@ -113,17 +151,16 @@ class AccessTokenSource:
|
|
|
113
151
|
else:
|
|
114
152
|
obs_time = exp_time
|
|
115
153
|
if expires_in <= self._TOKEN_EXPIRATION_GAP:
|
|
116
|
-
KameleoonLogger.error(
|
|
117
|
-
"Access token life time (%ss) is not long enough to cache the token", expires_in
|
|
118
|
-
)
|
|
154
|
+
KameleoonLogger.error("Access token life time (%ss) is not long enough to cache the token", expires_in)
|
|
119
155
|
else:
|
|
120
156
|
KameleoonLogger.warning(
|
|
121
|
-
|
|
157
|
+
"Access token life time (%ss) is not long enough to refresh cached token in background", expires_in
|
|
122
158
|
)
|
|
123
|
-
|
|
159
|
+
return self.ExpiringToken(token, exp_time, obs_time)
|
|
124
160
|
|
|
125
161
|
class ExpiringToken:
|
|
126
162
|
"""Expiring Token"""
|
|
163
|
+
|
|
127
164
|
def __init__(self, value: str, expiration_time: float, obsolescence_time: float) -> None:
|
|
128
165
|
self.value = value
|
|
129
166
|
self.expiration_time = expiration_time
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Network"""
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Any, Dict, Optional
|
|
4
|
+
from kameleoon.helpers.string_utils import StringUtils
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class ResponseContentType(Enum):
|
|
@@ -67,7 +68,7 @@ class Request:
|
|
|
67
68
|
body = self.body
|
|
68
69
|
|
|
69
70
|
headers = [
|
|
70
|
-
f"{name}:{
|
|
71
|
+
f"{name}:{StringUtils.secret(value) if name == 'Authorization' else value}"
|
|
71
72
|
for name, value in (self.headers or {}).items()
|
|
72
73
|
]
|
|
73
74
|
|
|
@@ -6,6 +6,7 @@ from typing import Any, Coroutine
|
|
|
6
6
|
|
|
7
7
|
from kameleoon.events.http_request_failure import HttpRequestFailure
|
|
8
8
|
from kameleoon.events.request_type import RequestType
|
|
9
|
+
from kameleoon.helpers.string_utils import StringUtils
|
|
9
10
|
from kameleoon.logging.kameleoon_logger import KameleoonLogger
|
|
10
11
|
from kameleoon.logging.log_level import LogLevel
|
|
11
12
|
from kameleoon.network.network_manager import NetworkManager
|
|
@@ -70,7 +71,8 @@ class ServiceImpl(Service):
|
|
|
70
71
|
response.content,
|
|
71
72
|
)
|
|
72
73
|
if request.access_token and (response.code == 401):
|
|
73
|
-
KameleoonLogger.warning("Unexpected rejection of access token %s",
|
|
74
|
+
KameleoonLogger.warning("Unexpected rejection of access token '%s'",
|
|
75
|
+
StringUtils.secret(request.access_token))
|
|
74
76
|
access_token_source.discard_token(request.access_token)
|
|
75
77
|
if attempt == retry_limit:
|
|
76
78
|
try_access_token_auth = False
|
kameleoon/sdk_version.py
CHANGED
|
@@ -23,7 +23,12 @@ class BrowserCondition(TargetingCondition):
|
|
|
23
23
|
else BrowserType.INTERNET_EXPLORER
|
|
24
24
|
)
|
|
25
25
|
self.__version = json_condition.get("version", None)
|
|
26
|
-
|
|
26
|
+
# Version is optional; the operator is only meaningful when it's set
|
|
27
|
+
self.__operator = (
|
|
28
|
+
TargetingOperator.from_string(json_condition.get("versionMatchType", None))
|
|
29
|
+
if self.__version is not None
|
|
30
|
+
else TargetingOperator.UNKNOWN
|
|
31
|
+
)
|
|
27
32
|
except KeyError as exeception:
|
|
28
33
|
KameleoonLogger.error("%s has wrong JSON structure: %s", self.__class__, exeception)
|
|
29
34
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
""" Constants """
|
|
2
2
|
|
|
3
3
|
from enum import Enum
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from kameleoon.logging.kameleoon_logger import KameleoonLogger
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
class TargetingOperator(Enum):
|
|
@@ -17,3 +20,13 @@ class TargetingOperator(Enum):
|
|
|
17
20
|
FALSE = "FALSE"
|
|
18
21
|
AMONG_VALUES = "AMONG_VALUES"
|
|
19
22
|
ANY = "ANY"
|
|
23
|
+
UNKNOWN = "UNKNOWN"
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_string(cls, raw: Any) -> "TargetingOperator":
|
|
27
|
+
"""Parse an operator name from the configuration; unparsable values map to UNKNOWN."""
|
|
28
|
+
try:
|
|
29
|
+
return cls[str(raw).upper()]
|
|
30
|
+
except KeyError:
|
|
31
|
+
KameleoonLogger.error("Unexpected TargetingOperator: %s", raw)
|
|
32
|
+
return cls.UNKNOWN
|
|
@@ -19,11 +19,12 @@ class VersionCondition(TargetingCondition):
|
|
|
19
19
|
def __init__(self, json_condition: Dict[str, Union[str, int, Any]]):
|
|
20
20
|
super().__init__(json_condition)
|
|
21
21
|
self._condition_version = json_condition.get("version", None)
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
# Version may be optional (e.g. SDK_LANGUAGE); the operator is only meaningful when it's set
|
|
23
|
+
self.__operator = (
|
|
24
|
+
TargetingOperator.from_string(json_condition.get("versionMatchType", None))
|
|
25
|
+
if self._condition_version is not None
|
|
26
|
+
else TargetingOperator.UNKNOWN
|
|
27
|
+
)
|
|
27
28
|
self.__cached_sem_version: Optional[SemVersion] = None
|
|
28
29
|
|
|
29
30
|
def check(self, data: Any) -> bool:
|
{kameleoon_client_python-3.22.0.dist-info → kameleoon_client_python-3.22.2.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kameleoon-client-python
|
|
3
|
-
Version: 3.22.
|
|
3
|
+
Version: 3.22.2
|
|
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"
|
|
25
23
|
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"
|
|
29
|
-
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
24
|
Requires-Dist: pytest-cov==2.12.1; extra == "test"
|
|
33
|
-
Requires-Dist:
|
|
25
|
+
Requires-Dist: pytest-django==4.5.2; extra == "test"
|
|
34
26
|
Requires-Dist: pylint==2.12.*; extra == "test"
|
|
35
|
-
Requires-Dist: Werkzeug==2.0.1; extra == "test"
|
|
36
|
-
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
|
-
Requires-Dist: requests<3.0.0,>=2.25; extra == "test"
|
|
41
27
|
Requires-Dist: wheel==0.36.2; extra == "test"
|
|
28
|
+
Requires-Dist: types-PyYAML==5.4.3; extra == "test"
|
|
29
|
+
Requires-Dist: types-requests==2.25.0; extra == "test"
|
|
42
30
|
Requires-Dist: types-python-dateutil==2.8.2; extra == "test"
|
|
31
|
+
Requires-Dist: aiohttp<4.0.0,>=3.8.0; extra == "test"
|
|
32
|
+
Requires-Dist: Django==4.2.*; extra == "test"
|
|
43
33
|
Requires-Dist: aioresponses==0.7.*; extra == "test"
|
|
34
|
+
Requires-Dist: freezegun==1.1.0; extra == "test"
|
|
35
|
+
Requires-Dist: pytest-asyncio==0.21.*; extra == "test"
|
|
36
|
+
Requires-Dist: Werkzeug==2.0.1; extra == "test"
|
|
37
|
+
Requires-Dist: mypy==0.991; extra == "test"
|
|
38
|
+
Requires-Dist: twine==3.4.1; extra == "test"
|
|
39
|
+
Requires-Dist: pyyaml<6.1.0,>=5.4.0; extra == "test"
|
|
40
|
+
Requires-Dist: flake8==4.0.1; extra == "test"
|
|
41
|
+
Requires-Dist: pytest-pythonpath==0.7.3; extra == "test"
|
|
42
|
+
Requires-Dist: requests<3.0.0,>=2.25; extra == "test"
|
|
43
|
+
Requires-Dist: pytest-ordering==0.6; extra == "test"
|
|
44
44
|
Dynamic: author
|
|
45
45
|
Dynamic: author-email
|
|
46
46
|
Dynamic: classifier
|
{kameleoon_client_python-3.22.0.dist-info → kameleoon_client_python-3.22.2.dist-info}/RECORD
RENAMED
|
@@ -5,7 +5,7 @@ kameleoon/kameleoon_client.py,sha256=Gg1NTcI0JUmYKPFYDRhJSG_Z2fxKaO8ECZ7R0yRgffA
|
|
|
5
5
|
kameleoon/kameleoon_client_config.py,sha256=DJm--TtaTPc_09q3uv8XjHrX_q7C-sh0tf65lL2TxCw,11181
|
|
6
6
|
kameleoon/kameleoon_client_factory.py,sha256=9_VXYnfZ0A5GNY6Y3j9aCfUV2uc-_-SQhslU3uUoeSQ,3228
|
|
7
7
|
kameleoon/middleware.py,sha256=gwbt2YK0D6Uvmm3M7WqkTdwUIZEA85Use39Tt3oVeus,1584
|
|
8
|
-
kameleoon/sdk_version.py,sha256=
|
|
8
|
+
kameleoon/sdk_version.py,sha256=7ZsKymIDnKmWCSJ-FIVDMmzAxUjdAcZfrkndtWxSG08,289
|
|
9
9
|
kameleoon/client_readiness/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
kameleoon/client_readiness/async_readiness.py,sha256=NTzreGNJ9jOdpia0gAnkOwjn14Vu6m53l7QC_eJQ5ig,1569
|
|
11
11
|
kameleoon/client_readiness/threading_readiness.py,sha256=bpld4cxl4H6ZIvJxomU_Qmhii2ru7fRK1yzYp2SQhTw,5794
|
|
@@ -49,7 +49,7 @@ kameleoon/data/manager/forced_variation.py,sha256=k4_86h5O4_S-bqzfKIS3iU4xRCMXTl
|
|
|
49
49
|
kameleoon/data/manager/legal_consent.py,sha256=5ymyDPywLetYa8e7nrgYp6Ds8j9qrqRpuCSg995gPNc,160
|
|
50
50
|
kameleoon/data/manager/page_view_visit.py,sha256=ghn7zPSFtDyGpkwT4zEFKiMsJpXhicgmJsTSOnzcank,1730
|
|
51
51
|
kameleoon/data/manager/visitor.py,sha256=YBXbQ7kMYEj38fikOuxfiXFde6nDowUmJjnW2e4aE2Q,24786
|
|
52
|
-
kameleoon/data/manager/visitor_manager.py,sha256=
|
|
52
|
+
kameleoon/data/manager/visitor_manager.py,sha256=dbXYE4mCJx0bU-ZErOXdL6UXdjGWGL_q6DJyIFH7Q70,9207
|
|
53
53
|
kameleoon/data/manager/visitor_slot.py,sha256=zf3Ov38CQrR5lS6oJ4Ehbxma2heBGNtUvRohEj1k7sY,528
|
|
54
54
|
kameleoon/events/__init__.py,sha256=LGBoX1JDZr6dPpsqlUmYE5jN4i9L_GdQif7bkQOL3Hs,751
|
|
55
55
|
kameleoon/events/data_file_update_event.py,sha256=caM6BSpMhMU1itXAIsAnVLsUbH-IDcL5HJy0UtF2yWI,1141
|
|
@@ -65,12 +65,12 @@ kameleoon/helpers/domain.py,sha256=muSkEMF-jbIzrHvwJQNB9XHZGQ18tluc1kNRGgjC0bU,2
|
|
|
65
65
|
kameleoon/helpers/functions.py,sha256=JEjReWV9u9muD6aGZwiwUjBMl8UPHn5CfiGmN8zxkfc,1401
|
|
66
66
|
kameleoon/helpers/hasher.py,sha256=CR0qaaD1y_AXGyCxiJZuuNOT8VjoXsoQhoXJMaNdgAM,945
|
|
67
67
|
kameleoon/helpers/logger.py,sha256=BHugg5UDUlKiPVMIU1x4X4oex1DK_9XUcaf8H7wSp2Y,1100
|
|
68
|
-
kameleoon/helpers/multi_threading.py,sha256=
|
|
68
|
+
kameleoon/helpers/multi_threading.py,sha256=Em1DeYRH5Vx7GUSv5m6sdAxc43za3WSijUahROS1iHY,5491
|
|
69
69
|
kameleoon/helpers/nonce.py,sha256=QmdCkHvzcdwY2yC7XsHzVLjUKxKdSyvgG1fi8Xz8Xh8,254
|
|
70
70
|
kameleoon/helpers/repeat_timer.py,sha256=S8nC4jbK5z6C8on-FJ_CN8kZynBN22PsPcgaO8gOMwc,412
|
|
71
|
-
kameleoon/helpers/scheduler.py,sha256=
|
|
71
|
+
kameleoon/helpers/scheduler.py,sha256=8VFnX8WpWItcuO1fDBZKV150wcR0jm5UM0nwqzcaKbw,4149
|
|
72
72
|
kameleoon/helpers/sem_version.py,sha256=BGqUkPo5jx0nTgr7m_paIl0l8xCgi_zFsiS1D4gzNDo,1976
|
|
73
|
-
kameleoon/helpers/string_utils.py,sha256=
|
|
73
|
+
kameleoon/helpers/string_utils.py,sha256=g2ofE2oF_04JsbG43xDS3clrVuxZIPq1cMKqeSqRthM,1855
|
|
74
74
|
kameleoon/helpers/time.py,sha256=E7279NV9pDuYqVesIIrEhixMviKEoNhDLO9XQXNaWdk,486
|
|
75
75
|
kameleoon/helpers/visitor_code.py,sha256=boya6fmyixNaOwdvxDzt91C7-XN8T9T_0mm3C5JQGIA,745
|
|
76
76
|
kameleoon/hybrid/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -92,15 +92,15 @@ kameleoon/managers/remote_data/remote_data_manager.py,sha256=UOAmAo8U9-H_RDm8czz
|
|
|
92
92
|
kameleoon/managers/remote_data/remote_visitor_data.py,sha256=8uZnwRt2zvhHhTA54-Y8IUvAve7sx7TGAf4x9mYX7fQ,13884
|
|
93
93
|
kameleoon/managers/tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
94
94
|
kameleoon/managers/tracking/tracking_builder.py,sha256=SjKDbDBpRrCdafFb1o-o8lyqaFd_URbwDTi8toHBKBU,7540
|
|
95
|
-
kameleoon/managers/tracking/tracking_manager.py,sha256=
|
|
96
|
-
kameleoon/managers/tracking/visitor_tracking_registry.py,sha256=
|
|
95
|
+
kameleoon/managers/tracking/tracking_manager.py,sha256=nOzqNHXpmEhTLOXRLIfwOSJHIdbTic0_-RLDXy1tEPY,7154
|
|
96
|
+
kameleoon/managers/tracking/visitor_tracking_registry.py,sha256=zTyOQtj3mFk1GUl9aSoT2Hv1EPcGrvGcWprKBg3y7fg,4834
|
|
97
97
|
kameleoon/managers/warehouse/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
98
98
|
kameleoon/managers/warehouse/warehouse_manager.py,sha256=9HFzc_B2EGWxipP7XIj9-1-P3cA-ksI7lBUB4PGP_wg,3351
|
|
99
99
|
kameleoon/network/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
100
|
-
kameleoon/network/access_token_source.py,sha256=
|
|
100
|
+
kameleoon/network/access_token_source.py,sha256=8ZrGA9TTBolti-_sCVTpQTHch9sC0PMVdHt-rszYvwU,9040
|
|
101
101
|
kameleoon/network/access_token_source_factory.py,sha256=7k07mpFjkeTUIsOmchsIHd6-o9hzo4yvl99hmHPoQuc,605
|
|
102
102
|
kameleoon/network/activity_event.py,sha256=acutjP-03lOQlJF_olCrQwNLuR3JaIZnA0-MAt5EiVE,425
|
|
103
|
-
kameleoon/network/net_provider.py,sha256=
|
|
103
|
+
kameleoon/network/net_provider.py,sha256=skL7Q4_mNeRrBQ016zCSxtW5cHJaLvMrdrNm1pU80hU,2573
|
|
104
104
|
kameleoon/network/net_provider_impl.py,sha256=nP1Lz3LgxPBqQuML9BtaY7ZzXwSdsQ1zm5tLwJHFaT0,3053
|
|
105
105
|
kameleoon/network/network_manager.py,sha256=OoE0PaKIDmsJbZV0F9KKroPHOWJGIiKx2ejkrHB5z6Y,1888
|
|
106
106
|
kameleoon/network/network_manager_factory.py,sha256=SAaiNF_Fw6xoyOCZ3y9Wu3BLZwW74JsBmkTSIJhtilw,850
|
|
@@ -123,23 +123,18 @@ kameleoon/network/services/configuration_service_impl.py,sha256=-xFKFtkhd0Hk_nOn
|
|
|
123
123
|
kameleoon/network/services/data_service.py,sha256=wWrb98JmlNCILvmG3JZPqdc8Sp5aUsMrDWaABvA0iKk,1098
|
|
124
124
|
kameleoon/network/services/data_service_impl.py,sha256=1BegpK7towoXCy5eqztFfG0XeNrtgjoSiQz219saLDA,3859
|
|
125
125
|
kameleoon/network/services/service.py,sha256=G2paqnw_1XjCC_eCbuOq9MWe0RqScIQMbnJUJj3ehtk,192
|
|
126
|
-
kameleoon/network/services/service_impl.py,sha256=
|
|
126
|
+
kameleoon/network/services/service_impl.py,sha256=7DIpo9_BX_s6usZqVDlT_Ao3FBdzVQgmQ9Gk940Ue-U,5465
|
|
127
127
|
kameleoon/real_time/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
128
128
|
kameleoon/real_time/real_time_event.py,sha256=84d1wcd34ZbctjWFa1EIt8LdDSNt6c49xZA5NKQn-IA,548
|
|
129
129
|
kameleoon/real_time/real_time_event_service.py,sha256=NOyQqs9704afYIIgLpsFcue1riFc6cbrTOOFIbOmbmQ,3060
|
|
130
|
-
kameleoon/storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
131
|
-
kameleoon/storage/cache.py,sha256=a9p7DlmFPFVBM_RCYOEqomf5ieyd9wcPZcfiPFplJ5U,1902
|
|
132
|
-
kameleoon/storage/cache_factory.py,sha256=joERiQmfKc0fSmgQsiTEH8SbJ20TekeG79UZnW45yW4,571
|
|
133
|
-
kameleoon/storage/cache_factory_impl.py,sha256=HU-7Y8OeUD0078KasR3Qru2HeP4gkRtCHtwMVzgB_d8,356
|
|
134
|
-
kameleoon/storage/cache_impl.py,sha256=8aGhq2Q2InKvY0UMpReVlt0tQZiwAA1HRxc6xmLGb9I,1058
|
|
135
130
|
kameleoon/targeting/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
136
131
|
kameleoon/targeting/models.py,sha256=21QikRumqRRlimD_qGnr9xzuFyLcCii3b398F7wf10Q,1018
|
|
137
132
|
kameleoon/targeting/targeting_manager.py,sha256=IH1LcAptfCB4KhcWrHWROzE1aXCG5eElcgt_UGf96qI,7722
|
|
138
133
|
kameleoon/targeting/tree_builder.py,sha256=Np7b4aIUN_Sgt87hWTXRNkMImCLCbzxe36dCmk6tpTc,5622
|
|
139
134
|
kameleoon/targeting/tree_condition_factory.py,sha256=FbAqnJ4Y1wQ-pxH4tBLbVOd2hbU-9LMplUvWM7vCX78,6366
|
|
140
135
|
kameleoon/targeting/conditions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
141
|
-
kameleoon/targeting/conditions/browser_condition.py,sha256=
|
|
142
|
-
kameleoon/targeting/conditions/constants.py,sha256=
|
|
136
|
+
kameleoon/targeting/conditions/browser_condition.py,sha256=_zgC6KrK5gFY4aQHcFkOdlNPK8TclNJwE-94_9hfISA,2818
|
|
137
|
+
kameleoon/targeting/conditions/constants.py,sha256=_m6M_HjKHFna-LbH1M3dWFaRUzzRN6h73Whl-VQJk04,857
|
|
143
138
|
kameleoon/targeting/conditions/conversion_condition.py,sha256=l4lju0HlkVD82neDZehGhCaBqr7uaNOzq3S51MiYoNI,1707
|
|
144
139
|
kameleoon/targeting/conditions/cookie_condition.py,sha256=iZsQKC6L1RD9jnQMOmEfEohPR9Ek5s4ON0SJCmpb7Cg,2714
|
|
145
140
|
kameleoon/targeting/conditions/custom_datum.py,sha256=H8SDA6sXAANFKmToasPzxibGRv8vB2Z8dRPjLCZCRWo,4020
|
|
@@ -162,7 +157,7 @@ kameleoon/targeting/conditions/target_personalization_condition.py,sha256=42fm_F
|
|
|
162
157
|
kameleoon/targeting/conditions/targeting_condition.py,sha256=QaGrJAaoGLQKQagHOUmdjGV6cDySl9O7KI_IdjOyQnU,1986
|
|
163
158
|
kameleoon/targeting/conditions/time_elapsed_since_visit_condition.py,sha256=xm2nnppMd5EEAD9-KqQLXGO838u1Srm6HROvgNaLErc,1161
|
|
164
159
|
kameleoon/targeting/conditions/unknown_condition.py,sha256=Hw2-ncD6bSSqQutiz_2uSqJNQMgwzh82k-UiVnJN8ag,551
|
|
165
|
-
kameleoon/targeting/conditions/version_condition.py,sha256=
|
|
160
|
+
kameleoon/targeting/conditions/version_condition.py,sha256=YUcP4ZDcNz2hYfdB-7uB0TgtwZ08LOYxe5X_eiNDuTE,2764
|
|
166
161
|
kameleoon/targeting/conditions/visit_number_today_condition.py,sha256=Zgm4f_YUCBbbItanK0ZVDnYDOU-gOvkJGq-XB0wXDMQ,1769
|
|
167
162
|
kameleoon/targeting/conditions/visit_number_total_condition.py,sha256=eKTBGNPsbwJh4_uayWbdSZxFFK5Cu5vzNjKV9Uilwcg,764
|
|
168
163
|
kameleoon/targeting/conditions/visitor_code_condition.py,sha256=03q8MeXc4y1FhHyA_7rCOmDjN7iDka-sOUfhNDTDrCg,415
|
|
@@ -175,7 +170,7 @@ kameleoon/types/remote_visitor_data_filter.py,sha256=YqInU1WvVwdBC9cTLhVXK7JYLic
|
|
|
175
170
|
kameleoon/types/rule.py,sha256=RrnZA_nkIbvzfUSUcuKCqxqs48aVVk6WhlGfg2WrPns,1307
|
|
176
171
|
kameleoon/types/variable.py,sha256=txEU9O07WPQOz6NjRIibyJWQ_mFexyqRa8YaR6W6eSE,833
|
|
177
172
|
kameleoon/types/variation.py,sha256=vrgQWDeDc1FvsG5_O7CfsB5Xl4QhiKgKsWQMyoF1BkQ,2771
|
|
178
|
-
kameleoon_client_python-3.22.
|
|
179
|
-
kameleoon_client_python-3.22.
|
|
180
|
-
kameleoon_client_python-3.22.
|
|
181
|
-
kameleoon_client_python-3.22.
|
|
173
|
+
kameleoon_client_python-3.22.2.dist-info/METADATA,sha256=5yUAN6CSAN7zbIPgZEcJxK9p-qPsCaxS03iKdDuM9VM,13195
|
|
174
|
+
kameleoon_client_python-3.22.2.dist-info/WHEEL,sha256=AeO2BvogYWm3eGaHCvhzmUYt8ia7KfURiHzO_1atlys,109
|
|
175
|
+
kameleoon_client_python-3.22.2.dist-info/top_level.txt,sha256=i6mIQRivSZ-iWKzQ7WnfUr-c19Hkt0uJR_PKO53yZEc,10
|
|
176
|
+
kameleoon_client_python-3.22.2.dist-info/RECORD,,
|
kameleoon/storage/__init__.py
DELETED
|
File without changes
|
kameleoon/storage/cache.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
""" Kameleoon Visitor Variation """
|
|
2
|
-
|
|
3
|
-
from typing import TypeVar, Generic, Optional
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
K = TypeVar("K")
|
|
7
|
-
V = TypeVar("V")
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class Cache(Generic[K, V]):
|
|
11
|
-
"""
|
|
12
|
-
Interface `Cache` represents a container type that is supposed to store relevant values by distinct keys
|
|
13
|
-
and to manage relevance of the stored values and is not supposed to be thread safe.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
def __init__(self, expiration_period: float):
|
|
17
|
-
if expiration_period <= 0.0:
|
|
18
|
-
raise ValueError("'expiration_period' arg must have positive value!")
|
|
19
|
-
self._expiration_period = expiration_period
|
|
20
|
-
|
|
21
|
-
@property
|
|
22
|
-
def expiration_period(self) -> float:
|
|
23
|
-
"""
|
|
24
|
-
Period during which a stored value is considered as relevant.
|
|
25
|
-
:return: Expiration period
|
|
26
|
-
:rtype: float
|
|
27
|
-
"""
|
|
28
|
-
return self._expiration_period
|
|
29
|
-
|
|
30
|
-
def __len__(self) -> int:
|
|
31
|
-
raise NotImplementedError()
|
|
32
|
-
|
|
33
|
-
def set(self, key: K, value: V) -> None:
|
|
34
|
-
"""
|
|
35
|
-
If item stored by specified key exists, this method updates its value and its relevance,
|
|
36
|
-
otherwise this method creates new relevant item with specified key and value.
|
|
37
|
-
:param key: Key
|
|
38
|
-
:type key: K
|
|
39
|
-
:param value: New value
|
|
40
|
-
:type value: V
|
|
41
|
-
"""
|
|
42
|
-
raise NotImplementedError()
|
|
43
|
-
|
|
44
|
-
def get(self, key: K) -> Optional[V]:
|
|
45
|
-
"""
|
|
46
|
-
Makes an attempt to get value of item stored by specified key.
|
|
47
|
-
:param key: Key
|
|
48
|
-
:type key: K
|
|
49
|
-
:return: Stored value or None if operation fails
|
|
50
|
-
:rtype: Optional[V]
|
|
51
|
-
"""
|
|
52
|
-
raise NotImplementedError()
|
|
53
|
-
|
|
54
|
-
def clear(self) -> None:
|
|
55
|
-
"""
|
|
56
|
-
Removes all stored values.
|
|
57
|
-
"""
|
|
58
|
-
raise NotImplementedError()
|
|
59
|
-
|
|
60
|
-
def purge(self) -> None:
|
|
61
|
-
"""
|
|
62
|
-
Removes all expired items.
|
|
63
|
-
"""
|
|
64
|
-
raise NotImplementedError()
|
|
65
|
-
|
|
66
|
-
def __iter__(self):
|
|
67
|
-
raise NotImplementedError()
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
""" Kameleoon Visitor Variation """
|
|
2
|
-
|
|
3
|
-
from .cache import Cache, K, V
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class CacheFactory:
|
|
7
|
-
"""`CacheFactory` is supposed to be used for creating instances of `Cache` type."""
|
|
8
|
-
|
|
9
|
-
def create(self, expiration_period: float) -> Cache[K, V]:
|
|
10
|
-
"""
|
|
11
|
-
Creates an instance of `Cache` type with specified expiration period.
|
|
12
|
-
:param expiration_period: Expiration period of new instance of `Cache` type
|
|
13
|
-
:type expiration_period: float
|
|
14
|
-
:return: New instance of `Cache` type
|
|
15
|
-
:rtype: Cache
|
|
16
|
-
"""
|
|
17
|
-
raise NotImplementedError()
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
""" Kameleoon Visitor Variation """
|
|
2
|
-
|
|
3
|
-
from .cache import Cache, K, V
|
|
4
|
-
from .cache_impl import CacheImpl
|
|
5
|
-
from .cache_factory import CacheFactory
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class CacheFactoryImpl(CacheFactory):
|
|
9
|
-
"""`CacheFactoryImpl` implements `CacheFactory` interface."""
|
|
10
|
-
|
|
11
|
-
def create(self, expiration_period: float) -> Cache[K, V]:
|
|
12
|
-
return CacheImpl(expiration_period)
|
kameleoon/storage/cache_impl.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
""" Kameleoon Visitor Variation """
|
|
2
|
-
|
|
3
|
-
import time
|
|
4
|
-
from typing import Dict, Tuple, Optional
|
|
5
|
-
from .cache import Cache, K, V
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class CacheImpl(Cache[K, V]):
|
|
9
|
-
"""`CacheImpl` implements `Cache` interface."""
|
|
10
|
-
|
|
11
|
-
def __init__(self, expiration_period: float):
|
|
12
|
-
super().__init__(expiration_period)
|
|
13
|
-
self.__items: Dict[K, Tuple[V, float]] = {}
|
|
14
|
-
|
|
15
|
-
def __len__(self) -> int:
|
|
16
|
-
return len(self.__items)
|
|
17
|
-
|
|
18
|
-
def set(self, key: K, value: V) -> None:
|
|
19
|
-
expiration_ts = time.time() + self._expiration_period
|
|
20
|
-
self.__items[key] = (value, expiration_ts)
|
|
21
|
-
|
|
22
|
-
def get(self, key: K) -> Optional[V]:
|
|
23
|
-
item = self.__items.get(key, None)
|
|
24
|
-
return item[0] if item is not None else None
|
|
25
|
-
|
|
26
|
-
def clear(self) -> None:
|
|
27
|
-
self.__items.clear()
|
|
28
|
-
|
|
29
|
-
def purge(self) -> None:
|
|
30
|
-
now = time.time()
|
|
31
|
-
to_be_removed = [k for k, v in self.__items.items() if v[1] <= now]
|
|
32
|
-
for key in to_be_removed:
|
|
33
|
-
del self.__items[key]
|
|
34
|
-
|
|
35
|
-
def __iter__(self):
|
|
36
|
-
return ((k, v[0]) for k, v in self.__items.items())
|
|
File without changes
|
{kameleoon_client_python-3.22.0.dist-info → kameleoon_client_python-3.22.2.dist-info}/top_level.txt
RENAMED
|
File without changes
|