diff --git a/.github/workflows/publish-to-pypi.yml b/.github/workflows/publish-to-pypi.yml index b612e80..62d0a39 100644 --- a/.github/workflows/publish-to-pypi.yml +++ b/.github/workflows/publish-to-pypi.yml @@ -30,10 +30,10 @@ jobs: with: fetch-depth: 0 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v3 with: - python-version: 3.7 + python-version: 3.8 - name: Run Test run: python -m unittest discover -s ./src -p 'test_*.py' diff --git a/.github/workflows/publish-to-test-pypi.yml b/.github/workflows/publish-to-test-pypi.yml index 62538dd..1bdcca0 100644 --- a/.github/workflows/publish-to-test-pypi.yml +++ b/.github/workflows/publish-to-test-pypi.yml @@ -21,10 +21,10 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.8 uses: actions/setup-python@v3 with: - python-version: 3.7 + python-version: 3.8 - name: Install dependencies run: python -m pip install build setuptools wheel twine diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45f4a47..818e564 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ "3.7", "3.8", "3.9", "3.10", "3.11" ] + python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ] steps: - name: Checkout source code uses: actions/checkout@v3 diff --git a/src/amplitude/constants.py b/src/amplitude/constants.py index 0eda78d..c3565a9 100644 --- a/src/amplitude/constants.py +++ b/src/amplitude/constants.py @@ -40,6 +40,7 @@ REVENUE_RECEIPT = "$receipt" REVENUE_RECEIPT_SIG = "$receiptSig" REVENUE = "$revenue" +REVENUE_CURRENCY = "$currency" AMP_REVENUE_EVENT = "revenue_amount" MAX_PROPERTY_KEYS = 1024 diff --git a/src/amplitude/event.py b/src/amplitude/event.py index 2d265d0..1d0d5b8 100644 --- a/src/amplitude/event.py +++ b/src/amplitude/event.py @@ -145,6 +145,7 @@ def get_body(self): "price": ["price", float], "quantity": ["quantity", int], "revenue": ["revenue", float], + "currency": ["currency", str], "product_id": ["productId", str], "revenue_type": ["revenueType", str], "location_lat": ["location_lat", float], @@ -240,6 +241,7 @@ def __init__(self, user_id: Optional[str] = None, revenue: Optional[float] = None, product_id: Optional[str] = None, revenue_type: Optional[str] = None, + currency: Optional[str] = None, location_lat: Optional[float] = None, location_lng: Optional[float] = None, ip: Optional[str] = None, @@ -277,6 +279,7 @@ def __init__(self, user_id: Optional[str] = None, self.revenue: Optional[str] = None self.product_id: Optional[str] = None self.revenue_type: Optional[str] = None + self.currency: Optional[str] = None self.location_lat: Optional[float] = None self.location_lng: Optional[float] = None self.ip: Optional[str] = None @@ -313,6 +316,7 @@ def __init__(self, user_id: Optional[str] = None, self["revenue"] = revenue self["product_id"] = product_id self["revenue_type"] = revenue_type + self["currency"] = currency self["location_lat"] = location_lat self["location_lng"] = location_lng self["ip"] = ip @@ -484,6 +488,7 @@ def __init__(self, event_type: str, revenue: Optional[float] = None, product_id: Optional[str] = None, revenue_type: Optional[str] = None, + currency: Optional[str] = None, location_lat: Optional[float] = None, location_lng: Optional[float] = None, ip: Optional[str] = None, @@ -520,6 +525,7 @@ def __init__(self, event_type: str, revenue=revenue, product_id=product_id, revenue_type=revenue_type, + currency=currency, location_lat=location_lat, location_lng=location_lng, ip=ip, @@ -1042,7 +1048,8 @@ def __init__(self, price: float, receipt: Optional[str] = None, receipt_sig: Optional[str] = None, properties: Optional[dict] = None, - revenue: Optional[float] = None): + revenue: Optional[float] = None, + currency: Optional[str] = None): """The constructor of the Revenue class""" self.price: float = price self.quantity: int = quantity @@ -1052,6 +1059,7 @@ def __init__(self, price: float, self.receipt_sig: Optional[str] = receipt_sig self.properties: Optional[dict] = properties self.revenue: Optional[float] = revenue + self.currency: Optional[str] = currency def set_receipt(self, receipt: str, receipt_signature: str): """Set the receipt and signature @@ -1098,7 +1106,8 @@ def get_event_properties(self): constants.REVENUE_TYPE: self.revenue_type, constants.REVENUE_RECEIPT: self.receipt, constants.REVENUE_RECEIPT_SIG: self.receipt_sig, - constants.REVENUE: self.revenue}) + constants.REVENUE: self.revenue, + constants.REVENUE_CURRENCY: self.currency}) return {key: value for key, value in event_properties.items() if value is not None} @@ -1178,6 +1187,7 @@ def __init__(self, user_id: Optional[str] = None, revenue: Optional[float] = None, product_id: Optional[str] = None, revenue_type: Optional[str] = None, + currency: Optional[str] = None, location_lat: Optional[float] = None, location_lng: Optional[float] = None, ip: Optional[str] = None, @@ -1219,6 +1229,7 @@ def __init__(self, user_id: Optional[str] = None, revenue=revenue, product_id=product_id, revenue_type=revenue_type, + currency=currency, location_lat=location_lat, location_lng=location_lng, ip=ip, diff --git a/src/test/test_client.py b/src/test/test_client.py index 1232b20..0224188 100644 --- a/src/test/test_client.py +++ b/src/test/test_client.py @@ -30,10 +30,15 @@ def test_amplitude_client_track_success(self): res = Response(HttpStatus.SUCCESS) post_method.return_value = res events = [] + self.assertion_errors = [] def callback_func(event, code, message=None): - self.assertEqual(200, code) - events.append(event.event_properties["id"]) + try: + self.assertEqual(200, code) + events.append(event.event_properties["id"]) + self.assertEqual('USD', event.currency) + except AssertionError as e: + self.assertion_errors.append(str(e)) self.client.configuration.callback = callback_func for use_batch in (True, False): @@ -42,12 +47,14 @@ def callback_func(event, code, message=None): events.clear() self.client.configuration.use_batch = use_batch for i in range(25): - self.client.track(BaseEvent("test_event", "test_user_id", event_properties={"id": i})) + self.client.track(BaseEvent("test_event", "test_user_id", + event_properties={"id": i}, currency='USD')) for flush_future in self.client.flush(): if flush_future: flush_future.result() self.assertEqual(25, len(events)) post_method.assert_called() + self.assertEqual(0, len(self.assertion_errors)) def test_amplitude_client_track_invalid_api_key_log_error(self): post_method = MagicMock() @@ -84,13 +91,17 @@ def test_amplitude_client_track_invalid_response_then_success_response(self): } success_res = Response(HttpStatus.SUCCESS) events = [] + self.assertion_errors = [] def callback_func(event, code, message=None): - if event.event_properties["id"] in (1, 2, 5, 6, 8): - self.assertEqual(400, code) - else: - self.assertEqual(200, code) - events.append((event.event_properties["id"], event.retry)) + try: + if event.event_properties["id"] in (1, 2, 5, 6, 8): + self.assertEqual(400, code) + else: + self.assertEqual(200, code) + events.append((event.event_properties["id"], event.retry)) + except AssertionError as e: + self.assertion_errors.append(str(e)) self.client.configuration.callback = callback_func for use_batch in (True, False): @@ -107,20 +118,25 @@ def callback_func(event, code, message=None): self.assertEqual(2, post_method.call_count) self.assertEqual([(1, 0), (2, 0), (5, 0), (6, 0), (8, 0), (0, 1), (3, 1), (4, 1), (7, 1), (9, 1)], events) + self.assertEqual(0, len(self.assertion_errors)) def test_amplitude_client_identify_invalid_log_error_then_success(self): post_method = MagicMock() HttpClient.post = post_method res = Response(HttpStatus.SUCCESS) post_method.return_value = res + self.assertion_errors = [] def callback_func(event, code, message=None): - self.assertEqual(200, code) - self.assertTrue(isinstance(event, IdentifyEvent)) - self.assertTrue("user_properties" in event) - self.assertEqual("$identify", event["event_type"]) - self.assertEqual("test_user_id", event["user_id"]) - self.assertEqual("test_device_id", event["device_id"]) + try: + self.assertEqual(200, code) + self.assertTrue(isinstance(event, IdentifyEvent)) + self.assertTrue("user_properties" in event) + self.assertEqual("$identify", event["event_type"]) + self.assertEqual("test_user_id", event["user_id"]) + self.assertEqual("test_device_id", event["device_id"]) + except AssertionError as e: + self.assertion_errors.append(str(e)) self.client.configuration.callback = callback_func for use_batch in (True, False): @@ -142,21 +158,26 @@ def callback_func(event, code, message=None): if flush_future: flush_future.result() post_method.assert_called_once() + self.assertEqual(0, len(self.assertion_errors)) def test_amplitude_client_group_identify_invalid_log_error_then_success(self): post_method = MagicMock() HttpClient.post = post_method res = Response(HttpStatus.SUCCESS) post_method.return_value = res + self.assertion_errors = [] def callback_func(event, code, message=None): - self.assertEqual(200, code) - self.assertTrue(isinstance(event, GroupIdentifyEvent)) - self.assertTrue("group_properties" in event) - self.assertEqual("$groupidentify", event["event_type"]) - self.assertEqual("test_user_id", event["user_id"]) - self.assertEqual("test_device_id", event["device_id"]) - self.assertEqual({"Sports": "Football"}, event["groups"]) + try: + self.assertEqual(200, code) + self.assertTrue(isinstance(event, GroupIdentifyEvent)) + self.assertTrue("group_properties" in event) + self.assertEqual("$groupidentify", event["event_type"]) + self.assertEqual("test_user_id", event["user_id"]) + self.assertEqual("test_device_id", event["device_id"]) + self.assertEqual({"Sports": "Football"}, event["groups"]) + except AssertionError as e: + self.assertion_errors.append(str(e)) self.client.configuration.callback = callback_func for use_batch in (True, False): @@ -179,22 +200,27 @@ def callback_func(event, code, message=None): if flush_future: flush_future.result() post_method.assert_called_once() + self.assertEqual(0, len(self.assertion_errors)) def test_amplitude_set_group_success(self): post_method = MagicMock() HttpClient.post = post_method res = Response(HttpStatus.SUCCESS) post_method.return_value = res + self.assertion_errors = [] def callback_func(event, code, message=None): - self.assertEqual(200, code) - self.assertTrue(isinstance(event, IdentifyEvent)) - self.assertTrue("groups" in event) - self.assertEqual("$identify", event["event_type"]) - self.assertEqual("test_user_id", event["user_id"]) - self.assertEqual("test_device_id", event["device_id"]) - self.assertEqual({"type": ["test_group", "test_group_2"]}, event.groups) - self.assertEqual({"$set": {"type": ["test_group", "test_group_2"]}}, event.user_properties) + try: + self.assertEqual(200, code) + self.assertTrue(isinstance(event, IdentifyEvent)) + self.assertTrue("groups" in event) + self.assertEqual("$identify", event["event_type"]) + self.assertEqual("test_user_id", event["user_id"]) + self.assertEqual("test_device_id", event["device_id"]) + self.assertEqual({"type": ["test_group", "test_group_2"]}, event.groups) + self.assertEqual({"$set": {"type": ["test_group", "test_group_2"]}}, event.user_properties) + except AssertionError as e: + self.assertion_errors.append(str(e)) self.client.configuration.callback = callback_func for use_batch in (True, False): @@ -207,30 +233,35 @@ def callback_func(event, code, message=None): if flush_future: flush_future.result() post_method.assert_called_once() + self.assertEqual(0, len(self.assertion_errors)) def test_amplitude_client_revenue_invalid_log_error_then_success(self): post_method = MagicMock() HttpClient.post = post_method res = Response(HttpStatus.SUCCESS) post_method.return_value = res + self.assertion_errors = [] def callback_func(event, code, message=None): - self.assertEqual(200, code) - self.assertTrue(isinstance(event, RevenueEvent)) - self.assertTrue("event_properties" in event) - self.assertEqual("revenue_amount", event["event_type"]) - self.assertEqual("test_user_id", event["user_id"]) - self.assertEqual("test_device_id", event["device_id"]) - self.assertEqual({'$price': 60.2, '$productId': 'P63', '$quantity': 3, '$receipt': 'A0001', - '$receiptSig': '0001A', 'other_property': 'test'}, - event["event_properties"]) + try: + self.assertEqual(200, code) + self.assertTrue(isinstance(event, RevenueEvent)) + self.assertTrue("event_properties" in event) + self.assertEqual("revenue_amount", event["event_type"]) + self.assertEqual("test_user_id", event["user_id"]) + self.assertEqual("test_device_id", event["device_id"]) + self.assertEqual({'$price': 60.2, '$productId': 'P63', '$quantity': 3, '$receipt': 'A0001', + '$currency': 'USD', '$receiptSig': '0001A', 'other_property': 'test'}, + event["event_properties"]) + except AssertionError as e: + self.assertion_errors.append(str(e)) self.client.configuration.callback = callback_func for use_batch in (True, False): with self.subTest(use_batch=use_batch): post_method.reset_mock() self.client.configuration.use_batch = use_batch - revenue_obj = Revenue(price="abc", quantity=3, product_id="P63", properties={"other_property": "test"}) + revenue_obj = Revenue(price="abc", quantity=3, product_id="P63", currency="USD", properties={"other_property": "test"}) self.assertFalse(revenue_obj.is_valid()) with self.assertLogs("test", "ERROR") as cm: self.client.configuration.logger = logging.getLogger("test") @@ -244,18 +275,23 @@ def callback_func(event, code, message=None): if flush_future: flush_future.result() post_method.assert_called_once() + self.assertEqual(0, len(self.assertion_errors)) def test_amplitude_client_flush_success(self): post_method = MagicMock() HttpClient.post = post_method res = Response(HttpStatus.SUCCESS) post_method.return_value = res + self.assertion_errors = [] def callback_func(event, code, message=None): - self.assertEqual(200, code) - self.assertEqual("flush_test", event["event_type"]) - self.assertEqual("test_user_id", event["user_id"]) - self.assertEqual("test_device_id", event["device_id"]) + try: + self.assertEqual(200, code) + self.assertEqual("flush_test", event["event_type"]) + self.assertEqual("test_user_id", event["user_id"]) + self.assertEqual("test_device_id", event["device_id"]) + except AssertionError as e: + self.assertion_errors.append(str(e)) self.client.configuration.callback = callback_func for use_batch in (True, False): @@ -272,6 +308,7 @@ def callback_func(event, code, message=None): if flush_future: flush_future.result() post_method.assert_called_once() + self.assertEqual(0, len(self.assertion_errors)) def test_amplitude_add_remove_plugins_success(self): timeline = self.client._Amplitude__timeline