Question

Moving a Look between Looker servers using the Looker API and the python requests library

  • 5 December 2016
  • 5 replies
  • 865 views

Userlevel 4

Moving a look from one looker server to another (or the same!)


For this example, I am hoping to take a look via a look ID and copy that to another server. This is really helpful in promoting content from a development to a production server.


I use python, and while you can use the Looker SDK, I prefer to use the python requests library. For this case I’ve put everything together in one file, but there are other best practices you can follow for managing API keys and separating these into several files.


# -*- coding: UTF-8 -*-
import requests
from pprint import pprint as pp
import json

from requests.packages.urllib3.exceptions import InsecureRequestWarning
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

class LookerApi(object):

def __init__(self, token, secret, host):

self.token = token
self.secret = secret
self.host = host

self.session = requests.Session()
self.session.verify = False

self.auth()

def auth(self):
url = '{}{}'.format(self.host,'login')
params = {'client_id':self.token,
'client_secret':self.secret
}
r = self.session.post(url,params=params)
access_token = r.json().get('access_token')
# print access_token
self.session.headers.update({'Authorization': 'token {}'.format(access_token)})

# GET /looks/
def get_look_info(self,look_id,fields=''):
url = '{}{}/{}'.format(self.host,'looks',look_id)
print url
params = {"fields":fields}
r = self.session.get(url,params=params)
if r.status_code == requests.codes.ok:
return r.json()

# GET /queries/
def get_query(self,query_id):
url = '{}{}/{}'.format(self.host,'queries',query_id)
print url
params = {}
r = self.session.get(url,params=params)
if r.status_code == requests.codes.ok:
return r.json()

# POST /queries/
def create_query(self,query_body, fields):
url = '{}{}'.format(self.host,'queries')
print url
params = json.dumps(query_body)
print " --- creating query --- "
r = self.session.post(url,headers = {'Content-type': 'application/json'},data=params, params = json.dumps({"fields": fields}))
if r.status_code == requests.codes.ok:
return r.json()

# POST /looks/
def create_look(self,look_body):
url = '{}{}'.format(self.host,'looks')
print url
params = json.dumps(look_body)
r = self.session.post(url,data=params)
if r.status_code == requests.codes.ok:
return r.json()

Then I log in with my api keys


dest_host = 'my destination host' ## i.e. https://looker.looker.com:19999/api/3.0/
dest_secret = 'my destination secret'
dest_token = 'my destination key'

dest_looker = LookerApi(host=dest_host,
token=dest_token,
secret = dest_secret)

source_host = 'my source host'
source_secret = 'my source secret'
source_token = 'my source key'

source_looker = LookerApi(host=source_host,
token=source_token,
secret = source_secret)

Now that I have the API ready to go, I follow my move look process:


source_name= 'my source'
source_look= 123
destination_space = '4'

look_body = source_looker.get_look_info(source_look)
print "---- Source Look Body ----"
pp(look_body)

print "---- Source query ----"
query_body = source_looker.get_query(look_body['query_id'])
pp(query_body)

print "---- New query ----"
new_query = dest_looker.create_query(query_body,'id')
new_query_id = str(new_query['id'])
print new_query_id+" is the new query id"


new_look = {}
new_look['space_id'] = destination_space
new_look['query_id'] = new_query_id
new_look['title'] = look_body['title'] + " copy from "+source_name
destination_look = dest_looker.create_look(new_look)

pp(destination_look)

When compiled, your output should look like this [shortened and masked]


https://host_name.looker.com:19999/api/3.0/looks/1
---- Source Look Body ----
{u'can': {u'copy': True,
u'create': True,
u'destroy': True,
u'dow
w': True,

...
u'show_details': True,
u'sudo': True},
u'id': 100},
u'user_id': 100,
u'view_count': None}
---- Source query ----
https://host_name.looker.com:19999/api/3.0/queries/5880
{u'can': {u'cost_estimate': True,
u'create': True,
u'download': True,
u'exp...
': None,
u'visible_ui_sections': None}
---- New query ----
https://destination_name.looker.com:19999/api/3.0/queries
--- creating query ---
47660 is the new query id
---- New Look ----
https://destination_name.looker.com:19999/api/3.0/looks
{u'can': {u'copy': True,
u'create': True,
u'destroy': True,
u'download': True,
u'find_a
...
w_details': True,
u'sudo': False},
u'id': 758},
u'user_id': 758,
u'view_count': None}

If you end up with a block of JSON printed out, that indicates you have successfully created the new Look.


Check out the full script called move_look.py here: https://github.com/llooker/python_api_samples



Note: the link will use a LookerAPI.py file to hold the class, and a configuration file for keys. Check the readme for setting this up.



5 replies

Userlevel 2

Eric, thank you very much for sharing this information.

There’s a typo when you are getting the look_body. It should be source instead of dest in the following block of code. After fixing that, it worked good.


source_look= 123
destination_space = '4'

look_body = source_looker.get_look_info(source_look)
print "---- Source Look Body ----"
pp(look_body)
Userlevel 1

Hi there, thanks for this post.

I tried to use the same approach but with the Swagger-generated API.

I can do the same stuff, i.e. find the query of interest and retrieve the query_body.

But when I call create_query, I get the error message shown below.

It’s pretty generic so I’m not sure if it’s due to swaggerAPI, or something else.

Any suggestions much appreciated…


looker.rest.ApiException: (500)

Reason: Internal Server Error

HTTP response headers: HTTPHeaderDict({‘Content-Length’: ‘82’, ‘X-Content-Type-Options’: ‘nosniff’, ‘Vary’: ‘Accept-Encoding’, ‘Server’: ‘nginx/1.10.1’, ‘Connection’: ‘keep-alive’, ‘Date’: ‘Thu, 20 Apr 2017 23:40:05 GMT’, ‘Content-Type’: ‘application/json;charset=utf-8’})

HTTP response body: {“message”:“An error has occurred.”,“documentation_url”:“http://docs.looker.com/”}

Userlevel 1

OK after a bunch of trial&error, I’ve answered my own question, I’ll post here in case it helps someone…

The problem is with the ‘dynamic_fields’, it doesn’t seem to like that being set when creating a query, even when copying a previous query as in the original example. If I set it to an empty array, it works OK (but of course the dynamic fields are not copied).

Seems like it would be good if the Looker API could deal with this field. Or if not, it would be good if it could return a better error message, thanks!

Userlevel 4

I’ve found that table calculations sometimes throw an issue if there is a % anywhere in the call. Adding this to the POST call (i’ve actually modified the example above) should fix some of those problems.



the final product looks like this:


    # POST /queries/
def create_query(self,query_body, fields):
url = '{}{}'.format(self.host,'queries')
print url
params = json.dumps(query_body)
print " --- creating query --- "
r = self.session.post(url,headers = {'Content-type': 'application/json'},data=params, params = json.dumps({"fields": fields}))
if r.status_code == requests.codes.ok:
return r.json()

Are you using custom Python or the Swagger output on its own?

Userlevel 1

Thanks Eric for your answer.

I am using a python program calling the swagger-generated SDK. That seems quite plausible that it’s a % sign in the data, I will look into that more carefully, maybe the swagger generation is not treating it properly.

Reply