Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions accessgrid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@
AccessGrid,
AccessGridError,
AuthenticationError,
LedgerItem,
LedgerItemAccessPass,
LedgerItemPassTemplate,
Org,
Template,
UnifiedAccessPass,
Expand All @@ -40,4 +43,7 @@
"UnifiedAccessPass",
"Template",
"Org",
"LedgerItem",
"LedgerItemAccessPass",
"LedgerItemPassTemplate",
]
85 changes: 85 additions & 0 deletions accessgrid/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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__(
Expand Down
199 changes: 199 additions & 0 deletions tests/test_accessgrid.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down