Skip to content

Issue 112 - Support for the new image api endpoint#113

Merged
jz-huang merged 5 commits intotableau:vnextfrom
jz-huang:112-feature-image-api-endpoint
Dec 15, 2016
Merged

Issue 112 - Support for the new image api endpoint#113
jz-huang merged 5 commits intotableau:vnextfrom
jz-huang:112-feature-image-api-endpoint

Conversation

@jz-huang
Copy link
Contributor

added support for the new image api endpoint. Added unit tests and a sample. Added the new sample to docs.

Unit tests and pep8 run clean. Manually tested sample.

@jz-huang jz-huang self-assigned this Dec 14, 2016
@jz-huang jz-huang requested review from LGraber and t8y8 December 14, 2016 02:33
@jz-huang
Copy link
Contributor Author

Added a new ImageRequestOptions class because the "resolution" parameter doesn't really fit in with the current parameters in RequestOptions. Filter, sort, paging are all for endpoints that return a list of resources, doesn't really make sense to to add "resolution" to the group. And resolution is only parameter that's available for the image endpoint. With the new ImageRequestOptions, it would be easier to add more parameters for this endpoint in the future (i.e. filters within the viz).

@t8y8
Copy link
Collaborator

t8y8 commented Dec 14, 2016

Do we really need ABC and metaclass stuff? I think just a simple inherit is probably enough, it's a single method and not a complex class. It can also all probably rest in the same file -- splitting everything out into multiple files and enforcing the subclassing via metaclasses is very "java"

The rest of the code looks pretty solid, I'll do a detailed review tomorrow!

Copy link
Collaborator

@t8y8 t8y8 left a comment

Choose a reason for hiding this comment

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

One major objection, and then mostly small nits and questions

def main():

parser = argparse.ArgumentParser(description='Query View Image From Server')
parser.add_argument('--server', '-s', required=True, help='server address')
Copy link
Collaborator

@t8y8 t8y8 Dec 14, 2016

Choose a reason for hiding this comment

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

Please add site as well (I know not all samples have site yet, but you can avoid the rework later ;) )

Copy link
Contributor

Choose a reason for hiding this comment

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

@t8y8 i think you are saying "don't assume that everything is in the default site." yes?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Right, I noticed when messing around with 'livetests' that we assume the default site, we should take a site-id/content_url parameter for all of the samples (especially for Online folks)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

sounds good, i'll add content_url as a parameter.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks!

(Don't worry about the other samples for now, I might do that in a quick PR against development; and we don't want merge conflicts :) )


# Step 2: Get all the projects on server, then look for the default one.
all_projects, pagination_item = server.projects.get()
default_project = next((project for project in all_projects if project.is_default()), None)
Copy link
Collaborator

@t8y8 t8y8 Dec 14, 2016

Choose a reason for hiding this comment

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

You can just use use TSC.Pager here.

(And this will fail if they have more than 100 projects and the default isn't in the first set of 100 returned)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

server.views.populate_image(view_item, image_req_option)

with open(args.filepath, "wb") as image_file:
image_file.write(view_item.image)
Copy link
Collaborator

Choose a reason for hiding this comment

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

How large can this file get?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The files are usually a few hundred KB, probably gets to a couple of MB's at most.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Cool, we shouldn't have to worry about it then.

import abc


class RequestOptionsBase(object):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the abc and metaclass magic is unnecessary and doesn't really buy us anything but complexity to new contributors. It's kinda black arts level stuff :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, I wanted to take advantage of the builtin abstract class support. But it's kind of an overkill for this simple use case and like you said, probably makes the code more difficult to understand as well. will switch back to simple inheritance =)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thank you!

Yeah, in this case it's not warranted (It's usually only done for large classes like a protocol in asyncio or twisted or something)

SIDE NOTE: I'm not a fan of the idea of having subclasses for all of our different url options/parameters, I feel like it'll just grow into a long list of "Which class do I need again", but in absence of a better idea I'll be quiet for now :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not a big fan of this either, but I didn't like adding "resolution" to RequestOptions. We don't have a way of dealing with this in the current design. I think the intention might be that once we add "fields" to RequestOptions that would cover all of the URL parameters. This endpoint is kind of an edge case. @LGraber probably have an opinion or two on this.

@@ -0,0 +1,67 @@
####
# This script demonstrates how to use the Tableau Server Client
# to query a high resolution image of a view from Tableau Server.
Copy link
Contributor

Choose a reason for hiding this comment

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

We're not really 'querying' an image are we? More like 'fetch' or 'download'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good catch, download is more appropriate, especially since we are saving the image to a specified filepath

parser = argparse.ArgumentParser(description='Query View Image From Server')
parser.add_argument('--server', '-s', required=True, help='server address')
parser.add_argument('--username', '-u', required=True, help='username to sign into server')
parser.add_argument('--view-name', '-v', required=True, help='name of view')
Copy link
Contributor

Choose a reason for hiding this comment

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

can we expand the help text just a bit please? something like "name of the view we want an image of"

# Step 1: Sign in to server.
tableau_auth = TSC.TableauAuth(args.username, password)
server = TSC.Server(args.server)
server.version = 2.5
Copy link
Contributor

Choose a reason for hiding this comment

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

@t8y8 @LGraber is the inclusion of 'server.version = x' the way we are indicating a min version for the sample? will the new auto-detect version help here or not? if we are going to keep server.version then let's add a comment that says this is the version in which this was introduced.

Copy link
Collaborator

@t8y8 t8y8 Dec 14, 2016

Choose a reason for hiding this comment

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

This will just fail if run against an old Server (actually, it will fail on sign in, not even on the new call).

My PR for detecting the version is outstanding, #100 -- waiting for approval to check it in.

But even then it wouldn't know which calls each version can and cannot make.

@RussTheAerialist had an idea to decorate the calls in the API with what version they use so we could throw a nice "NotSupported" exception or something, that work isn't yet started that I'm aware of (and is sorta dependent on my PR to get the version from Server)

default_project = next((project for project in all_projects if project.is_default()), None)

# Step 3: If default project is found, download the image for the specified view name
if default_project is not None:
Copy link
Contributor

Choose a reason for hiding this comment

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

should we add optional ability to specify the project in the args or is that over-complicating the sample?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good question.

All sites will have a default project, but exposing the ability to choose another might be nice (I think all the samples would want that though)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually the views endpoint returns all the views on the site, so we don't need to specify the project. Copy and pasting problems... I'll remove the project related logic

req_option = TSC.RequestOptions()
req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name,
TSC.RequestOptions.Operator.Equals, args.view_name))
all_views, pagination_item = server.views.get(req_option)
Copy link
Contributor

Choose a reason for hiding this comment

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

@t8y8 use pager here too? is our guidance to always use pager or only use it when you expect to have a lot of items (in this case we'd expect filter to reduce item count)?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ha, good point.

But since he's not searching for one in particular, I don't think it matters much -- he just wants to get a view

image_req_option = TSC.ImageRequestOptions(imageresolution=TSC.ImageRequestOptions.Resolution.High)
server.views.populate_image(view_item, image_req_option)

with open(args.filepath, "wb") as image_file:
Copy link
Contributor

Choose a reason for hiding this comment

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

what does "wb" do here? confusing to me b/c I assume it means workbook but this is an image.

Copy link
Collaborator

Choose a reason for hiding this comment

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

"write binary" it's the standard in python code for opening a file in binary mode for writing


class ImageRequestOptions(RequestOptionsBase):
class Resolution:
High = 'high'
Copy link
Contributor

Choose a reason for hiding this comment

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

is there only one option?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, it defaults to a standard resolution (~800x800), high doubles that.

Copy link
Contributor

Choose a reason for hiding this comment

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

ok. can you please add a comment here that says if you don't specify 'high' that the underlying REST API will default to standard?

with open(POPULATE_PREVIEW_IMAGE, 'rb') as f:
response = f.read()
with requests_mock.mock() as m:
m.get(self.baseurl + '/views/d79634e1-6063-4ec9-95ff-50acbf609ff5/image', content=response)
Copy link
Contributor

Choose a reason for hiding this comment

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

what server will this execute against? is this a valid view ID?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's a mock, he's registering that URL so that when the API calls it it'll hit this fake endpoint and return the image

Copy link
Collaborator

@t8y8 t8y8 left a comment

Choose a reason for hiding this comment

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

🚀 I'll leave it up to you if you want to clean up my final nit

def __init__(self, imageresolution=None):
self.imageresolution = imageresolution

def image_resolution(self, imageresolution):
Copy link
Collaborator

Choose a reason for hiding this comment

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

I called :)

This is unnecessary (and technically slower)

view_item = all_views[0]

# Step 3: Query the image endpoint and save the image to the specified location
image_req_option = TSC.ImageRequestOptions(imageresolution=TSC.ImageRequestOptions.Resolution.High)
Copy link
Contributor

Choose a reason for hiding this comment

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

I agree with Tyler. This is starting to feel heavy to code. The base RequestOptions felt more necessary because fields was more than a simple hash. As I watch this code being written in our samples, though, it is feeling heavier and heavier. It feels like the simple hash syntax might be more lightweight and we can still validate it. I am okay with you checking this in since it is in vnext but I think we should discuss this as I want users to be able to write minimal code that is easy to read and it is starting to feel like perhaps our use of enums and strongly typed objects is making the python harder and less ... pythonic. I am open to discussion.

Copy link
Contributor

Choose a reason for hiding this comment

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

I meant to say filter, not fields. Fields, also, was going to be interesting.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks @LGraber , I opened #114 to track this idea -- I've been googling how some other libraries do it for inspiration (nothing struck me yet), but that'll make it so we don't lose the convo in this PR

@jz-huang
Copy link
Contributor Author

I'm merging this in for now so Ben can WAM, let's keep the discussion going in #114.

@jz-huang jz-huang merged commit 04398da into tableau:vnext Dec 15, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants