rudder-sdk-python 2.1.2__tar.gz → 2.1.4__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 (27) hide show
  1. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/PKG-INFO +5 -3
  2. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/rudder_sdk_python.egg-info/PKG-INFO +5 -3
  3. rudder_sdk_python-2.1.4/rudder_sdk_python.egg-info/SOURCES.txt +24 -0
  4. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/rudder_sdk_python.egg-info/requires.txt +4 -2
  5. rudder_sdk_python-2.1.4/rudder_sdk_python.egg-info/top_level.txt +1 -0
  6. rudder_sdk_python-2.1.4/rudderstack/analytics/__init__.py +113 -0
  7. rudder_sdk_python-2.1.4/rudderstack/analytics/client.py +338 -0
  8. rudder_sdk_python-2.1.4/rudderstack/analytics/consumer.py +133 -0
  9. rudder_sdk_python-2.1.4/rudderstack/analytics/get_env.py +7 -0
  10. rudder_sdk_python-2.1.4/rudderstack/analytics/request.py +82 -0
  11. rudder_sdk_python-2.1.4/rudderstack/analytics/test/__init__.py +82 -0
  12. rudder_sdk_python-2.1.4/rudderstack/analytics/test/test_client.py +355 -0
  13. rudder_sdk_python-2.1.4/rudderstack/analytics/test/test_constants.py +1 -0
  14. rudder_sdk_python-2.1.4/rudderstack/analytics/test/test_consumer.py +226 -0
  15. rudder_sdk_python-2.1.4/rudderstack/analytics/test/test_module.py +49 -0
  16. rudder_sdk_python-2.1.4/rudderstack/analytics/test/test_request.py +77 -0
  17. rudder_sdk_python-2.1.4/rudderstack/analytics/test/test_utils.py +73 -0
  18. rudder_sdk_python-2.1.4/rudderstack/analytics/utils.py +88 -0
  19. rudder_sdk_python-2.1.4/rudderstack/analytics/version.py +1 -0
  20. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/setup.py +6 -5
  21. rudder_sdk_python-2.1.2/rudder_sdk_python.egg-info/SOURCES.txt +0 -10
  22. rudder_sdk_python-2.1.2/rudder_sdk_python.egg-info/top_level.txt +0 -1
  23. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/LICENSE.md +0 -0
  24. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/README.md +0 -0
  25. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/rudder_sdk_python.egg-info/dependency_links.txt +0 -0
  26. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/rudder_sdk_python.egg-info/not-zip-safe +0 -0
  27. {rudder_sdk_python-2.1.2 → rudder_sdk_python-2.1.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rudder-sdk-python
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: RudderStack is an open-source Segment alternative written in Go, built for the enterprise.
5
5
  Home-page: https://github.com/rudderlabs/rudder-sdk-python
6
6
  Author: RudderStack
@@ -27,10 +27,12 @@ License-File: LICENSE.md
27
27
  Requires-Dist: requests<3.0,>=2.32.3
28
28
  Requires-Dist: monotonic<2.0,>=1.6
29
29
  Requires-Dist: backoff<3.0,>=2.2.1
30
+ Requires-Dist: python-dateutil<3.0,>=2.2
31
+ Requires-Dist: python-dotenv<2.0.0,>=1.0.1
30
32
  Requires-Dist: deprecation<3.0.0,>=2.1.0
31
33
  Provides-Extra: test
32
- Requires-Dist: mock==4.0.3; extra == "test"
33
- Requires-Dist: flake8==7.1.0; extra == "test"
34
+ Requires-Dist: mock==5.1.0; extra == "test"
35
+ Requires-Dist: flake8==7.1.1; extra == "test"
34
36
 
35
37
  # RudderStack Python SDK
36
38
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rudder-sdk-python
3
- Version: 2.1.2
3
+ Version: 2.1.4
4
4
  Summary: RudderStack is an open-source Segment alternative written in Go, built for the enterprise.
5
5
  Home-page: https://github.com/rudderlabs/rudder-sdk-python
6
6
  Author: RudderStack
@@ -27,10 +27,12 @@ License-File: LICENSE.md
27
27
  Requires-Dist: requests<3.0,>=2.32.3
28
28
  Requires-Dist: monotonic<2.0,>=1.6
29
29
  Requires-Dist: backoff<3.0,>=2.2.1
30
+ Requires-Dist: python-dateutil<3.0,>=2.2
31
+ Requires-Dist: python-dotenv<2.0.0,>=1.0.1
30
32
  Requires-Dist: deprecation<3.0.0,>=2.1.0
31
33
  Provides-Extra: test
32
- Requires-Dist: mock==4.0.3; extra == "test"
33
- Requires-Dist: flake8==7.1.0; extra == "test"
34
+ Requires-Dist: mock==5.1.0; extra == "test"
35
+ Requires-Dist: flake8==7.1.1; extra == "test"
34
36
 
35
37
  # RudderStack Python SDK
36
38
 
@@ -0,0 +1,24 @@
1
+ LICENSE.md
2
+ README.md
3
+ setup.cfg
4
+ setup.py
5
+ rudder_sdk_python.egg-info/PKG-INFO
6
+ rudder_sdk_python.egg-info/SOURCES.txt
7
+ rudder_sdk_python.egg-info/dependency_links.txt
8
+ rudder_sdk_python.egg-info/not-zip-safe
9
+ rudder_sdk_python.egg-info/requires.txt
10
+ rudder_sdk_python.egg-info/top_level.txt
11
+ rudderstack/analytics/__init__.py
12
+ rudderstack/analytics/client.py
13
+ rudderstack/analytics/consumer.py
14
+ rudderstack/analytics/get_env.py
15
+ rudderstack/analytics/request.py
16
+ rudderstack/analytics/utils.py
17
+ rudderstack/analytics/version.py
18
+ rudderstack/analytics/test/__init__.py
19
+ rudderstack/analytics/test/test_client.py
20
+ rudderstack/analytics/test/test_constants.py
21
+ rudderstack/analytics/test/test_consumer.py
22
+ rudderstack/analytics/test/test_module.py
23
+ rudderstack/analytics/test/test_request.py
24
+ rudderstack/analytics/test/test_utils.py
@@ -1,8 +1,10 @@
1
1
  requests<3.0,>=2.32.3
2
2
  monotonic<2.0,>=1.6
3
3
  backoff<3.0,>=2.2.1
4
+ python-dateutil<3.0,>=2.2
5
+ python-dotenv<2.0.0,>=1.0.1
4
6
  deprecation<3.0.0,>=2.1.0
5
7
 
6
8
  [test]
7
- mock==4.0.3
8
- flake8==7.1.0
9
+ mock==5.1.0
10
+ flake8==7.1.1
@@ -0,0 +1,113 @@
1
+
2
+ import warnings
3
+ from rudderstack.analytics.version import VERSION
4
+ from rudderstack.analytics.client import Client
5
+ import deprecation
6
+ __version__ = VERSION
7
+
8
+ """Settings."""
9
+ write_key = Client.DefaultConfig.write_key
10
+
11
+ @property
12
+ @deprecation.deprecated(deprecated_in="2.0",
13
+ current_version=__version__,
14
+ details="Use the dataPlaneUrl property instead")
15
+ def host(self):
16
+ warnings.warn('The use of host is deprecated. Use dataPlaneUrl instead', DeprecationWarning)
17
+ return host
18
+
19
+ @host.setter
20
+ @deprecation.deprecated(deprecated_in="2.0",
21
+ current_version=__version__,
22
+ details="Use the dataPlaneUrl property instead")
23
+ def host(self, value: str):
24
+ warnings.warn('The use of host is deprecated. Use dataPlaneUrl instead', DeprecationWarning)
25
+ self.host = value
26
+
27
+ host = Client.DefaultConfig.host
28
+
29
+ @property
30
+ def dataPlaneUrl(self):
31
+ return dataPlaneUrl
32
+
33
+ @dataPlaneUrl.setter
34
+ def dataPlaneUrl(self, value: str):
35
+ self.host = value
36
+
37
+ on_error = Client.DefaultConfig.on_error
38
+ debug = Client.DefaultConfig.debug
39
+ send = Client.DefaultConfig.send
40
+ sync_mode = Client.DefaultConfig.sync_mode
41
+ max_queue_size = Client.DefaultConfig.max_queue_size
42
+ gzip = Client.DefaultConfig.gzip
43
+ timeout = Client.DefaultConfig.timeout
44
+ upload_interval = Client.DefaultConfig.upload_interval
45
+ upload_size = Client.DefaultConfig.upload_size
46
+ max_retries = Client.DefaultConfig.max_retries
47
+
48
+ default_client = None
49
+
50
+
51
+ def track(*args, **kwargs):
52
+ """Send a track call."""
53
+ _proxy('track', *args, **kwargs)
54
+
55
+
56
+ def identify(*args, **kwargs):
57
+ """Send a identify call."""
58
+ _proxy('identify', *args, **kwargs)
59
+
60
+
61
+ def group(*args, **kwargs):
62
+ """Send a group call."""
63
+ _proxy('group', *args, **kwargs)
64
+
65
+
66
+ def alias(*args, **kwargs):
67
+ """Send a alias call."""
68
+ _proxy('alias', *args, **kwargs)
69
+
70
+
71
+ def page(*args, **kwargs):
72
+ """Send a page call."""
73
+ _proxy('page', *args, **kwargs)
74
+
75
+
76
+ def screen(*args, **kwargs):
77
+ """Send a screen call."""
78
+ _proxy('screen', *args, **kwargs)
79
+
80
+
81
+ def flush():
82
+ """Tell the client to flush."""
83
+ _proxy('flush')
84
+
85
+
86
+ def join():
87
+ """Block program until the client clears the queue"""
88
+ _proxy('join')
89
+
90
+
91
+ def shutdown():
92
+ """Flush all messages and cleanly shutdown the client"""
93
+ _proxy('flush')
94
+ _proxy('join')
95
+
96
+
97
+ def _proxy(method, *args, **kwargs):
98
+ """Create an analytics client if one doesn't exist and send to it."""
99
+ global default_client
100
+ if not default_client:
101
+ if isinstance(dataPlaneUrl,str) and dataPlaneUrl != "":
102
+ finalDataplaneUrl = dataPlaneUrl
103
+ else:
104
+ finalDataplaneUrl = host
105
+
106
+ default_client = Client(write_key, host=finalDataplaneUrl, debug=debug,
107
+ max_queue_size=max_queue_size,
108
+ send=send, on_error=on_error,
109
+ gzip=gzip, max_retries=max_retries,
110
+ sync_mode=sync_mode, timeout=timeout)
111
+
112
+ fn = getattr(default_client, method)
113
+ fn(*args, **kwargs)
@@ -0,0 +1,338 @@
1
+ from datetime import datetime, timezone
2
+ from uuid import uuid4
3
+ import logging
4
+ import numbers
5
+ import atexit
6
+ import json
7
+
8
+ from dateutil.tz import tzutc
9
+ from rudderstack.analytics.get_env import TEST_DATA_PLANE_URL
10
+
11
+ from rudderstack.analytics.utils import guess_timezone, clean
12
+ from rudderstack.analytics.consumer import Consumer, MAX_MSG_SIZE
13
+ from rudderstack.analytics.request import post, DatetimeSerializer
14
+ from rudderstack.analytics.version import VERSION
15
+
16
+ import queue
17
+
18
+ ID_TYPES = (numbers.Number, str)
19
+ CHANNEL = 'server'
20
+
21
+ class Client(object):
22
+ class DefaultConfig(object):
23
+ write_key = None
24
+ host = TEST_DATA_PLANE_URL
25
+ on_error = None
26
+ debug = False
27
+ send = True
28
+ sync_mode = False
29
+ max_queue_size = 10000
30
+ gzip = True
31
+ timeout = 15
32
+ max_retries = 10
33
+ proxies = None
34
+ thread = 1
35
+ upload_interval = 0.5
36
+ upload_size = 100
37
+
38
+ """Create a new rudderstack client."""
39
+ log = logging.getLogger('rudderstack')
40
+ def __init__(self,
41
+ write_key=DefaultConfig.write_key,
42
+ host=DefaultConfig.host,
43
+ debug=DefaultConfig.debug,
44
+ max_queue_size=DefaultConfig.max_queue_size,
45
+ send=DefaultConfig.send,
46
+ on_error=DefaultConfig.on_error,
47
+ gzip=DefaultConfig.gzip,
48
+ max_retries=DefaultConfig.max_retries,
49
+ sync_mode=DefaultConfig.sync_mode,
50
+ timeout=DefaultConfig.timeout,
51
+ proxies=DefaultConfig.proxies,
52
+ thread=DefaultConfig.thread,
53
+ upload_size=DefaultConfig.upload_size,
54
+ upload_interval=DefaultConfig.upload_interval,):
55
+ require('write_key', write_key, str)
56
+
57
+ self.queue = queue.Queue(max_queue_size)
58
+ self.write_key = write_key
59
+ self.on_error = on_error
60
+ self.debug = debug
61
+ self.send = send
62
+ self.sync_mode = sync_mode
63
+ self.host = host
64
+ self.gzip = gzip
65
+ self.timeout = timeout
66
+ self.proxies = proxies
67
+
68
+ if debug:
69
+ self.log.setLevel(logging.DEBUG)
70
+
71
+ if sync_mode:
72
+ self.consumers = None
73
+ else:
74
+ # On program exit, allow the consumer thread to exit cleanly.
75
+ # This prevents exceptions and a messy shutdown when the
76
+ # interpreter is destroyed before the daemon thread finishes
77
+ # execution. However, it is *not* the same as flushing the queue!
78
+ # To guarantee all messages have been delivered, you'll still need
79
+ # to call flush().
80
+ if send:
81
+ atexit.register(self.join)
82
+ for _ in range(thread):
83
+ self.consumers = []
84
+ consumer = Consumer(
85
+ self.queue, write_key, host=host, on_error=on_error,
86
+ upload_size=upload_size, upload_interval=upload_interval,
87
+ gzip=gzip, retries=max_retries, timeout=timeout,
88
+ proxies=proxies,
89
+ )
90
+ self.consumers.append(consumer)
91
+
92
+ # if we've disabled sending, just don't start the consumer
93
+ if send:
94
+ consumer.start()
95
+
96
+ def identify(self, user_id=None, traits=None, context=None, timestamp=None,
97
+ anonymous_id=None, integrations=None, message_id=None):
98
+ traits = traits or {}
99
+ context = context or {}
100
+ # putting traits inside context
101
+ context['traits'] = traits
102
+ integrations = integrations or {}
103
+ require('user_id or anonymous_id', user_id or anonymous_id, ID_TYPES)
104
+ require('traits', traits, dict)
105
+
106
+ msg = {
107
+ 'integrations': integrations,
108
+ 'anonymousId': anonymous_id,
109
+ 'timestamp': timestamp,
110
+ 'context': context,
111
+ 'type': 'identify',
112
+ 'userId': user_id,
113
+ # 'traits': traits, #traits not needed at root level
114
+ 'messageId': message_id,
115
+ }
116
+
117
+ return self._enqueue(msg)
118
+
119
+ def track(self, user_id=None, event=None, properties=None, context=None,
120
+ timestamp=None, anonymous_id=None, integrations=None,
121
+ message_id=None):
122
+ properties = properties or {}
123
+ context = context or {}
124
+ integrations = integrations or {}
125
+ require('user_id or anonymous_id', user_id or anonymous_id, ID_TYPES)
126
+ require('properties', properties, dict)
127
+ require('event', event, str)
128
+
129
+ msg = {
130
+ 'integrations': integrations,
131
+ 'anonymousId': anonymous_id,
132
+ 'properties': properties,
133
+ 'timestamp': timestamp,
134
+ 'context': context,
135
+ 'userId': user_id,
136
+ 'type': 'track',
137
+ 'event': event,
138
+ 'messageId': message_id,
139
+ }
140
+
141
+ return self._enqueue(msg)
142
+
143
+ def alias(self, previous_id=None, user_id=None, context=None,
144
+ timestamp=None, integrations=None, message_id=None):
145
+ context = context or {}
146
+ integrations = integrations or {}
147
+ require('previous_id', previous_id, ID_TYPES)
148
+ require('user_id', user_id, ID_TYPES)
149
+
150
+ msg = {
151
+ 'integrations': integrations,
152
+ 'previousId': previous_id,
153
+ 'timestamp': timestamp,
154
+ 'context': context,
155
+ 'userId': user_id,
156
+ 'type': 'alias',
157
+ 'messageId': message_id,
158
+ }
159
+
160
+ return self._enqueue(msg)
161
+
162
+ def group(self, user_id=None, group_id=None, traits=None, context=None,
163
+ timestamp=None, anonymous_id=None, integrations=None,
164
+ message_id=None):
165
+ traits = traits or {}
166
+ context = context or {}
167
+ integrations = integrations or {}
168
+ require('user_id or anonymous_id', user_id or anonymous_id, ID_TYPES)
169
+ require('group_id', group_id, ID_TYPES)
170
+ require('traits', traits, dict)
171
+
172
+ msg = {
173
+ 'integrations': integrations,
174
+ 'anonymousId': anonymous_id,
175
+ 'timestamp': timestamp,
176
+ 'groupId': group_id,
177
+ 'context': context,
178
+ 'userId': user_id,
179
+ 'traits': traits,
180
+ 'type': 'group',
181
+ 'messageId': message_id,
182
+ }
183
+
184
+ return self._enqueue(msg)
185
+
186
+ def page(self, user_id=None, category=None, name=None, properties=None,
187
+ context=None, timestamp=None, anonymous_id=None,
188
+ integrations=None, message_id=None):
189
+ properties = properties or {}
190
+ context = context or {}
191
+ integrations = integrations or {}
192
+ require('user_id or anonymous_id', user_id or anonymous_id, ID_TYPES)
193
+ require('properties', properties, dict)
194
+
195
+ if name:
196
+ require('name', name, str)
197
+ if category:
198
+ require('category', category, str)
199
+
200
+ msg = {
201
+ 'integrations': integrations,
202
+ 'anonymousId': anonymous_id,
203
+ 'properties': properties,
204
+ 'timestamp': timestamp,
205
+ 'category': category,
206
+ 'context': context,
207
+ 'userId': user_id,
208
+ 'type': 'page',
209
+ 'name': name,
210
+ 'messageId': message_id,
211
+ }
212
+
213
+ return self._enqueue(msg)
214
+
215
+ def screen(self, user_id=None, category=None, name=None, properties=None,
216
+ context=None, timestamp=None, anonymous_id=None,
217
+ integrations=None, message_id=None):
218
+ properties = properties or {}
219
+ context = context or {}
220
+ integrations = integrations or {}
221
+ require('user_id or anonymous_id', user_id or anonymous_id, ID_TYPES)
222
+ require('properties', properties, dict)
223
+
224
+ if name:
225
+ require('name', name, str)
226
+ if category:
227
+ require('category', category, str)
228
+
229
+ msg = {
230
+ 'integrations': integrations,
231
+ 'anonymousId': anonymous_id,
232
+ 'properties': properties,
233
+ 'timestamp': timestamp,
234
+ 'category': category,
235
+ 'context': context,
236
+ 'userId': user_id,
237
+ 'type': 'screen',
238
+ 'name': name,
239
+ 'messageId': message_id,
240
+ }
241
+
242
+ return self._enqueue(msg)
243
+
244
+ def _enqueue(self, msg):
245
+ """Push a new `msg` onto the queue, return `(success, msg)`"""
246
+ timestamp = msg['timestamp']
247
+ if timestamp is None:
248
+ timestamp = datetime.now(timezone.utc).replace(tzinfo=tzutc())
249
+ message_id = msg.get('messageId')
250
+ if message_id is None:
251
+ message_id = uuid4()
252
+
253
+ # default integrations should be "All": True
254
+ if msg['integrations'] == {} :
255
+ msg['integrations'] = {"All" : True}
256
+
257
+ require('integrations', msg['integrations'], dict)
258
+ require('type', msg['type'], str)
259
+ require('timestamp', timestamp, datetime)
260
+ require('context', msg['context'], dict)
261
+
262
+ # add common
263
+ timestamp = guess_timezone(timestamp)
264
+ msg['timestamp'] = timestamp.isoformat()
265
+ msg['messageId'] = stringify_id(message_id)
266
+ msg['context']['library'] = {
267
+ 'name': 'analytics-python',
268
+ 'version': VERSION
269
+ }
270
+ msg['userId'] = stringify_id(msg.get('userId', None))
271
+ msg['anonymousId'] = stringify_id(msg.get('anonymousId', None))
272
+ msg['channel'] = CHANNEL
273
+ msg = clean(msg)
274
+ self.log.debug('queueing: %s', msg)
275
+
276
+ # Check message size.
277
+ msg_size = len(json.dumps(msg, cls=DatetimeSerializer).encode())
278
+ if msg_size > MAX_MSG_SIZE:
279
+ raise RuntimeError(f'Message exceeds {str(int(MAX_MSG_SIZE / 1024))}kb limit. ({str(msg)})')
280
+
281
+ # if send is False, return msg as if it was successfully queued
282
+ if not self.send:
283
+ return True, msg
284
+
285
+ if self.sync_mode:
286
+ self.log.debug('enqueued with blocking %s.', msg['type'])
287
+ post(self.write_key, self.host, gzip=self.gzip,
288
+ timeout=self.timeout, proxies=self.proxies, batch=[msg])
289
+
290
+ return True, msg
291
+
292
+ try:
293
+ self.queue.put(msg, block=False)
294
+ self.log.debug('enqueued %s.', msg['type'])
295
+ return True, msg
296
+ except queue.Full:
297
+ self.log.warning('analytics-python queue is full')
298
+ return False, msg
299
+
300
+ def flush(self):
301
+ """Forces a flush from the internal queue to the server"""
302
+ queue = self.queue
303
+ size = queue.qsize()
304
+ queue.join()
305
+ # Note that this message may not be precise, because of threading.
306
+ self.log.debug('successfully flushed about %s items.', size)
307
+
308
+ def join(self):
309
+ """Ends the consumer thread once the queue is empty.
310
+ Blocks execution until finished
311
+ """
312
+ for consumer in self.consumers:
313
+ consumer.pause()
314
+ try:
315
+ consumer.join()
316
+ except RuntimeError:
317
+ # consumer thread has not started
318
+ pass
319
+
320
+ def shutdown(self):
321
+ """Flush all messages and cleanly shutdown the client"""
322
+ self.flush()
323
+ self.join()
324
+
325
+
326
+ def require(name, field, data_type):
327
+ """Require that the named `field` has the right `data_type`"""
328
+ if not isinstance(field, data_type):
329
+ msg = '{0} must have {1}, got: {2}'.format(name, data_type, field)
330
+ raise AssertionError(msg)
331
+
332
+
333
+ def stringify_id(val):
334
+ if val is None:
335
+ return None
336
+ if isinstance(val, str):
337
+ return val
338
+ return str(val)
@@ -0,0 +1,133 @@
1
+ import logging
2
+ from threading import Thread
3
+ import monotonic
4
+ import backoff
5
+ import json
6
+
7
+ from rudderstack.analytics.request import post, APIError, DatetimeSerializer
8
+
9
+ from queue import Empty
10
+
11
+ # Our servers only accept payloads within this limit. Here limit is set slightly
12
+ # lower to leave space for extra data that will be added later, eg. "sentAt".
13
+ MAX_MSG_SIZE = 4 << 10 << 10 ## 4 mb
14
+ BATCH_SIZE_LIMIT = 4 << 10 << 10 ## 4 mb
15
+
16
+
17
+ class Consumer(Thread):
18
+ """Consumes the messages from the client's queue."""
19
+ log = logging.getLogger('rudderstack')
20
+
21
+ def __init__(self, queue, write_key, upload_size=100, host=None,
22
+ on_error=None, upload_interval=0.5, gzip=True, retries=10,
23
+ timeout=15, proxies=None):
24
+ """Create a consumer thread."""
25
+ Thread.__init__(self)
26
+ # Make consumer a daemon thread so that it doesn't block program exit
27
+ self.daemon = True
28
+ self.upload_size = upload_size
29
+ self.upload_interval = upload_interval
30
+ self.write_key = write_key
31
+ self.host = host
32
+ self.on_error = on_error
33
+ self.queue = queue
34
+ self.gzip = gzip
35
+ # It's important to set running in the constructor: if we are asked to
36
+ # pause immediately after construction, we might set running to True in
37
+ # run() *after* we set it to False in pause... and keep running
38
+ # forever.
39
+ self.running = True
40
+ self.retries = retries
41
+ self.timeout = timeout
42
+ self.proxies = proxies
43
+
44
+ def run(self):
45
+ """Runs the consumer."""
46
+ self.log.debug('consumer is running...')
47
+ while self.running:
48
+ self.upload()
49
+
50
+ self.log.debug('consumer exited.')
51
+
52
+ def pause(self):
53
+ """Pause the consumer."""
54
+ self.running = False
55
+
56
+ def upload(self):
57
+ """Upload the next batch of items, return whether successful."""
58
+ success = False
59
+ batch = self.next()
60
+ if len(batch) == 0:
61
+ return False
62
+
63
+ try:
64
+ self.request(batch)
65
+ success = True
66
+ except Exception as e:
67
+ self.log.error('error uploading: %s', e)
68
+ success = False
69
+ if self.on_error:
70
+ self.on_error(e, batch)
71
+ finally:
72
+ # mark items as acknowledged from queue
73
+ for _ in batch:
74
+ self.queue.task_done()
75
+ return success
76
+
77
+ def next(self):
78
+ """Return the next batch of items to upload."""
79
+ queue = self.queue
80
+ items = []
81
+
82
+ start_time = monotonic.monotonic()
83
+ total_size = 0
84
+
85
+ while len(items) < self.upload_size:
86
+ elapsed = monotonic.monotonic() - start_time
87
+ if elapsed >= self.upload_interval:
88
+ break
89
+ try:
90
+ item = queue.get(
91
+ block=True, timeout=self.upload_interval - elapsed)
92
+ item_size = len(json.dumps(
93
+ item, cls=DatetimeSerializer).encode())
94
+ if item_size > MAX_MSG_SIZE:
95
+ self.log.error(
96
+ 'Item exceeds 4mb limit, dropping. (%s)', str(item))
97
+ continue
98
+ items.append(item)
99
+ total_size += item_size
100
+ if total_size >= BATCH_SIZE_LIMIT:
101
+ self.log.debug(
102
+ 'hit batch size limit (size: %d)', total_size)
103
+ break
104
+ except Empty:
105
+ break
106
+ except Exception as e:
107
+ self.log.exception('Exception: %s', e)
108
+
109
+ return items
110
+
111
+ def request(self, batch):
112
+ """Attempt to upload the batch and retry before raising an error """
113
+
114
+ def fatal_exception(exc):
115
+ if isinstance(exc, APIError):
116
+ # retry on server errors and client errors
117
+ # with 429 status code (rate limited),
118
+ # don't retry on other client errors
119
+ return (400 <= exc.status < 500) and exc.status != 429
120
+ else:
121
+ # retry on all other errors (eg. network)
122
+ return False
123
+
124
+ @backoff.on_exception(
125
+ backoff.expo,
126
+ Exception,
127
+ max_tries=self.retries + 1,
128
+ giveup=fatal_exception)
129
+ def send_request():
130
+ post(self.write_key, self.host, gzip=self.gzip,
131
+ timeout=self.timeout, batch=batch, proxies=self.proxies)
132
+
133
+ send_request()
@@ -0,0 +1,7 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ TEST_WRITE_KEY = os.getenv('TEST_WRITE_KEY')
7
+ TEST_DATA_PLANE_URL = os.getenv('TEST_DATA_PLANE_URL')