rudder-sdk-python 2.1.3__tar.gz → 2.1.8__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 (30) hide show
  1. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/PKG-INFO +39 -4
  2. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/README.md +36 -0
  3. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudder_sdk_python.egg-info/PKG-INFO +39 -4
  4. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudder_sdk_python.egg-info/SOURCES.txt +3 -0
  5. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudder_sdk_python.egg-info/requires.txt +2 -3
  6. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/__init__.py +1 -2
  7. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/client.py +2 -2
  8. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/consumer.py +7 -8
  9. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/request.py +8 -5
  10. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/test/test_client.py +13 -7
  11. rudder_sdk_python-2.1.8/rudderstack/analytics/test/test_constants.py +4 -0
  12. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/test/test_consumer.py +10 -4
  13. rudder_sdk_python-2.1.8/rudderstack/analytics/test/test_public_api_contract.py +96 -0
  14. rudder_sdk_python-2.1.8/rudderstack/analytics/test/test_release_validation.py +62 -0
  15. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/test/test_request.py +13 -8
  16. rudder_sdk_python-2.1.8/rudderstack/analytics/test/test_request_options.py +136 -0
  17. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/test/test_utils.py +2 -2
  18. rudder_sdk_python-2.1.8/rudderstack/analytics/version.py +1 -0
  19. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/setup.py +2 -4
  20. rudder_sdk_python-2.1.3/rudderstack/analytics/test/test_constants.py +0 -1
  21. rudder_sdk_python-2.1.3/rudderstack/analytics/version.py +0 -1
  22. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/LICENSE.md +0 -0
  23. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudder_sdk_python.egg-info/dependency_links.txt +0 -0
  24. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudder_sdk_python.egg-info/not-zip-safe +0 -0
  25. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudder_sdk_python.egg-info/top_level.txt +0 -0
  26. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/get_env.py +0 -0
  27. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/test/__init__.py +0 -0
  28. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/test/test_module.py +0 -0
  29. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/rudderstack/analytics/utils.py +0 -0
  30. {rudder_sdk_python-2.1.3 → rudder_sdk_python-2.1.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rudder-sdk-python
3
- Version: 2.1.3
3
+ Version: 2.1.8
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
@@ -25,17 +25,27 @@ Requires-Python: >=3.8.0
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE.md
27
27
  Requires-Dist: requests<3.0,>=2.32.3
28
- Requires-Dist: monotonic<2.0,>=1.6
29
28
  Requires-Dist: backoff<3.0,>=2.2.1
30
29
  Requires-Dist: python-dateutil<3.0,>=2.2
31
30
  Requires-Dist: python-dotenv<2.0.0,>=1.0.1
32
31
  Requires-Dist: deprecation<3.0.0,>=2.1.0
33
32
  Provides-Extra: test
34
- Requires-Dist: mock==4.0.3; extra == "test"
35
- Requires-Dist: flake8==7.1.0; extra == "test"
33
+ Requires-Dist: mock==5.1.0; extra == "test"
34
+ Requires-Dist: flake8==7.1.1; extra == "test"
35
+
36
+ <p align="center">
37
+ <a href="https://rudderstack.com/">
38
+ <picture>
39
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rudderlabs/rudder-sdk-js/develop/assets/rs-logo-full-dark.png">
40
+ <img alt="RudderStack" width="512" src="https://raw.githubusercontent.com/rudderlabs/rudder-sdk-js/develop/assets/rs-logo-full-light.jpg">
41
+ </picture>
42
+ </a>
43
+ </p>
36
44
 
37
45
  # RudderStack Python SDK
38
46
 
47
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rudderlabs/rudder-sdk-python)
48
+
39
49
  RudderStack’s Python SDK lets you track events from your Python application. Once enabled, the event requests hit the RudderStack servers. RudderStack then transforms and routes these events to your specified destination platforms.
40
50
 
41
51
  More details on the Python SDK can be found in our [**documentation**](https://docs.rudderstack.com/stream-sources/rudderstack-sdk-integration-guides/rudderstack-python-sdk)
@@ -79,6 +89,31 @@ analytics.track('user_id', 'Simple Track Event', anonymous_id='anonymousId',
79
89
 
80
90
  For more information on the supported calls, refer to the [**documentation**](https://docs.rudderstack.com/stream-sources/rudderstack-sdk-integration-guides/rudderstack-python-sdk#sending-events-from-rudderstack).
81
91
 
92
+ ## Proxy configuration
93
+
94
+ Pass `proxies` when you create a `Client`. A Requests-style mapping is preferred:
95
+
96
+ ```python
97
+ from rudderstack.analytics.client import Client
98
+
99
+ client = Client(
100
+ 'SOURCE_WRITE_KEY',
101
+ host='https://YOUR_DATA_PLANE_URL',
102
+ proxies={
103
+ 'http': 'http://proxy.example:8080',
104
+ 'https': 'http://proxy.example:8080',
105
+ },
106
+ )
107
+ ```
108
+
109
+ A legacy string such as `proxy.example:8080` is also supported. The SDK applies
110
+ the string to both HTTP and HTTPS requests. If the string has no scheme, the SDK
111
+ adds `http://`. An existing scheme is preserved.
112
+
113
+ Configured proxies are now used for event requests. Earlier versions ignored
114
+ this option. Check that any existing proxy configuration is reachable before
115
+ upgrading.
116
+
82
117
  ## About RudderStack
83
118
 
84
119
  [**RudderStack**](https://rudderstack.com/) is a customer data platform for developers. Our tooling makes it easy to deploy pipelines that collect customer data from every app, website and SaaS platform, then activate it in your warehouse and business tools.
@@ -1,5 +1,16 @@
1
+ <p align="center">
2
+ <a href="https://rudderstack.com/">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rudderlabs/rudder-sdk-js/develop/assets/rs-logo-full-dark.png">
5
+ <img alt="RudderStack" width="512" src="https://raw.githubusercontent.com/rudderlabs/rudder-sdk-js/develop/assets/rs-logo-full-light.jpg">
6
+ </picture>
7
+ </a>
8
+ </p>
9
+
1
10
  # RudderStack Python SDK
2
11
 
12
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rudderlabs/rudder-sdk-python)
13
+
3
14
  RudderStack’s Python SDK lets you track events from your Python application. Once enabled, the event requests hit the RudderStack servers. RudderStack then transforms and routes these events to your specified destination platforms.
4
15
 
5
16
  More details on the Python SDK can be found in our [**documentation**](https://docs.rudderstack.com/stream-sources/rudderstack-sdk-integration-guides/rudderstack-python-sdk)
@@ -43,6 +54,31 @@ analytics.track('user_id', 'Simple Track Event', anonymous_id='anonymousId',
43
54
 
44
55
  For more information on the supported calls, refer to the [**documentation**](https://docs.rudderstack.com/stream-sources/rudderstack-sdk-integration-guides/rudderstack-python-sdk#sending-events-from-rudderstack).
45
56
 
57
+ ## Proxy configuration
58
+
59
+ Pass `proxies` when you create a `Client`. A Requests-style mapping is preferred:
60
+
61
+ ```python
62
+ from rudderstack.analytics.client import Client
63
+
64
+ client = Client(
65
+ 'SOURCE_WRITE_KEY',
66
+ host='https://YOUR_DATA_PLANE_URL',
67
+ proxies={
68
+ 'http': 'http://proxy.example:8080',
69
+ 'https': 'http://proxy.example:8080',
70
+ },
71
+ )
72
+ ```
73
+
74
+ A legacy string such as `proxy.example:8080` is also supported. The SDK applies
75
+ the string to both HTTP and HTTPS requests. If the string has no scheme, the SDK
76
+ adds `http://`. An existing scheme is preserved.
77
+
78
+ Configured proxies are now used for event requests. Earlier versions ignored
79
+ this option. Check that any existing proxy configuration is reachable before
80
+ upgrading.
81
+
46
82
  ## About RudderStack
47
83
 
48
84
  [**RudderStack**](https://rudderstack.com/) is a customer data platform for developers. Our tooling makes it easy to deploy pipelines that collect customer data from every app, website and SaaS platform, then activate it in your warehouse and business tools.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: rudder-sdk-python
3
- Version: 2.1.3
3
+ Version: 2.1.8
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
@@ -25,17 +25,27 @@ Requires-Python: >=3.8.0
25
25
  Description-Content-Type: text/markdown
26
26
  License-File: LICENSE.md
27
27
  Requires-Dist: requests<3.0,>=2.32.3
28
- Requires-Dist: monotonic<2.0,>=1.6
29
28
  Requires-Dist: backoff<3.0,>=2.2.1
30
29
  Requires-Dist: python-dateutil<3.0,>=2.2
31
30
  Requires-Dist: python-dotenv<2.0.0,>=1.0.1
32
31
  Requires-Dist: deprecation<3.0.0,>=2.1.0
33
32
  Provides-Extra: test
34
- Requires-Dist: mock==4.0.3; extra == "test"
35
- Requires-Dist: flake8==7.1.0; extra == "test"
33
+ Requires-Dist: mock==5.1.0; extra == "test"
34
+ Requires-Dist: flake8==7.1.1; extra == "test"
35
+
36
+ <p align="center">
37
+ <a href="https://rudderstack.com/">
38
+ <picture>
39
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/rudderlabs/rudder-sdk-js/develop/assets/rs-logo-full-dark.png">
40
+ <img alt="RudderStack" width="512" src="https://raw.githubusercontent.com/rudderlabs/rudder-sdk-js/develop/assets/rs-logo-full-light.jpg">
41
+ </picture>
42
+ </a>
43
+ </p>
36
44
 
37
45
  # RudderStack Python SDK
38
46
 
47
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/rudderlabs/rudder-sdk-python)
48
+
39
49
  RudderStack’s Python SDK lets you track events from your Python application. Once enabled, the event requests hit the RudderStack servers. RudderStack then transforms and routes these events to your specified destination platforms.
40
50
 
41
51
  More details on the Python SDK can be found in our [**documentation**](https://docs.rudderstack.com/stream-sources/rudderstack-sdk-integration-guides/rudderstack-python-sdk)
@@ -79,6 +89,31 @@ analytics.track('user_id', 'Simple Track Event', anonymous_id='anonymousId',
79
89
 
80
90
  For more information on the supported calls, refer to the [**documentation**](https://docs.rudderstack.com/stream-sources/rudderstack-sdk-integration-guides/rudderstack-python-sdk#sending-events-from-rudderstack).
81
91
 
92
+ ## Proxy configuration
93
+
94
+ Pass `proxies` when you create a `Client`. A Requests-style mapping is preferred:
95
+
96
+ ```python
97
+ from rudderstack.analytics.client import Client
98
+
99
+ client = Client(
100
+ 'SOURCE_WRITE_KEY',
101
+ host='https://YOUR_DATA_PLANE_URL',
102
+ proxies={
103
+ 'http': 'http://proxy.example:8080',
104
+ 'https': 'http://proxy.example:8080',
105
+ },
106
+ )
107
+ ```
108
+
109
+ A legacy string such as `proxy.example:8080` is also supported. The SDK applies
110
+ the string to both HTTP and HTTPS requests. If the string has no scheme, the SDK
111
+ adds `http://`. An existing scheme is preserved.
112
+
113
+ Configured proxies are now used for event requests. Earlier versions ignored
114
+ this option. Check that any existing proxy configuration is reachable before
115
+ upgrading.
116
+
82
117
  ## About RudderStack
83
118
 
84
119
  [**RudderStack**](https://rudderstack.com/) is a customer data platform for developers. Our tooling makes it easy to deploy pipelines that collect customer data from every app, website and SaaS platform, then activate it in your warehouse and business tools.
@@ -20,5 +20,8 @@ rudderstack/analytics/test/test_client.py
20
20
  rudderstack/analytics/test/test_constants.py
21
21
  rudderstack/analytics/test/test_consumer.py
22
22
  rudderstack/analytics/test/test_module.py
23
+ rudderstack/analytics/test/test_public_api_contract.py
24
+ rudderstack/analytics/test/test_release_validation.py
23
25
  rudderstack/analytics/test/test_request.py
26
+ rudderstack/analytics/test/test_request_options.py
24
27
  rudderstack/analytics/test/test_utils.py
@@ -1,10 +1,9 @@
1
1
  requests<3.0,>=2.32.3
2
- monotonic<2.0,>=1.6
3
2
  backoff<3.0,>=2.2.1
4
3
  python-dateutil<3.0,>=2.2
5
4
  python-dotenv<2.0.0,>=1.0.1
6
5
  deprecation<3.0.0,>=2.1.0
7
6
 
8
7
  [test]
9
- mock==4.0.3
10
- flake8==7.1.0
8
+ mock==5.1.0
9
+ flake8==7.1.1
@@ -90,8 +90,7 @@ def join():
90
90
 
91
91
  def shutdown():
92
92
  """Flush all messages and cleanly shutdown the client"""
93
- _proxy('flush')
94
- _proxy('join')
93
+ _proxy('shutdown')
95
94
 
96
95
 
97
96
  def _proxy(method, *args, **kwargs):
@@ -1,4 +1,4 @@
1
- from datetime import datetime
1
+ from datetime import datetime, timezone
2
2
  from uuid import uuid4
3
3
  import logging
4
4
  import numbers
@@ -245,7 +245,7 @@ class Client(object):
245
245
  """Push a new `msg` onto the queue, return `(success, msg)`"""
246
246
  timestamp = msg['timestamp']
247
247
  if timestamp is None:
248
- timestamp = datetime.utcnow().replace(tzinfo=tzutc())
248
+ timestamp = datetime.now(timezone.utc).replace(tzinfo=tzutc())
249
249
  message_id = msg.get('messageId')
250
250
  if message_id is None:
251
251
  message_id = uuid4()
@@ -1,6 +1,6 @@
1
1
  import logging
2
+ import time
2
3
  from threading import Thread
3
- import monotonic
4
4
  import backoff
5
5
  import json
6
6
 
@@ -8,11 +8,10 @@ from rudderstack.analytics.request import post, APIError, DatetimeSerializer
8
8
 
9
9
  from queue import Empty
10
10
 
11
- MAX_MSG_SIZE = 32 << 10
12
-
13
- # Our servers only accept batches less than 500KB. Here limit is set slightly
11
+ # Our servers only accept payloads within this limit. Here limit is set slightly
14
12
  # lower to leave space for extra data that will be added later, eg. "sentAt".
15
- BATCH_SIZE_LIMIT = 475000
13
+ MAX_MSG_SIZE = 4 << 10 << 10 ## 4 mb
14
+ BATCH_SIZE_LIMIT = 4 << 10 << 10 ## 4 mb
16
15
 
17
16
 
18
17
  class Consumer(Thread):
@@ -80,11 +79,11 @@ class Consumer(Thread):
80
79
  queue = self.queue
81
80
  items = []
82
81
 
83
- start_time = monotonic.monotonic()
82
+ start_time = time.monotonic()
84
83
  total_size = 0
85
84
 
86
85
  while len(items) < self.upload_size:
87
- elapsed = monotonic.monotonic() - start_time
86
+ elapsed = time.monotonic() - start_time
88
87
  if elapsed >= self.upload_interval:
89
88
  break
90
89
  try:
@@ -94,7 +93,7 @@ class Consumer(Thread):
94
93
  item, cls=DatetimeSerializer).encode())
95
94
  if item_size > MAX_MSG_SIZE:
96
95
  self.log.error(
97
- 'Item exceeds 32kb limit, dropping. (%s)', str(item))
96
+ 'Item exceeds 4mb limit, dropping. (%s)', str(item))
98
97
  continue
99
98
  items.append(item)
100
99
  total_size += item_size
@@ -1,4 +1,4 @@
1
- from datetime import date, datetime
1
+ from datetime import date, datetime, timezone
2
2
  from io import BytesIO
3
3
  from gzip import GzipFile
4
4
  import logging
@@ -17,7 +17,7 @@ def post(write_key, host=None, gzip=True, timeout=15, proxies=None, **kwargs):
17
17
  """Post the `kwargs` to the API"""
18
18
  log = logging.getLogger('rudderstack')
19
19
  body = kwargs
20
- body["sentAt"] = datetime.utcnow().replace(tzinfo=tzutc()).isoformat()
20
+ body["sentAt"] = datetime.now(timezone.utc).replace(tzinfo=tzutc()).isoformat()
21
21
  url = remove_trailing_slash(host or 'https://api.rudderstack.com') + '/v1/batch'
22
22
  auth = HTTPBasicAuth(write_key, '')
23
23
  data = json.dumps(body, cls=DatetimeSerializer)
@@ -34,14 +34,17 @@ def post(write_key, host=None, gzip=True, timeout=15, proxies=None, **kwargs):
34
34
  "data": data,
35
35
  "auth": auth,
36
36
  "headers": headers,
37
- "timeout": 15,
37
+ "timeout": timeout,
38
38
  }
39
39
 
40
40
  if proxies:
41
+ # Older SDK versions accepted a single proxy string.
42
+ if isinstance(proxies, str):
43
+ proxy_url = proxies if '://' in proxies else 'http://' + proxies
44
+ proxies = {'http': proxy_url, 'https': proxy_url}
41
45
  kwargs['proxies'] = proxies
42
46
 
43
- res = _session.post(url, data=data, auth=auth,
44
- headers=headers, timeout=timeout)
47
+ res = _session.post(url, **kwargs)
45
48
 
46
49
  if res.status_code == 200:
47
50
  log.debug('data uploaded successfully')
@@ -325,10 +325,14 @@ class TestClient(unittest.TestCase):
325
325
 
326
326
  def test_user_defined_upload_size(self):
327
327
  client = Client(write_key = TEST_WRITE_KEY, on_error=self.fail,
328
- upload_size=10, upload_interval=3)
329
- print("write_key", TEST_WRITE_KEY)
328
+ upload_size=10, upload_interval=0.01)
329
+ consumer = client.consumers[0]
330
+ consumer.pause()
331
+ consumer.join()
332
+
333
+ batch_sizes = []
330
334
  def mock_post_fn(*args, **kwargs):
331
- self.assertEqual(len(kwargs['batch']), 10)
335
+ batch_sizes.append(len(kwargs['batch']))
332
336
 
333
337
  # the post function should be called 2 times, with a batch size of 10
334
338
  # each time.
@@ -336,8 +340,10 @@ class TestClient(unittest.TestCase):
336
340
  as mock_post:
337
341
  for _ in range(20):
338
342
  client.identify('userId', {'trait': 'value'})
339
- time.sleep(1)
343
+ consumer.upload()
344
+ consumer.upload()
340
345
  self.assertEqual(mock_post.call_count, 2)
346
+ self.assertEqual(batch_sizes, [10, 10])
341
347
 
342
348
  def test_user_defined_timeout(self):
343
349
  client = Client(TEST_WRITE_KEY, timeout=10)
@@ -350,6 +356,6 @@ class TestClient(unittest.TestCase):
350
356
  self.assertEqual(consumer.timeout, 15)
351
357
 
352
358
  def test_proxies(self):
353
- client = Client(TEST_WRITE_KEY, proxies=TEST_PROXY)
354
- success, msg = client.identify('userId', {'trait': 'value'})
355
- self.assertTrue(success)
359
+ client = Client(TEST_WRITE_KEY, proxies=TEST_PROXY, send=False)
360
+
361
+ self.assertEqual(client.consumers[0].proxies, TEST_PROXY)
@@ -0,0 +1,4 @@
1
+ TEST_PROXY = {
2
+ 'http': 'http://proxy.example:8080',
3
+ 'https': 'http://proxy.example:8080',
4
+ }
@@ -196,7 +196,7 @@ class TestConsumer(unittest.TestCase):
196
196
  }
197
197
  msg_size = len(json.dumps(track).encode())
198
198
  # number of messages in a maximum-size batch
199
- n_msgs = int(475000 / msg_size)
199
+ n_msgs = int((4 << 10 << 10) / msg_size)
200
200
 
201
201
  def mock_post_fn(_, data, **kwargs):
202
202
  res = mock.Mock()
@@ -208,14 +208,17 @@ class TestConsumer(unittest.TestCase):
208
208
 
209
209
  with mock.patch('rudderstack.analytics.request._session.post',
210
210
  side_effect=mock_post_fn) as mock_post:
211
- consumer.start()
212
211
  for _ in range(0, n_msgs + 2):
213
212
  q.put(track)
213
+ consumer.start()
214
214
  q.join()
215
+ consumer.pause()
216
+ consumer.join()
215
217
  self.assertEqual(mock_post.call_count, 2)
216
218
 
217
- @classmethod
218
- def test_proxies(cls):
219
+ @mock.patch('rudderstack.analytics.request._session.post')
220
+ def test_proxies(self, session_post):
221
+ session_post.return_value = mock.Mock(status_code=200)
219
222
  consumer = Consumer(None, host=TEST_DATA_PLANE_URL,
220
223
  write_key=TEST_WRITE_KEY, proxies=TEST_PROXY)
221
224
  track = {
@@ -224,3 +227,6 @@ class TestConsumer(unittest.TestCase):
224
227
  'userId': 'userId'
225
228
  }
226
229
  consumer.request([track])
230
+
231
+ _, kwargs = session_post.call_args
232
+ self.assertEqual(kwargs['proxies'], TEST_PROXY)
@@ -0,0 +1,96 @@
1
+ """Compatibility checks for the existing module and Client APIs."""
2
+
3
+ import unittest
4
+ from unittest import mock
5
+
6
+ import rudderstack.analytics as analytics
7
+ from rudderstack.analytics.client import Client
8
+
9
+
10
+ EVENT_CALLS = (
11
+ ('track', ('user-id', 'test event')),
12
+ ('identify', ('user-id',)),
13
+ ('group', ('user-id', 'group-id')),
14
+ ('alias', ('previous-id', 'user-id')),
15
+ ('page', ('user-id',)),
16
+ ('screen', ('user-id',)),
17
+ )
18
+
19
+
20
+ class TestPublicApiContract(unittest.TestCase):
21
+ def test_module_discards_client_results(self):
22
+ client = mock.Mock()
23
+ with mock.patch.object(analytics, 'default_client', client):
24
+ for method, args in EVENT_CALLS:
25
+ with self.subTest(method=method):
26
+ getattr(client, method).return_value = mock.sentinel.result
27
+ self.assertIsNone(getattr(analytics, method)(
28
+ *args, message_id='message-id'))
29
+ getattr(client, method).assert_called_once_with(
30
+ *args, message_id='message-id')
31
+ for method in ('flush', 'join', 'shutdown'):
32
+ with self.subTest(method=method):
33
+ getattr(client, method).return_value = mock.sentinel.result
34
+ self.assertIsNone(getattr(analytics, method)())
35
+
36
+ def test_module_returns_none_when_queue_accepts_or_rejects_event(self):
37
+ for method, args in EVENT_CALLS:
38
+ with self.subTest(method=method):
39
+ client = Client('test-key', send=False, max_queue_size=1)
40
+ # Enable enqueueing without starting a background consumer.
41
+ client.send = True
42
+ with mock.patch.object(analytics, 'default_client', client):
43
+ self.assertIsNone(getattr(analytics, method)(*args))
44
+ self.assertEqual(client.queue.qsize(), 1)
45
+ self.assertIsNone(getattr(analytics, method)(*args))
46
+ self.assertEqual(client.queue.qsize(), 1)
47
+
48
+ def test_client_returns_success_and_message(self):
49
+ for method, args in EVENT_CALLS:
50
+ with self.subTest(method=method):
51
+ client = Client('test-key', send=False)
52
+ result = getattr(client, method)(
53
+ *args, message_id='message-id')
54
+ self.assertIsInstance(result, tuple)
55
+ self.assertEqual(len(result), 2)
56
+ success, message = result
57
+ self.assertIs(success, True)
58
+ self.assertEqual(message['type'], method)
59
+ self.assertEqual(message['messageId'], 'message-id')
60
+
61
+ def test_client_returns_false_and_message_when_queue_is_full(self):
62
+ for method, args in EVENT_CALLS:
63
+ with self.subTest(method=method):
64
+ client = Client('test-key', send=False, max_queue_size=1)
65
+ client.send = True
66
+ success, message = getattr(client, method)(*args)
67
+ self.assertIs(success, True)
68
+ self.assertEqual(client.queue.get_nowait(), message)
69
+ client.queue.task_done()
70
+ client.queue.put_nowait(message)
71
+ result = getattr(client, method)(*args)
72
+ self.assertIsInstance(result, tuple)
73
+ self.assertEqual(len(result), 2)
74
+ success, message = result
75
+ self.assertIs(success, False)
76
+ self.assertEqual(message['type'], method)
77
+
78
+ def test_module_preserves_validation_errors(self):
79
+ client = Client('test-key', send=False)
80
+ with mock.patch.object(analytics, 'default_client', client):
81
+ with self.assertRaises(AssertionError):
82
+ analytics.track('user-id', event=123)
83
+
84
+ def test_client_lifecycle_returns_none(self):
85
+ client = Client('test-key', send=False)
86
+ for method in ('flush', 'join', 'shutdown'):
87
+ with self.subTest(method=method):
88
+ self.assertIsNone(getattr(client, method)())
89
+
90
+
91
+ class TestShutdownDelegation(unittest.TestCase):
92
+ def test_module_calls_client_shutdown_once(self):
93
+ client = mock.Mock()
94
+ with mock.patch.object(analytics, 'default_client', client):
95
+ self.assertIsNone(analytics.shutdown())
96
+ self.assertEqual(client.mock_calls, [mock.call.shutdown()])
@@ -0,0 +1,62 @@
1
+ import os
2
+ import runpy
3
+ import subprocess
4
+ import textwrap
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+
10
+ REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
11
+ PUBLISH_WORKFLOW = REPOSITORY_ROOT / '.github' / 'workflows' / 'publish-pypi.yml'
12
+ VERSION_FILE = REPOSITORY_ROOT / 'rudderstack' / 'analytics' / 'version.py'
13
+ VERSION = runpy.run_path(str(VERSION_FILE))['VERSION']
14
+
15
+
16
+ def release_validation_script():
17
+ workflow = PUBLISH_WORKFLOW.read_text(encoding='utf-8')
18
+ validation_step = workflow.split(
19
+ ' - name: Validate release tag and package version\n', 1
20
+ )[1].split('\n - name:', 1)[0]
21
+ script = validation_step.split(' run: |\n', 1)[1]
22
+ return textwrap.dedent(script)
23
+
24
+
25
+ def run_validation(release_tag, check_syntax=False):
26
+ command = ['bash', '-n'] if check_syntax else ['bash']
27
+ return subprocess.run(
28
+ command,
29
+ check=False,
30
+ capture_output=True,
31
+ cwd=REPOSITORY_ROOT,
32
+ env={**os.environ, 'RELEASE_TAG': release_tag},
33
+ input=release_validation_script(),
34
+ text=True,
35
+ )
36
+
37
+
38
+ def test_release_validation_script_has_valid_shell_syntax():
39
+ result = run_validation('v{}'.format(VERSION), check_syntax=True)
40
+
41
+ assert result.returncode == 0
42
+
43
+
44
+ def test_current_package_version_matches_release_tag():
45
+ result = run_validation('v{}'.format(VERSION))
46
+
47
+ assert result.returncode == 0
48
+
49
+
50
+ @pytest.mark.parametrize('release_tag', ['2.1.7', 'v2.1', 'release-v2.1.7'])
51
+ def test_invalid_release_tag_is_rejected(release_tag):
52
+ result = run_validation(release_tag)
53
+
54
+ assert result.returncode == 1
55
+ assert 'Invalid release tag' in result.stderr
56
+
57
+
58
+ def test_release_tag_must_match_package_version():
59
+ result = run_validation('v999.0.0')
60
+
61
+ assert result.returncode == 1
62
+ assert 'does not match package version' in result.stderr
@@ -1,5 +1,6 @@
1
1
  from datetime import datetime, date
2
2
  import unittest
3
+ from unittest import mock
3
4
  import json
4
5
  import requests
5
6
 
@@ -67,11 +68,15 @@ class TestRequests(unittest.TestCase):
67
68
  data = json.dumps(body, cls=DatetimeSerializer)
68
69
  self.assertTrue(len(data) > len(_gzip_json(data = data)))
69
70
 
70
- def test_proxies(self):
71
- res = post(TEST_WRITE_KEY,host=TEST_DATA_PLANE_URL, batch=[{
72
- 'userId': 'userId',
73
- 'event': 'python event',
74
- 'type': 'track',
75
- 'proxies': TEST_PROXY
76
- }])
77
- self.assertEqual(res.status_code, 200)
71
+ @mock.patch('rudderstack.analytics.request._session.post')
72
+ def test_passes_proxy_and_timeout_to_session(self, session_post):
73
+ response = mock.Mock(status_code=200)
74
+ session_post.return_value = response
75
+
76
+ self.assertIs(
77
+ post(TEST_WRITE_KEY, host=TEST_DATA_PLANE_URL, proxies=TEST_PROXY,
78
+ timeout=7.5, batch=[]),
79
+ response)
80
+ _, kwargs = session_post.call_args
81
+ self.assertEqual(kwargs['proxies'], TEST_PROXY)
82
+ self.assertEqual(kwargs['timeout'], 7.5)
@@ -0,0 +1,136 @@
1
+ """Request option tests without credentials or external network access."""
2
+
3
+ from http.server import BaseHTTPRequestHandler, HTTPServer
4
+ import gzip
5
+ import json
6
+ import socket
7
+ from threading import Thread
8
+ import unittest
9
+ from unittest import mock
10
+
11
+ import requests
12
+
13
+ from rudderstack.analytics.client import Client
14
+ from rudderstack.analytics.consumer import Consumer
15
+ from rudderstack.analytics.request import post
16
+
17
+
18
+ class TestRequestOptions(unittest.TestCase):
19
+ def setUp(self):
20
+ patcher = mock.patch('rudderstack.analytics.request._session.post')
21
+ self.session_post = patcher.start()
22
+ self.addCleanup(patcher.stop)
23
+ self.session_post.return_value = mock.Mock(status_code=200)
24
+
25
+ def test_legacy_proxy_strings(self):
26
+ for value, expected in (
27
+ ('proxy.example:8080', 'http://proxy.example:8080'),
28
+ ('http://proxy.example:8080', 'http://proxy.example:8080'),
29
+ ('https://proxy.example:8080', 'https://proxy.example:8080'),
30
+ ('socks5://proxy.example:1080', 'socks5://proxy.example:1080'),
31
+ ):
32
+ with self.subTest(proxy=value):
33
+ post('test-key', proxies=value, batch=[])
34
+ self.assertEqual(self.session_post.call_args.kwargs['proxies'],
35
+ {'http': expected, 'https': expected})
36
+
37
+ def test_proxy_mapping_is_not_modified(self):
38
+ proxies = {'http': 'http://proxy.example:8080',
39
+ 'https': 'http://secure-proxy.example:8080'}
40
+ original = proxies.copy()
41
+ post('test-key', proxies=proxies, batch=[])
42
+ self.assertIs(self.session_post.call_args.kwargs['proxies'], proxies)
43
+ self.assertEqual(proxies, original)
44
+
45
+ def test_empty_proxy_configuration_is_omitted(self):
46
+ for proxies in (None, '', {}):
47
+ with self.subTest(proxies=proxies):
48
+ post('test-key', proxies=proxies, batch=[])
49
+ self.assertNotIn('proxies', self.session_post.call_args.kwargs)
50
+
51
+ def test_default_and_custom_timeouts(self):
52
+ post('test-key', batch=[])
53
+ self.assertEqual(self.session_post.call_args.kwargs['timeout'], 15)
54
+ post('test-key', timeout=7.5, batch=[])
55
+ self.assertEqual(self.session_post.call_args.kwargs['timeout'], 7.5)
56
+
57
+ def test_sync_client_forwards_legacy_proxy_and_timeout(self):
58
+ client = Client('test-key', sync_mode=True,
59
+ proxies='proxy.example:8080', timeout=7.5)
60
+ success, message = client.track('user-id', 'test event')
61
+ self.assertIs(success, True)
62
+ self.assertEqual(message['type'], 'track')
63
+ self.assertEqual(self.session_post.call_args.kwargs['proxies'],
64
+ {'http': 'http://proxy.example:8080',
65
+ 'https': 'http://proxy.example:8080'})
66
+ self.assertEqual(self.session_post.call_args.kwargs['timeout'], 7.5)
67
+
68
+ def test_consumer_forwards_legacy_proxy_and_timeout(self):
69
+ consumer = Consumer(None, 'test-key', proxies='proxy.example:8080',
70
+ timeout=7.5, retries=0)
71
+ consumer.request([{'type': 'track', 'event': 'test event'}])
72
+ self.assertEqual(self.session_post.call_args.kwargs['proxies'],
73
+ {'http': 'http://proxy.example:8080',
74
+ 'https': 'http://proxy.example:8080'})
75
+ self.assertEqual(self.session_post.call_args.kwargs['timeout'], 7.5)
76
+
77
+
78
+ class TestProxyTransport(unittest.TestCase):
79
+ def test_real_requests_routes_through_proxy(self):
80
+ received = []
81
+ getaddrinfo = socket.getaddrinfo
82
+
83
+ def local_only(host, *args, **kwargs):
84
+ self.assertEqual(host, '127.0.0.1',
85
+ 'Unexpected external connection')
86
+ return getaddrinfo(host, *args, **kwargs)
87
+
88
+ patcher = mock.patch('socket.getaddrinfo', side_effect=local_only)
89
+ patcher.start()
90
+ self.addCleanup(patcher.stop)
91
+
92
+ class ProxyHandler(BaseHTTPRequestHandler):
93
+ def do_POST(self):
94
+ body = self.rfile.read(int(self.headers['Content-Length']))
95
+ if self.headers.get('Content-Encoding') == 'gzip':
96
+ body = gzip.decompress(body)
97
+ received.append((self.path, json.loads(body)))
98
+ self.send_response(200)
99
+ self.end_headers()
100
+ self.wfile.write(b'{}')
101
+
102
+ def log_message(self, *args):
103
+ pass
104
+
105
+ server = HTTPServer(('127.0.0.1', 0), ProxyHandler)
106
+ thread = Thread(target=server.serve_forever,
107
+ kwargs={'poll_interval': 0.01}, daemon=True)
108
+ thread.start()
109
+ try:
110
+ address = '127.0.0.1:{}'.format(server.server_port)
111
+ with requests.Session() as session:
112
+ session.trust_env = False
113
+ with mock.patch('rudderstack.analytics.request._session',
114
+ session):
115
+ for proxies in (address, 'http://' + address,
116
+ {'http': 'http://' + address}):
117
+ for compressed in (False, True):
118
+ with self.subTest(proxies=proxies,
119
+ gzip=compressed):
120
+ response = post(
121
+ 'test-key', host='http://data.example',
122
+ proxies=proxies, timeout=2,
123
+ gzip=compressed, batch=[])
124
+ self.assertEqual(response.status_code, 200)
125
+ path, payload = received[-1]
126
+ self.assertEqual(
127
+ path, 'http://data.example/v1/batch')
128
+ self.assertEqual(payload['batch'], [])
129
+ self.assertIn('sentAt', payload)
130
+ self.assertNotIn('proxies', payload)
131
+ self.assertNotIn('timeout', payload)
132
+ self.assertEqual(len(received), 6)
133
+ finally:
134
+ server.shutdown()
135
+ thread.join(timeout=2)
136
+ server.server_close()
@@ -1,4 +1,4 @@
1
- from datetime import date, datetime, timedelta
1
+ from datetime import date, datetime, timedelta, timezone
2
2
  from decimal import Decimal
3
3
  import unittest
4
4
 
@@ -51,7 +51,7 @@ class TestUtils(unittest.TestCase):
51
51
  def test_clean_with_dates(self):
52
52
  dict_with_dates = {
53
53
  'birthdate': date(1980, 1, 1),
54
- 'registration': datetime.utcnow(),
54
+ 'registration': datetime.now(timezone.utc),
55
55
  }
56
56
  self.assertEqual(dict_with_dates, utils.clean(dict_with_dates))
57
57
 
@@ -0,0 +1 @@
1
+ VERSION = '2.1.8' # x-release-please-version
@@ -10,7 +10,6 @@ long_description = (Path(__file__).resolve().parent / 'README.md').read_text(enc
10
10
 
11
11
  install_requires = [
12
12
  "requests>=2.32.3,<3.0",
13
- "monotonic>=1.6,<2.0",
14
13
  "backoff>=2.2.1,<3.0",
15
14
  "python-dateutil>=2.2,<3.0",
16
15
  "python-dotenv>=1.0.1,<2.0.0",
@@ -18,8 +17,8 @@ install_requires = [
18
17
  ]
19
18
 
20
19
  tests_require = [
21
- "mock==4.0.3",
22
- "flake8==7.1.0",
20
+ "mock==5.1.0",
21
+ "flake8==7.1.1",
23
22
  ]
24
23
 
25
24
  setup(
@@ -30,7 +29,6 @@ setup(
30
29
  author_email='sdk@rudderstack.com',
31
30
  maintainer='RudderStack',
32
31
  maintainer_email='sdk@rudderstack.com',
33
- test_suite='rudderstack.analytics.test.all',
34
32
  packages=['rudderstack.analytics', 'rudderstack.analytics.test'],
35
33
  python_requires='>=3.8.0',
36
34
  license='MIT License',
@@ -1 +0,0 @@
1
- TEST_PROXY = '203.243.63.16:80'
@@ -1 +0,0 @@
1
- VERSION = '2.1.3'