import math
from linode_api4.objects.serializable import JSONObject
[docs]
class PaginatedList(object):
"""
The PaginatedList encapsulates the API V4's pagination in an easily
consumable way. A PaginatedList may be treated like a normal `list` in all
ways, and can be iterated over, indexed, and sliced.
PaginatedLists should never be constructed manually, and instead should
be created by requesting a collection of resources from the :any:`LinodeClient`.
For example::
linodes = client.linode.instances() # returns a PaginatedList of Linodes
Once you have a PaginatedList of resources, it doesn't matter how many
resources the API will return - you can iterate over all of them without
having to worry about pagination.::
# iterate over all linodes. If there are two or more pages,
# they will be loaded as required.
for linode in linodes:
print(linode.label)
You may access the number of items in a collection by calling `len` on the
PaginatedList::
num_linodes = len(linodes)
This will _not_ emit another API request.
"""
def __init__(
self,
client,
page_endpoint,
page=[],
max_pages=1,
total_items=None,
parent_id=None,
filters=None,
):
self.client = client
self.page_endpoint = page_endpoint
self.query_filters = filters
self.page_size = len(page)
self.max_pages = max_pages
self.lists = [None for _ in range(0, self.max_pages)]
if self.lists:
self.lists[0] = page
self.list_cls = (
type(page[0]) if page else None
) # TODO if this is None that's bad
self.objects_parent_id = parent_id
self.cur = 0 # for being a generator
self.total_items = total_items
if not total_items:
self.total_items = len(page)
[docs]
def first(self):
"""
A convenience method for getting only the first item in this list.
Exactly equivalent to getting index 0.
:returns: The first item in this list.
"""
return self[0]
[docs]
def last(self):
"""
A convenience method for getting only the last item in this list.
Exactly equivalent to getting index -1.
:returns: The first item in this list.
"""
return self[-1]
[docs]
def only(self):
"""
Returns the first item in this list, and asserts that it is the only
item. This is useful when querying a collection for a resource and
expecting to get only one back. For instance::
# raises if it finds more than one Linode
production_box = client.linode.instances(Linode.group == "prod").only()
:returns: The first and only item in this list.
:raises ValueError: If more than one item is in this list.
"""
if len(self) == 1:
return self[0]
raise ValueError("List {} has more than one element!".format(self))
def __repr__(self):
return "PaginatedList ({} items)".format(self.total_items)
def _load_page(self, page_number):
j = self.client.get(
"/{}?page={}&page_size={}".format(
self.page_endpoint, page_number + 1, self.page_size
),
filters=self.query_filters,
)
if j["pages"] != self.max_pages or j["results"] != len(self):
raise RuntimeError(
"List {} has changed since creation!".format(self)
)
l = PaginatedList.make_list(
j["data"],
self.client,
self.list_cls,
parent_id=self.objects_parent_id,
)
self.lists[page_number] = l
def __getitem__(self, index):
# this comes in here now, but we're hadling it elsewhere
if isinstance(index, slice):
return self._get_slice(index)
# handle negative indexing
if index < 0:
index = len(self) + index
if index < 0:
raise IndexError("list index out of range")
if index >= self.page_size * self.max_pages:
raise IndexError("list index out of range")
normalized_index = index % self.page_size
target_page = math.ceil((index + 1.0) / self.page_size) - 1
target_page = int(target_page)
if not self.lists[target_page]:
self._load_page(target_page)
return self.lists[target_page][normalized_index]
def __len__(self):
return self.total_items
def _get_slice(self, s):
# get range
i = s.start if s.start is not None else 0
j = s.stop if s.stop is not None else self.total_items
# we do not support steps outside of 1 yet
if s.step is not None and s.step != 1:
raise NotImplementedError(
"Only step sizes of 1 are currently supported."
)
# if i or j are negative, normalize them
if i < 0:
i = self.total_items + i
if j < 0:
j = self.total_items + j
# if i or j are still negative, that's an IndexError
if i < 0 or j < 0:
raise IndexError("list index out of range")
# if we're going nowhere or backward, return nothing
if j <= i:
return []
result = []
for c in range(i, j):
result.append(self[c])
return result
def __setitem__(self, index, value):
raise AttributeError(
"Assigning to indicies in paginated lists is not supported"
)
def __delitem__(self, index):
raise AttributeError("Deleting from paginated lists is not supported")
def __next__(self):
if self.cur < len(self):
self.cur += 1
return self[self.cur - 1]
else:
raise StopIteration()
@staticmethod
def make_list(json_arr, client, cls, parent_id=None):
"""
Returns a list of Populated objects of the given class type. This
should not be called outside of the :any:`LinodeClient` class.
:param json_arr: The array of JSON data to make into a list
:param client: The LinodeClient to pass to new objects
:param parent_id: The parent id for derived objects
:returns: A list of models from the JSON
"""
result = []
for obj in json_arr:
id_val = None
# Special handling for JSON objects
if issubclass(cls, JSONObject):
result.append(cls.from_json(obj))
continue
if "id" in obj:
id_val = obj["id"]
elif (
hasattr(cls, "id_attribute")
and getattr(cls, "id_attribute") in obj
):
id_val = obj[getattr(cls, "id_attribute")]
else:
continue
o = cls.make_instance(id_val, client, parent_id=parent_id, json=obj)
result.append(o)
return result
@staticmethod
def make_paginated_list(
json, client, cls, parent_id=None, page_url=None, filters=None
):
"""
Returns a PaginatedList populated with the first page of data provided,
and the ability to load additional pages. This should not be called
outside of the :any:`LinodeClient` class.
:param json: The JSON list to use as the first page
:param client: A LinodeClient to use to load additional pages
:param parent_id: The parent ID for derived objects
:param page_url: The URL to use when loading more pages
:param cls: The class to instantiate for objects
:param filters: The filters used when making the call that generated
this list. If not provided, this will fail when
loading additional pages.
:returns: An instance of PaginatedList that will represent the entire
collection whose first page is json
"""
l = PaginatedList.make_list(
json["data"], client, cls, parent_id=parent_id
)
p = PaginatedList(
client,
page_url,
page=l,
max_pages=json["pages"],
total_items=json["results"],
parent_id=parent_id,
filters=filters,
)
return p