File: collection.py

package info (click to toggle)
python-shopifyapi 12.7.0-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 560 kB
  • sloc: python: 1,752; sh: 10; makefile: 9
file content (156 lines) | stat: -rw-r--r-- 5,299 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
from pyactiveresource.collection import Collection


class PaginatedCollection(Collection):
    """
    A subclass of Collection which allows cycling through pages of
    data through cursor-based pagination.

    :next_page_url contains a url for fetching the next page
    :previous_page_url contains a url for fetching the previous page

    You can use next_page_url and previous_page_url to fetch the next page
    of data by calling Resource.find(from_=page.next_page_url)
    """

    def __init__(self, *args, **kwargs):
        """If given a Collection object as an argument, inherit its metadata."""

        metadata = kwargs.pop("metadata", None)
        obj = args[0]
        if isinstance(obj, Collection):
            if metadata:
                metadata.update(obj.metadata)
            else:
                metadata = obj.metadata
            super(PaginatedCollection, self).__init__(obj, metadata=metadata)
        else:
            super(PaginatedCollection, self).__init__(metadata=metadata or {}, *args, **kwargs)

        if not ("resource_class" in self.metadata):
            raise AttributeError('Cursor-based pagination requires a "resource_class" attribute in the metadata.')

        self.metadata["pagination"] = self.__parse_pagination()
        self.next_page_url = self.metadata["pagination"].get("next", None)
        self.previous_page_url = self.metadata["pagination"].get("previous", None)

        self._next = None
        self._previous = None
        self._current_iter = None
        self._no_iter_next = kwargs.pop("no_iter_next", True)

    def __parse_pagination(self):
        if "headers" not in self.metadata:
            return {}

        values = self.metadata["headers"].get("Link", self.metadata["headers"].get("link", None))
        if values is None:
            return {}

        result = {}
        for value in values.split(", "):
            link, rel = value.split("; ")
            result[rel.split('"')[1]] = link[1:-1]
        return result

    def has_previous_page(self):
        """Returns true if the current page has any previous pages before it."""
        return bool(self.previous_page_url)

    def has_next_page(self):
        """Returns true if the current page has any pages beyond the current position."""
        return bool(self.next_page_url)

    def previous_page(self, no_cache=False):
        """Returns the previous page of items.

        Args:
            no_cache: If true the page will not be cached.
        Returns:
            A PaginatedCollection object with the new data set.
        """
        if self._previous:
            return self._previous
        elif not self.has_previous_page():
            raise IndexError("No previous page")
        return self.__fetch_page(self.previous_page_url, no_cache)

    def next_page(self, no_cache=False):
        """Returns the next page of items.

        Args:
            no_cache: If true the page will not be cached.
        Returns:
            A PaginatedCollection object with the new data set.
        """
        if self._next:
            return self._next
        elif not self.has_next_page():
            raise IndexError("No next page")
        return self.__fetch_page(self.next_page_url, no_cache)

    def __fetch_page(self, url, no_cache=False):
        next = self.metadata["resource_class"].find(from_=url)
        if not no_cache:
            self._next = next
            self._next._previous = self
        next._no_iter_next = self._no_iter_next
        return next

    def __iter__(self):
        """Iterates through all items, also fetching other pages."""
        for item in super(PaginatedCollection, self).__iter__():
            yield item

        if self._no_iter_next:
            return

        try:
            if not self._current_iter:
                self._current_iter = self
            self._current_iter = self.next_page()

            for item in self._current_iter:
                yield item
        except IndexError:
            return

    def __len__(self):
        """If fetched count all the pages."""

        if self._next:
            count = len(self._next)
        else:
            count = 0
        return count + super(PaginatedCollection, self).__len__()


class PaginatedIterator(object):
    """
    This class implements an iterator over paginated collections which aims to
    be more memory-efficient by not keeping more than one page in memory at a
    time.

    >>> from shopify import Product, PaginatedIterator
    >>> for page in PaginatedIterator(Product.find()):
    ...     for item in page:
    ...         do_something(item)
    ...
    # every page and the page items are iterated
    """

    def __init__(self, collection):
        if not isinstance(collection, PaginatedCollection):
            raise TypeError("PaginatedIterator expects a PaginatedCollection instance")
        self.collection = collection
        self.collection._no_iter_next = True

    def __iter__(self):
        """Iterate over pages, returning one page at a time."""
        current_page = self.collection
        while True:
            yield current_page
            try:
                current_page = current_page.next_page(no_cache=True)
            except IndexError:
                return