From 639b328f23650312790758b9ff437b4e4fdeed9f Mon Sep 17 00:00:00 2001 From: Chet Bortz Date: Thu, 5 Mar 2026 18:34:45 -0500 Subject: [PATCH] add ledger-items endpoint support --- accessgrid/__init__.py | 6 ++ accessgrid/client.py | 85 +++++++++++++++++ tests/test_accessgrid.py | 199 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 290 insertions(+) diff --git a/accessgrid/__init__.py b/accessgrid/__init__.py index a2e1040..a23a599 100644 --- a/accessgrid/__init__.py +++ b/accessgrid/__init__.py @@ -23,6 +23,9 @@ AccessGrid, AccessGridError, AuthenticationError, + LedgerItem, + LedgerItemAccessPass, + LedgerItemPassTemplate, Org, Template, UnifiedAccessPass, @@ -40,4 +43,7 @@ "UnifiedAccessPass", "Template", "Org", + "LedgerItem", + "LedgerItemAccessPass", + "LedgerItemPassTemplate", ] diff --git a/accessgrid/client.py b/accessgrid/client.py index 08c37c1..0bd9372 100644 --- a/accessgrid/client.py +++ b/accessgrid/client.py @@ -159,6 +159,69 @@ def __repr__(self) -> str: return self.__str__() +class LedgerItemPassTemplate: + def __init__(self, client, data: Dict[str, Any]): + self._client = client + self.id = data.get("id") + self.name = data.get("name") + self.protocol = data.get("protocol") + self.platform = data.get("platform") + self.use_case = data.get("use_case") + + def __str__(self) -> str: + return f"LedgerItemPassTemplate(id='{self.id}', name='{self.name}')" + + def __repr__(self) -> str: + return self.__str__() + + +class LedgerItemAccessPass: + def __init__(self, client, data: Dict[str, Any]): + self._client = client + self.id = data.get("id") + self.full_name = data.get("full_name") + self.state = data.get("state") + self.metadata = data.get("metadata", {}) + self.unified_access_pass_ex_id = data.get("unified_access_pass_ex_id") + self.pass_template = ( + LedgerItemPassTemplate(client, data["pass_template"]) + if data.get("pass_template") + else None + ) + + def __str__(self) -> str: + return ( + f"LedgerItemAccessPass(id='{self.id}', " + f"full_name='{self.full_name}', state='{self.state}')" + ) + + def __repr__(self) -> str: + return self.__str__() + + +class LedgerItem: + def __init__(self, client, data: Dict[str, Any]): + self._client = client + self.id = data.get("id") + self.created_at = data.get("created_at") + self.amount = data.get("amount") + self.kind = data.get("kind") + self.metadata = data.get("metadata", {}) + self.access_pass = ( + LedgerItemAccessPass(client, data["access_pass"]) + if data.get("access_pass") + else None + ) + + def __str__(self) -> str: + return ( + f"LedgerItem(id='{self.id}', kind='{self.kind}', " f"amount={self.amount})" + ) + + def __repr__(self) -> str: + return self.__str__() + + class AccessCards: def __init__(self, client): self._client = client @@ -342,6 +405,28 @@ def list_pass_template_pairs(self, **kwargs) -> Dict[str, Any]: return response + def list_ledger_items(self, **kwargs) -> Dict[str, Any]: + """ + List Ledger Items with pagination and date filter support. + + Args: + page: Page number for pagination (default: 1) + per_page: Number of results per page (default: 50, max: 100) + start_date: ISO8601 datetime to filter from + end_date: ISO8601 datetime to filter to + + Returns: + Dict containing ledger_items list and pagination info + """ + response = self._client._get("/v1/console/ledger-items", params=kwargs) + + if "ledger_items" in response: + response["ledger_items"] = [ + LedgerItem(self._client, item) for item in response["ledger_items"] + ] + + return response + class AccessGrid: def __init__( diff --git a/tests/test_accessgrid.py b/tests/test_accessgrid.py index 8449a75..350edba 100644 --- a/tests/test_accessgrid.py +++ b/tests/test_accessgrid.py @@ -413,6 +413,205 @@ def test_list_pass_template_pairs(self, mock_request, client): assert pairs[1].ios_template.id == "tmpl-ios-2" +class TestLedgerItems: + @pytest.fixture + def mock_ledger_response(self): + mock_resp = Mock() + mock_resp.status_code = 200 + mock_resp.json.return_value = { + "ledger_items": [ + { + "id": "li-1", + "created_at": "2025-03-01T12:00:00Z", + "amount": 150, + "kind": "provision", + "metadata": {"access_pass_ex_id": "ap-1"}, + "access_pass": { + "id": "ap-1", + "full_name": "Jane Doe", + "state": "active", + "metadata": {"department": "Engineering"}, + "unified_access_pass_ex_id": "uap-1", + "pass_template": { + "id": "pt-1", + "name": "Employee Badge", + "protocol": "desfire", + "platform": "apple", + "use_case": "employee_badge", + }, + }, + }, + { + "id": "li-2", + "created_at": "2025-03-02T12:00:00Z", + "amount": 50, + "kind": "renewal", + "metadata": {}, + "access_pass": None, + }, + ], + "pagination": { + "current_page": 1, + "per_page": 50, + "total_pages": 3, + "total_count": 125, + }, + } + return mock_resp + + @patch("requests.request") + def test_list_ledger_items(self, mock_request, client, mock_ledger_response): + mock_request.return_value = mock_ledger_response + + client.console.list_ledger_items() + + call_args = mock_request.call_args[1] + assert call_args["method"] == "GET" + assert call_args["url"] == f"{client.base_url}/v1/console/ledger-items" + + @patch("requests.request") + def test_list_ledger_items_with_pagination( + self, mock_request, client, mock_ledger_response + ): + mock_request.return_value = mock_ledger_response + + client.console.list_ledger_items(page=2, per_page=10) + + call_args = mock_request.call_args[1] + assert call_args["params"]["page"] == 2 + assert call_args["params"]["per_page"] == 10 + + @patch("requests.request") + def test_list_ledger_items_with_date_filters( + self, mock_request, client, mock_ledger_response + ): + mock_request.return_value = mock_ledger_response + + client.console.list_ledger_items( + start_date="2025-03-01T00:00:00Z", + end_date="2025-03-31T23:59:59Z", + ) + + call_args = mock_request.call_args[1] + assert call_args["params"]["start_date"] == "2025-03-01T00:00:00Z" + assert call_args["params"]["end_date"] == "2025-03-31T23:59:59Z" + + @patch("requests.request") + def test_list_ledger_items_deserializes_models( + self, mock_request, client, mock_ledger_response + ): + mock_request.return_value = mock_ledger_response + + result = client.console.list_ledger_items() + items = result["ledger_items"] + + assert len(items) == 2 + + item = items[0] + assert type(item).__name__ == "LedgerItem" + assert item.id == "li-1" + assert item.created_at == "2025-03-01T12:00:00Z" + assert item.amount == 150 + assert item.kind == "provision" + assert item.metadata == {"access_pass_ex_id": "ap-1"} + + @patch("requests.request") + def test_list_ledger_items_nested_access_pass( + self, mock_request, client, mock_ledger_response + ): + mock_request.return_value = mock_ledger_response + + result = client.console.list_ledger_items() + item = result["ledger_items"][0] + + assert type(item.access_pass).__name__ == "LedgerItemAccessPass" + assert item.access_pass.id == "ap-1" + assert item.access_pass.full_name == "Jane Doe" + assert item.access_pass.state == "active" + assert item.access_pass.metadata == {"department": "Engineering"} + assert item.access_pass.unified_access_pass_ex_id == "uap-1" + + @patch("requests.request") + def test_list_ledger_items_nested_pass_template( + self, mock_request, client, mock_ledger_response + ): + mock_request.return_value = mock_ledger_response + + result = client.console.list_ledger_items() + tmpl = result["ledger_items"][0].access_pass.pass_template + + assert type(tmpl).__name__ == "LedgerItemPassTemplate" + assert tmpl.id == "pt-1" + assert tmpl.name == "Employee Badge" + assert tmpl.protocol == "desfire" + assert tmpl.platform == "apple" + assert tmpl.use_case == "employee_badge" + + @patch("requests.request") + def test_list_ledger_items_null_access_pass( + self, mock_request, client, mock_ledger_response + ): + mock_request.return_value = mock_ledger_response + + result = client.console.list_ledger_items() + item = result["ledger_items"][1] + + assert item.id == "li-2" + assert item.access_pass is None + + @patch("requests.request") + def test_list_ledger_items_null_pass_template(self, mock_request, client): + mock_resp = Mock() + mock_resp.status_code = 200 + mock_resp.json.return_value = { + "ledger_items": [ + { + "id": "li-3", + "created_at": "2025-03-03T12:00:00Z", + "amount": 75, + "kind": "provision", + "metadata": {}, + "access_pass": { + "id": "ap-2", + "full_name": "John Smith", + "state": "suspended", + "metadata": {}, + "unified_access_pass_ex_id": None, + "pass_template": None, + }, + } + ], + "pagination": { + "current_page": 1, + "per_page": 50, + "total_pages": 1, + "total_count": 1, + }, + } + mock_request.return_value = mock_resp + + result = client.console.list_ledger_items() + item = result["ledger_items"][0] + + assert type(item.access_pass).__name__ == "LedgerItemAccessPass" + assert item.access_pass.pass_template is None + + @patch("requests.request") + def test_list_ledger_items_preserves_pagination( + self, mock_request, client, mock_ledger_response + ): + mock_request.return_value = mock_ledger_response + + result = client.console.list_ledger_items() + + assert result["pagination"] == { + "current_page": 1, + "per_page": 50, + "total_pages": 3, + "total_count": 125, + } + + class TestHIDOrgs: @patch("requests.request") def test_create_org(self, mock_request, client):