-
Notifications
You must be signed in to change notification settings - Fork 445
Implement Pager for auto-paging requests #90
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
graysonarts
left a comment
There was a problem hiding this 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 :)
tableauserverclient/server/server.py
Outdated
| """ | ||
|
|
||
| def __init__(self, fetcher, opts=None): | ||
| self._fetcher = fetcher.get |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
tableauserverclient/server/server.py
Outdated
| self._fetcher = fetcher.get | ||
| self._options = opts | ||
|
|
||
| def __call__(self): |
There was a problem hiding this comment.
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__
tableauserverclient/server/server.py
Outdated
| self._options = opts | ||
|
|
||
| def __call__(self): | ||
| current_item_list, last_pagination_item = self._fetcher(self._options) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- On first iteration, run the generator
- Store some state that says "we're working on it now"
- 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:
__iter__does the first call__next__does the remaining calls
tableauserverclient/server/server.py
Outdated
| 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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
tableauserverclient/server/server.py
Outdated
| 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) |
There was a problem hiding this comment.
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.
|
Oh yeah ... why did you put this in the server.py file and not its own file? |
test/test_pager.py
Outdated
| 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 |
There was a problem hiding this comment.
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
|
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() |
test/test_pager.py
Outdated
| self.assertTrue(len(list(workbooks)) == 3) | ||
|
|
||
| # Let's check that workbook items aren't duplicates | ||
| workbooks = TSC.Pager(self.server.workbooks) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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
test/test_pager.py
Outdated
| 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) |
There was a problem hiding this comment.
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?
|
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 |
graysonarts
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
LGraber
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚀
graysonarts
left a comment
There was a problem hiding this 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) |
There was a problem hiding this comment.
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)
Here's a first draft based on @RussTheAerialist's code in
pagination_sample.py.Currently it:
getfunction, this is because these calls only apply togetanyway, so let's keep it clean.I would like it to:
__call__method. I think we can move that to a helper or to__init____len__)total_avaliablewe 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.__next__or not.This script works (tested manually):
Open Questions:
__len__?__call__? It might just be me who thinks it's weird./cc @RussTheAerialist @LGraber