Note
Do not edit this document, the source is reStructured Text elsewhere.
Contents
This tutorial is a companion piece to the client side scope tutorial describing how to implement the same scope but to run on a server instead of on the device.
We'll implement it using Python to make it easy to read, but there's nothing language-specific about this code, you could implement it just as easily using Go, PHP, or Java.
All you need to implement are two methods, search and preview and expose them. To make them available via HTTP I'll use bottle which is a very small web framework, but you can use the one you prefer with minimal changes.
The example is a server that searches the Open Clipart Library to find graphics matching the query terms. The API is described at https://openclipart.org/developers
The reference documentation for REST Scopes, which describes the required API is here
The search method simply takes parameters and returns search results in the proper format, as defined in the REST Scopes API
The REST scope has to support a number of parameters for search, most of which are not very useful in the context of searching for clipart, so we'll only use q (the query text) and limit, the optional maximum number of results.
Here's the code for search:
@route('/search') def search(): # We are ignoring all optional parameters other than limit q = request.query.get('q') limit = request.query.get('limit', 20) if not q: #empty query return params={ 'query': q, 'amount': limit, } r = requests.get('https://openclipart.org/search/json/', params=params) # Check for errors if r.status_code != 200: raise HTTPError(status=r.status_code) if r.headers['content-type'] != 'application/json; charset=utf-8': raise HTTPError(status=502) # To see what data looks like, check # https://openclipart.org/search/json/?query=christmas&page=1&amount=20 data = r.json()['payload'] # We need to return at least one category object, as described in the docs category = { 'category': { 'id': 'graphics', 'title': 'Graphics', 'icon': 'https://openclipart.org//people/rejon/openclipart-big-scissors.svg', 'render_template': json.dumps({ # See https://docs.google.com/a/canonical.com/document/d/1NmiM4UCnJgf6IEawmfyTOHRNAA5ZGrqpyrPqPOibwc8/edit# "schema-version": 1, "template": { "category-layout": "grid", "card-layout": "horizontal", "card-size": "small", }, "components": { "title": "title", "subtitle": "subtitle", "mascot": "mascot", }, }), }, } # And the results, of course results = [category] for item in data: results.append({ 'result':{ 'cat_id': 'graphics', 'uri': item['detail_link'], 'title': item['title'], 'subtitle': item['uploader'], 'mascot': item['svg']['url'], 'description': item['description'], 'detail_link': item['detail_link'], } }) # Now, we convert that into the format defined in the REST Scope docs # one JSON object per row, first category, then results, with # application/json content_type response.content_type = 'application/json' result_data = '\r\n'.join(json.dumps(r) for r in results) return result_data
The preview method takes a result (exactly like the one returned by search) plus optional parameters and returns some JSON describing how that result should be presented to the user.
@route('/preview') def preview(): result = json.loads(request.query.get('result')) # We have to create JSON describing the desired preview for this result widgets = [{ 'widget' : { 'id': 'image', 'type': 'image', 'source': result['mascot'], }}, {'widget' : { 'id': 'name', 'type': 'header', 'title': result['title'], 'subtitle': result['subtitle'], }}, {'widget' : { 'id': 'description', 'type': 'text', 'text': result['description'], }}, {'widget' : { 'id': 'buttons', 'type': 'actions', 'actions': [{ 'label': 'view on OpenClipart', 'uri': result['detail_link'], 'id': 'action1', }], }}, ] response.content_type = 'application/json' result_data = '\r\n'.join(json.dumps(w) for w in widgets) return result_data
EXPLAIN AGAIN ONCE ALL THIS TOWER OF THINGS IS IN DISTRO
To test a REST scope, you need a scope server that points to your scope. Luckily, this is very easy to do, and we can even make the scope itself do it. Here's the code for that:
@route('/remote-scopes') def remote(): """This is just for testing purposes, doesn't belong in production.""" response.content_type = 'application/json' return json.dumps([{ 'id': 'com.canonical.scopes.openclipart', 'name': 'OpenClipart', 'description': 'Graphics from the Open Clipart Library', 'base_url': 'http://localhost:8000', 'art': 'https://openclipart.org//people/rejon/openclipart-big-scissors.svg', 'author': 'Canonical', }])
Remember to comment or remove that code before you put this scope in production!
Finally, to test the scope:
Stop your smartscopesproxy: stop smart-scopes-proxy
Start your scope in localhost: python ./openclipart_scope.py
Start your smartscopesproxy pointing at the test code:
cd /usr/lib/x86_64-linux-gnu/smartscopesproxy/ ./smartscopesproxy http://localhost:8000
Start unity-scope-tool: UNITY_FORCE_NEW_SCOPES=1 unity-scope-tool
Navigate to your scope sliding the top bar, and test as needed.
The OpenClipart scope in action!
And finally, here's the full source, including bottle boilerplate, ready to try:
import json from bottle import ( response, request, route, run, ) import requests # Handler for searches @route('/search') def search(): # We are ignoring all optional parameters other than limit q = request.query.get('q') limit = request.query.get('limit', 20) if not q: #empty query return params={ 'query': q, 'amount': limit, } r = requests.get('https://openclipart.org/search/json/', params=params) # Check for errors if r.status_code != 200: raise HTTPError(status=r.status_code) if r.headers['content-type'] != 'application/json; charset=utf-8': raise HTTPError(status=502) # To see what data looks like, check # https://openclipart.org/search/json/?query=christmas&page=1&amount=20 data = r.json()['payload'] # We need to return at least one category object, as described in the docs category = { 'category': { 'id': 'graphics', 'title': 'Graphics', 'icon': 'https://openclipart.org//people/rejon/openclipart-big-scissors.svg', 'render_template': json.dumps({ # See https://docs.google.com/a/canonical.com/document/d/1NmiM4UCnJgf6IEawmfyTOHRNAA5ZGrqpyrPqPOibwc8/edit# "schema-version": 1, "template": { "category-layout": "grid", "card-layout": "horizontal", "card-size": "small", }, "components": { "title": "title", "subtitle": "subtitle", "mascot": "mascot", }, }), }, } # And the results, of course results = [category] for item in data: results.append({ 'result':{ 'cat_id': 'graphics', 'uri': item['detail_link'], 'title': item['title'], 'subtitle': item['uploader'], 'mascot': item['svg']['url'], 'description': item['description'], 'detail_link': item['detail_link'], } }) # Now, we convert that into the format defined in the REST Scope docs # one JSON object per row, first category, then results, with # application/json content_type response.content_type = 'application/json' result_data = '\r\n'.join(json.dumps(r) for r in results) return result_data # Handler for previews @route('/preview') def preview(): result = json.loads(request.query.get('result')) # We have to create JSON describing the desired preview for this result widgets = [{ 'widget' : { 'id': 'image', 'type': 'image', 'source': result['mascot'], }}, {'widget' : { 'id': 'name', 'type': 'header', 'title': result['title'], 'subtitle': result['subtitle'], }}, {'widget' : { 'id': 'description', 'type': 'text', 'text': result['description'], }}, {'widget' : { 'id': 'buttons', 'type': 'actions', 'actions': [{ 'label': 'view on OpenClipart', 'uri': result['detail_link'], 'id': 'action1', }], }}, ] response.content_type = 'application/json' result_data = '\r\n'.join(json.dumps(w) for w in widgets) return result_data # Handler for scope listing @route('/remote-scopes') def remote(): """This is just for testing purposes, doesn't belong in production.""" response.content_type = 'application/json' return json.dumps([{ 'id': 'com.canonical.scopes.openclipart', 'name': 'OpenClipart', 'description': 'Graphics from the Open Clipart Library', 'base_url': 'http://localhost:8000', 'art': 'https://openclipart.org//people/rejon/openclipart-big-scissors.svg', 'author': 'Canonical', }]) if __name__ == '__main__': run(host='localhost', port=8000)