From cf6dc6e89a7c8563ede0be1ab0a13c41fdb26ca3 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 11 Feb 2026 19:29:33 +0400 Subject: [PATCH 1/4] Add customer wallet alerts --- lago_python_client/customers/wallet_client.py | 5 + .../customers/wallets/alert_client.py | 28 +++ lago_python_client/models/alert.py | 10 +- tests/fixtures/subscription_alert.json | 43 ++++ tests/fixtures/subscription_alert_index.json | 59 ++++++ tests/fixtures/wallet_alert.json | 26 +++ tests/fixtures/wallet_alert_index.json | 34 +++ tests/test_customer_wallet_alert_client.py | 194 ++++++++++++++++++ 8 files changed, 395 insertions(+), 4 deletions(-) create mode 100644 lago_python_client/customers/wallets/alert_client.py create mode 100644 tests/fixtures/subscription_alert.json create mode 100644 tests/fixtures/subscription_alert_index.json create mode 100644 tests/fixtures/wallet_alert.json create mode 100644 tests/fixtures/wallet_alert_index.json create mode 100644 tests/test_customer_wallet_alert_client.py diff --git a/lago_python_client/customers/wallet_client.py b/lago_python_client/customers/wallet_client.py index 990466ae..d9ada870 100644 --- a/lago_python_client/customers/wallet_client.py +++ b/lago_python_client/customers/wallet_client.py @@ -13,6 +13,7 @@ ) from .wallets.metadata_client import CustomerWalletMetadataClient +from .wallets.alert_client import CustomerWalletAlertClient class CustomerWalletClient( @@ -37,3 +38,7 @@ def api_resource(self, customer_id: str) -> tuple[str]: @callable_cached_property def metadata(self) -> CustomerWalletMetadataClient: return CustomerWalletMetadataClient(self.base_url, self.api_key) + + @callable_cached_property + def alerts(self) -> CustomerWalletAlertClient: + return CustomerWalletAlertClient(self.base_url, self.api_key) diff --git a/lago_python_client/customers/wallets/alert_client.py b/lago_python_client/customers/wallets/alert_client.py new file mode 100644 index 00000000..30ef6f80 --- /dev/null +++ b/lago_python_client/customers/wallets/alert_client.py @@ -0,0 +1,28 @@ +from typing import ClassVar, Type + +from ...base_client import BaseClient +from ...models.alert import AlertResponse + +from ...mixins import ( + NestedCreateCommandMixin, + NestedUpdateCommandMixin, + NestedDestroyCommandMixin, + NestedFindCommandMixin, + NestedFindAllCommandMixin, +) + + +class CustomerWalletAlertClient( + NestedCreateCommandMixin[AlertResponse], + NestedUpdateCommandMixin[AlertResponse], + NestedDestroyCommandMixin[AlertResponse], + NestedFindCommandMixin[AlertResponse], + NestedFindAllCommandMixin[AlertResponse], + BaseClient, +): + API_RESOURCE: ClassVar[str] = "alerts" + RESPONSE_MODEL: ClassVar[Type[AlertResponse]] = AlertResponse + ROOT_NAME: ClassVar[str] = "alert" + + def api_resource(self, customer_id: str, wallet_code: str) -> tuple[str]: + return ("customers", customer_id, "wallets", wallet_code, "alerts") diff --git a/lago_python_client/models/alert.py b/lago_python_client/models/alert.py index 3add5117..d1f1b843 100644 --- a/lago_python_client/models/alert.py +++ b/lago_python_client/models/alert.py @@ -15,11 +15,11 @@ class AlertThresholdList(BaseModel): class Alert(BaseModel): - alert_type: str - code: str + alert_type: Optional[str] + code: Optional[str] name: Optional[str] + thresholds: Optional[AlertThresholdList] billable_metric_code: Optional[str] - thresholds: AlertThresholdList class AlertsList(BaseModel): @@ -39,7 +39,9 @@ class AlertThresholdResponseList(BaseResponseModel): class AlertResponse(BaseResponseModel): lago_id: str lago_organization_id: str - external_subscription_id: str + external_subscription_id: Optional[str] + lago_wallet_id: Optional[str] + wallet_code: Optional[str] alert_type: str code: str name: Optional[str] diff --git a/tests/fixtures/subscription_alert.json b/tests/fixtures/subscription_alert.json new file mode 100644 index 00000000..9b73ceb2 --- /dev/null +++ b/tests/fixtures/subscription_alert.json @@ -0,0 +1,43 @@ +{ + "alert": { + "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "external_subscription_id": "sub_1234567890", + "external_customer_id": "cus_0987654321", + "alert_type": "billable_metric_current_usage_amount", + "code": "storage_threshold_alert", + "name": "Storage Usage Alert", + "billable_metric": { + "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "name": "Storage", + "code": "storage", + "description": "GB of storage used in my application", + "recurring": false, + "rounding_function": "round", + "rounding_precision": 2, + "created_at": "2022-09-14T16:35:31Z", + "expression": "round((ended_at - started_at) * units)", + "field_name": "gb", + "aggregation_type": "sum_agg", + "weighted_interval": "seconds", + "filters": [ + { + "key": "region", + "values": [ + "us-east-1" + ] + } + ] + }, + "previous_value": 1000, + "last_processed_at": "2025-05-19T10:04:21Z", + "thresholds": [ + { + "code": "warn", + "recurring": false, + "value": "99.0" + } + ], + "created_at": "2025-03-20T10:00:00Z" + } +} diff --git a/tests/fixtures/subscription_alert_index.json b/tests/fixtures/subscription_alert_index.json new file mode 100644 index 00000000..4e0358ca --- /dev/null +++ b/tests/fixtures/subscription_alert_index.json @@ -0,0 +1,59 @@ +Subscription alerts + +Media type + +Controls Accept header. +Example Value +Schema +{ + "alerts": [ + { + "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "external_subscription_id": "sub_1234567890", + "external_customer_id": "cus_0987654321", + "billable_metric": { + "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "name": "Storage", + "code": "storage", + "description": "GB of storage used in my application", + "recurring": false, + "rounding_function": "round", + "rounding_precision": 2, + "created_at": "2022-09-14T16:35:31Z", + "expression": "round((ended_at - started_at) * units)", + "field_name": "gb", + "aggregation_type": "sum_agg", + "weighted_interval": "seconds", + "filters": [ + { + "key": "region", + "values": [ + "us-east-1" + ] + } + ] + }, + "alert_type": "billable_metric_current_usage_amount", + "code": "storage_threshold_alert", + "name": "Storage Usage Alert", + "previous_value": 1000, + "last_processed_at": "2025-05-19T10:04:21Z", + "thresholds": [ + { + "code": "warn", + "recurring": false, + "value": "99.0" + } + ], + "created_at": "2025-03-20T10:00:00Z" + } + ], + "meta": { + "current_page": 2, + "next_page": 3, + "prev_page": 1, + "total_pages": 4, + "total_count": 70 + } +} diff --git a/tests/fixtures/wallet_alert.json b/tests/fixtures/wallet_alert.json new file mode 100644 index 00000000..ab7e76c2 --- /dev/null +++ b/tests/fixtures/wallet_alert.json @@ -0,0 +1,26 @@ +{ + "alert": { + "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "external_subscription_id": null, + "external_customer_id": "customer_id", + "lago_wallet_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "wallet_code": "wallet_code", + "code": "wallet_balance_alert", + "name": "Balance Amount Alert", + "alert_type": "wallet_balance_amount", + "direction": "increasing", + "previous_value": 1000, + "thresholds": [ + { + "code": "warn", + "value": 10000, + "recurring": false + } + ], + "billable_metric": null, + "created_at": "2025-03-20T10:00:00Z", + "last_processed_at": "2025-05-19T10:04:21Z" + } +} + diff --git a/tests/fixtures/wallet_alert_index.json b/tests/fixtures/wallet_alert_index.json new file mode 100644 index 00000000..acc1c613 --- /dev/null +++ b/tests/fixtures/wallet_alert_index.json @@ -0,0 +1,34 @@ +{ + "alerts": [ + { + "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "external_subscription_id": null, + "external_customer_id": "cus_0987654321", + "lago_wallet_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", + "wallet_code": "wallet_code", + "code": "wallet_balance_alert", + "name": "Balance Amount Alert", + "alert_type": "wallet_balance_amount", + "direction": "increasing", + "previous_value": 1000, + "thresholds": [ + { + "code": "warn", + "recurring": false, + "value": "99.0" + } + ], + "billable_metric": null, + "created_at": "2025-03-20T10:00:00Z", + "last_processed_at": "2025-05-19T10:04:21Z" + } + ], + "meta": { + "current_page": 1, + "next_page": 2, + "prev_page": null, + "total_pages": 4, + "total_count": 70 + } +} diff --git a/tests/test_customer_wallet_alert_client.py b/tests/test_customer_wallet_alert_client.py new file mode 100644 index 00000000..d18cdfad --- /dev/null +++ b/tests/test_customer_wallet_alert_client.py @@ -0,0 +1,194 @@ +import pytest +from pytest_httpx import HTTPXMock + +from lago_python_client.client import Client +from lago_python_client.exceptions import LagoApiError +from lago_python_client.models import ( + Alert, + AlertThreshold, + AlertThresholdList, +) + +from .utils.mixin import mock_response + + +def alert_object(): + threshold = AlertThreshold(code="warn", value=10000) + + return Alert( + alert_type="wallet_balance_amount", + code="wallet_balance_alert", + name="Balance Amount Alert", + thresholds=AlertThresholdList(__root__=[threshold]), + ) + + +def test_valid_create_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="POST", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + content=mock_response("wallet_alert"), + ) + response = client.customers.wallets.alerts.create("customer_id", "wallet_code", alert_object()) + + assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response.lago_organization_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response.external_subscription_id == None + assert response.lago_wallet_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response.wallet_code == "wallet_code" + assert response.code == "wallet_balance_alert" + assert response.name == "Balance Amount Alert" + assert response.alert_type == "wallet_balance_amount" + assert response.direction == "increasing" + assert response.previous_value == 1000 + assert response.thresholds == AlertThresholdList( + __root__=[AlertThreshold(code="warn", value=10000, recurring=False)] + ) + assert response.billable_metric == None + + +def test_invalid_create_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="POST", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + status_code=401, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.create("customer_id", "wallet_code", alert_object()) + + +def test_valid_update_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + code = "alert-code" + + httpx_mock.add_response( + method="PUT", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + content=mock_response("wallet_alert"), + ) + response = client.customers.wallets.alerts.update("customer_id", "wallet_code", code, alert_object()) + + assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + + +def test_invalid_update_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + code = "invalid" + + httpx_mock.add_response( + method="PUT", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + status_code=401, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.update("customer_id", "wallet_code", code, alert_object()) + + +def test_valid_find_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + code = "alert-code" + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + content=mock_response("wallet_alert"), + ) + response = client.customers.wallets.alerts.find("customer_id", "wallet_code", code) + + assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + + +def test_invalid_find_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + code = "invalid" + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.find("customer_id", "wallet_code", code) + + +def test_valid_destroy_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + code = "alert-code" + + httpx_mock.add_response( + method="DELETE", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + content=mock_response("wallet_alert"), + ) + response = client.customers.wallets.alerts.destroy("customer_id", "wallet_code", code) + + assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + + +def test_invalid_destroy_customer_wallet_alert_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + code = "invalid" + + httpx_mock.add_response( + method="DELETE", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts/" + code, + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.destroy("customer_id", "wallet_code", code) + + +def test_valid_find_all_customer_wallet_alerts_request(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + content=mock_response("wallet_alert_index"), + ) + response = client.customers.wallets.alerts.find_all("customer_id", "wallet_code") + + assert response["alerts"][0].lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response["meta"]["current_page"] == 1 + + +def test_valid_find_all_customer_wallet_alerts_request_with_options(httpx_mock: HTTPXMock): + client = Client(api_key="886fe239-927d-4072-ab72-6dd345e8dd0d") + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts?page=1&per_page=2", + content=mock_response("wallet_alert_index"), + ) + response = client.customers.wallets.alerts.find_all( + "customer_id", "wallet_code", options={"per_page": 2, "page": 1} + ) + + assert response["alerts"][0].lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" + assert response["meta"]["current_page"] == 1 + + +def test_invalid_find_all_wallet_request(httpx_mock: HTTPXMock): + client = Client(api_key="invalid") + + httpx_mock.add_response( + method="GET", + url="https://api.getlago.com/api/v1/customers/customer_id/wallets/wallet_code/alerts", + status_code=404, + content=b"", + ) + + with pytest.raises(LagoApiError): + client.customers.wallets.alerts.find_all("customer_id", "wallet_code") From 2d8c9d6d791ff8c7b565402c44dbb113f0475a8b Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 11 Feb 2026 19:36:29 +0400 Subject: [PATCH 2/4] Fix linters --- tests/test_customer_wallet_alert_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_customer_wallet_alert_client.py b/tests/test_customer_wallet_alert_client.py index d18cdfad..6a495b4f 100644 --- a/tests/test_customer_wallet_alert_client.py +++ b/tests/test_customer_wallet_alert_client.py @@ -35,7 +35,7 @@ def test_valid_create_customer_wallet_alert_request(httpx_mock: HTTPXMock): assert response.lago_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" assert response.lago_organization_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" - assert response.external_subscription_id == None + assert response.external_subscription_id is None assert response.lago_wallet_id == "1a901a90-1a90-1a90-1a90-1a901a901a90" assert response.wallet_code == "wallet_code" assert response.code == "wallet_balance_alert" @@ -46,7 +46,7 @@ def test_valid_create_customer_wallet_alert_request(httpx_mock: HTTPXMock): assert response.thresholds == AlertThresholdList( __root__=[AlertThreshold(code="warn", value=10000, recurring=False)] ) - assert response.billable_metric == None + assert response.billable_metric is None def test_invalid_create_customer_wallet_alert_request(httpx_mock: HTTPXMock): From b32840f6cac31e80b670b351fd9cebad28176069 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 11 Feb 2026 20:10:45 +0400 Subject: [PATCH 3/4] Delete subscription alerts fixturs --- tests/fixtures/subscription_alert.json | 43 -------------- tests/fixtures/subscription_alert_index.json | 59 -------------------- tests/test_customer_wallet_alert_client.py | 2 +- 3 files changed, 1 insertion(+), 103 deletions(-) delete mode 100644 tests/fixtures/subscription_alert.json delete mode 100644 tests/fixtures/subscription_alert_index.json diff --git a/tests/fixtures/subscription_alert.json b/tests/fixtures/subscription_alert.json deleted file mode 100644 index 9b73ceb2..00000000 --- a/tests/fixtures/subscription_alert.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "alert": { - "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", - "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", - "external_subscription_id": "sub_1234567890", - "external_customer_id": "cus_0987654321", - "alert_type": "billable_metric_current_usage_amount", - "code": "storage_threshold_alert", - "name": "Storage Usage Alert", - "billable_metric": { - "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", - "name": "Storage", - "code": "storage", - "description": "GB of storage used in my application", - "recurring": false, - "rounding_function": "round", - "rounding_precision": 2, - "created_at": "2022-09-14T16:35:31Z", - "expression": "round((ended_at - started_at) * units)", - "field_name": "gb", - "aggregation_type": "sum_agg", - "weighted_interval": "seconds", - "filters": [ - { - "key": "region", - "values": [ - "us-east-1" - ] - } - ] - }, - "previous_value": 1000, - "last_processed_at": "2025-05-19T10:04:21Z", - "thresholds": [ - { - "code": "warn", - "recurring": false, - "value": "99.0" - } - ], - "created_at": "2025-03-20T10:00:00Z" - } -} diff --git a/tests/fixtures/subscription_alert_index.json b/tests/fixtures/subscription_alert_index.json deleted file mode 100644 index 4e0358ca..00000000 --- a/tests/fixtures/subscription_alert_index.json +++ /dev/null @@ -1,59 +0,0 @@ -Subscription alerts - -Media type - -Controls Accept header. -Example Value -Schema -{ - "alerts": [ - { - "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", - "lago_organization_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", - "external_subscription_id": "sub_1234567890", - "external_customer_id": "cus_0987654321", - "billable_metric": { - "lago_id": "1a901a90-1a90-1a90-1a90-1a901a901a90", - "name": "Storage", - "code": "storage", - "description": "GB of storage used in my application", - "recurring": false, - "rounding_function": "round", - "rounding_precision": 2, - "created_at": "2022-09-14T16:35:31Z", - "expression": "round((ended_at - started_at) * units)", - "field_name": "gb", - "aggregation_type": "sum_agg", - "weighted_interval": "seconds", - "filters": [ - { - "key": "region", - "values": [ - "us-east-1" - ] - } - ] - }, - "alert_type": "billable_metric_current_usage_amount", - "code": "storage_threshold_alert", - "name": "Storage Usage Alert", - "previous_value": 1000, - "last_processed_at": "2025-05-19T10:04:21Z", - "thresholds": [ - { - "code": "warn", - "recurring": false, - "value": "99.0" - } - ], - "created_at": "2025-03-20T10:00:00Z" - } - ], - "meta": { - "current_page": 2, - "next_page": 3, - "prev_page": 1, - "total_pages": 4, - "total_count": 70 - } -} diff --git a/tests/test_customer_wallet_alert_client.py b/tests/test_customer_wallet_alert_client.py index 6a495b4f..fb2ab253 100644 --- a/tests/test_customer_wallet_alert_client.py +++ b/tests/test_customer_wallet_alert_client.py @@ -180,7 +180,7 @@ def test_valid_find_all_customer_wallet_alerts_request_with_options(httpx_mock: assert response["meta"]["current_page"] == 1 -def test_invalid_find_all_wallet_request(httpx_mock: HTTPXMock): +def test_invalid_find_all_wallet_alerts_request(httpx_mock: HTTPXMock): client = Client(api_key="invalid") httpx_mock.add_response( From 320d6af3703ce17c0162fca8931b35f8557655d9 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 17 Feb 2026 13:48:28 +0400 Subject: [PATCH 4/4] Update alert models --- lago_python_client/models/alert.py | 1 + tests/test_customer_wallet_alert_client.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lago_python_client/models/alert.py b/lago_python_client/models/alert.py index d1f1b843..eacd1845 100644 --- a/lago_python_client/models/alert.py +++ b/lago_python_client/models/alert.py @@ -45,6 +45,7 @@ class AlertResponse(BaseResponseModel): alert_type: str code: str name: Optional[str] + direction: Optional[str] previous_value: Optional[str] last_processed_at: Optional[str] thresholds: AlertThresholdResponseList diff --git a/tests/test_customer_wallet_alert_client.py b/tests/test_customer_wallet_alert_client.py index fb2ab253..5da936a5 100644 --- a/tests/test_customer_wallet_alert_client.py +++ b/tests/test_customer_wallet_alert_client.py @@ -42,7 +42,7 @@ def test_valid_create_customer_wallet_alert_request(httpx_mock: HTTPXMock): assert response.name == "Balance Amount Alert" assert response.alert_type == "wallet_balance_amount" assert response.direction == "increasing" - assert response.previous_value == 1000 + assert response.previous_value == "1000" assert response.thresholds == AlertThresholdList( __root__=[AlertThreshold(code="warn", value=10000, recurring=False)] )