Skip to content

Conversation

@t8y8
Copy link
Collaborator

@t8y8 t8y8 commented Oct 31, 2016

Here's a first draft based on @RussTheAerialist's code in pagination_sample.py.

Currently it:

  • Takes an endpoint as its argument instead of a get function, this is because these calls only apply to get anyway, so let's keep it clean.
  • Takes an options object and seeds the filters/sorts with it, as well as the initial page size
  • Can be used as an iterator

I would like it to:

  • Not do everything in the __call__ method. I think we can move that to a helper or to __init__
    • We would make the first page call on instantiation, which populates some properties that could be useful (like __len__)
  • Since we know the total_avaliable we might be able to implement __len__. This gives users the ability to check the length to see if they really want to iterate over it or not.
  • I need to read more to see if I need to worry about __next__ or not.
  • Add tests

This script works (tested manually):

import tableauserverclient as TSC

auth_stuff = TSC.TableauAuth(username='USERNAME', password='PASSWORD')

server = TSC.Server("http://SERVER")
server.auth.sign_in(auth_stuff)

options = TSC.RequestOptions()
options.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name,
                                         TSC.RequestOptions.Operator.Equals,
                                         "WORKBOOKNAME"))

# Call with a filter
wbs = TSC.server.Pager(server.workbooks, options)
print(len([i for i in wbs[)) # prints 1

# Get them all!
wbs = TSC.server.Pager(server.workbooks)
print(len([i for i in wbs])) # prints 1086 on my test server, that's all of em!
server.auth.sign_out()

Open Questions:

  1. Where should it live? I'm not sure Server is the right place
  2. Thoughts on implementing __len__?
  3. Thoughts on implementing the primary logic somewhere other than __call__? It might just be me who thinks it's weird.

/cc @RussTheAerialist @LGraber

Copy link
Contributor

@graysonarts graysonarts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have strong opinions on location. @LGraber might, but having it live in the server package makes some sense since it's specific to how the server is returning information.

Also, we should be in the habit of including new tests for new functionality. I know I've been a bit lax on that recently myself, but we need to strive to be better than our previous selves :)

"""

def __init__(self, fetcher, opts=None):
self._fetcher = fetcher.get
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably name the argument that is passed in endpoint

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

self._fetcher = fetcher.get
self._options = opts

def __call__(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this is not ideal. It should probably just live in __iter__ and we get rid of __call__

self._options = opts

def __call__(self):
current_item_list, last_pagination_item = self._fetcher(self._options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this won't work if they set a page number in the options because count will never be greater than total available until we hit the end and throw an error.

We should check that a page number hasn't been set in the options, and if it has output a warning or throw an exception.

Copy link
Collaborator Author

@t8y8 t8y8 Oct 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's always a page number (it's defaulted to 1).

Should the logic be:

  1. On first iteration, run the generator
  2. Store some state that says "we're working on it now"
  3. Then just exhaust the generator starting from that point, then they could, if they want, start on page 3

I think that can happen if:

  1. __iter__ does the first call
  2. __next__ does the remaining calls

class Pager(object):
""" This class returns a generator that will iterate over all of the results.
server is the server object that will be used when calling the callback. It will be passed
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment does not appear to be relevant anymore

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why doesn't it work if we simply change how we initialize count?

if len(current_item_list) == 0:
current_item_list, last_pagination_item = self._load_next_page(current_item_list, last_pagination_item)

yield current_item_list.pop(0)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a bit more protection. Technically _load_next_page can return an empty list ... even after we fix our counting issue. This could happen if someone deletes an item while we are iterating. We should make sure the list is not empty before popping.

@LGraber
Copy link
Contributor

LGraber commented Oct 31, 2016

Oh yeah ... why did you put this in the server.py file and not its own file?

workbooks = TSC.Pager(self.server.workbooks, opts)
self.assertTrue(len(list(workbooks)) == 3)

# Starting on 3 with pagesize of 1 should get the last item
Copy link
Collaborator Author

@t8y8 t8y8 Oct 31, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's just a duplicate section, I'll remove

@t8y8
Copy link
Collaborator Author

t8y8 commented Oct 31, 2016

Alright! Addressed feedback (in person, and on here) tests added, and tested manually against an internal Server with this code:

import tableauserverclient as TSC


auth_stuff = TSC.TableauAuth(username='USER', password='password')

server = TSC.Server("http://server")

server.auth.sign_in(auth_stuff)

print(server.auth_token)

options = TSC.RequestOptions(pagenumber=2, pagesize=300)

wbs = TSC.Pager(server.workbooks, options)
print("GOT WBS")
print(len({i.id for i in wbs}))
server.auth.sign_out()

self.assertTrue(len(list(workbooks)) == 3)

# Let's check that workbook items aren't duplicates
workbooks = TSC.Pager(self.server.workbooks)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why did you need to call this again?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generators are one time use. Once you iterate over them, you cannot go back. He could have stored the output of list(workbooks) and then he wouldn't have had to call it twice.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically he constructed a list in the line above ... he just didn't capture it. I don't care that much, though, as this is not actually hitting a server.

# Register Pager with some pages
m.get(self.baseurl + "?pageNumber=1&pageSize=1", text=page_1)
m.get(self.baseurl + "?pageNumber=2&pageSize=1", text=page_2)
m.get(self.baseurl + "?pageNumber=3&pageSize=1", text=page_3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are you registering these if we are not supposed to call them?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is part of the requests-mock syntax. You register a url that will be called via requests, and the result you'd like returned when this is called (text= arguments), and then requests-mock does magic and returns the text when you make a requests call of the appropriate method to the appropriate url.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hahahaha. :) I know what it does. My comment was that we don't call these urls in this test so why do we need to register them. :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad ... it does get called (except maybe the first one). OOps

page_3 = f.read().decode('utf-8')
with requests_mock.mock() as m:
# Register Pager with default request options
m.get(self.baseurl, text=page_1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you registering this if the test is not supposed to call it?

@LGraber
Copy link
Contributor

LGraber commented Oct 31, 2016

I am confused because the tests don't represent how we expect the users to use this class (ie you keep casting to list and doing len) and you don't have a sample of how to use it. Can you please add a sample

Copy link
Contributor

@graysonarts graysonarts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

Copy link
Contributor

@LGraber LGraber left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

Copy link
Contributor

@graysonarts graysonarts left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 x2

workbooks = TSC.Pager(self.server.workbooks)
self.assertTrue(len(list(workbooks)) == 3)
workbooks = list(TSC.Pager(self.server.workbooks))
self.assertTrue(len(workbooks) == 3)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick

self.assertEquals(len(workbooks), 3)

@t8y8 t8y8 merged commit a6d0ede into tableau:development Oct 31, 2016
@t8y8 t8y8 deleted the 85-feature-pager branch October 31, 2016 21:27
bryceglarsen pushed a commit to bryceglarsen/server-client-python that referenced this pull request Dec 28, 2023
bryceglarsen pushed a commit to bryceglarsen/server-client-python that referenced this pull request Dec 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants