#!/usr/bin/env python # coding: utf-8 # # A Minimal Python Client for MyBinder # # Some experiments trying to figure out how to convert juniper.js to Python so we can run code in a remote MyBinder code execution sandbox using a minimal Python API.... # In[5]: #!pip3 install aiohttp asyncio # ## Juniper.js # # - https://github.com/ines/juniper/blob/master/src/index.js # # *Juniper.js* is a minimal javascript client, based on *thebelab.js*, that shows how to: # # - launch a mybinder repo; # - request a kernel; # - run code on the kernel. # # Can we use it as a crib for a minimal Python client for using MyBinder as a simple remote code execution environment? # ## Fire Up a MyBinder Repo # # We start a MyBinder repo from a standardised URL: # In[87]: import requests url = 'https://mybinder.org' branch = 'master' repo = 'binder-examples/requirements' binderURL = '{url}/build/gh/{repo}/{branch}'.format(url=url, repo=repo, branch=branch) binderURL # In[29]: #!pip3 install sseclient-py # The connection seems to be a streaming one, so let's see if we can fire one up: # In[166]: import json import pprint import sseclient def with_urllib3(url): """Get a streaming response for the given event feed using urllib3.""" import urllib3 http = urllib3.PoolManager() return http.request('GET', url, preload_content=False) def with_requests(url): """Get a streaming response for the given event feed using requests.""" import requests return requests.get(url, stream=True) response = with_urllib3(binderURL) # or with_requests(url) client = sseclient.SSEClient(response) for event in client.events(): msg = json.loads(event.data) pprint.pprint(msg) # So it looks like we can start a MyBinder server running. # # Let's grab the details... # In[167]: resp = json.loads(event.data) resp # Inspecting the Juniper.js traffic, it looks like good form to create some sort of numerical connection ID, perhaps as a callback identifie?, although in practice it appears to be optional. Let's create one anyway: # In[168]: #We can generate a rand session ID as a long-ish number import random random.getrandbits(32) # Now we can make a request for a kernel: # In[169]: import requests randsessionId = random.getrandbits(32) r = requests.post('{}api/kernels?{}'.format(resp['url'], randsessionId), headers={'Authorization': 'token {}'.format(resp['token'])}) r.json() # The kernel conversation is handled over a websocket. So we need to create a websocket connection # In[170]: wss = 'wss://{}api/kernels/{}/channels?token={}'.format(resp['url'].split('//')[1], r.json()['id'], resp['token']) #Also need &session_id=NNNN; does wss supply that? wss # It looks like we can fire up a websocket client... # In[185]: #https://github.com/websocket-client/websocket-client #!pip3 install websocket_client wsaccel import json import websocket ws = websocket.WebSocket() ws.connect(wss) # In[172]: from uuid import uuid4 session = str(uuid4()) # In[173]: kernel_info_request = { 'channel': 'shell', 'header': { 'msg_type': 'kernel_info_request', 'msg_id': str(uuid4()), 'username': '', 'session': session, 'version': "5.2" }, 'parent_header':{}, 'metadata': {}, 'content': {} } # In[178]: ws.send( json.dumps(kernel_info_request) ) ws.recv() # In[179]: ws.recv() # In[180]: ws.recv() # In[187]: iopub_messages = [] shell_messages = [] ws.send( json.dumps(kernel_info_request) ) import time got_execute_reply = False got_idle_status = False while not (got_execute_reply and got_idle_status): time.sleep(1) msg = json.loads(ws.recv()) print(msg) if msg['channel'] == 'shell': shell_messages.append(msg) # an execute_reply message signifies the computation is done if msg['header']['msg_type'] == 'kernel_info_reply': got_execute_reply = True elif msg['channel'] == 'iopub': iopub_messages.append(msg) # the kernel status idle message signifies the kernel is done if (msg['header']['msg_type'] == 'status' and msg['content']['execution_state'] == 'idle'): got_idle_status = True # In[158]: code = 'print("Hello World")' # In[159]: execute_request = { 'channel': 'shell', 'header': { 'msg_type': 'execute_request', 'msg_id': str(uuid4()), 'username': '', 'session': session, }, 'parent_header':{}, 'metadata': {}, 'content': { 'code': code, 'silent': False, 'stop_on_error': False, 'user_expressions': {}, 'allow_stdin': True, 'store_history': True, } } #code: "%matplotlib inline↵import numpy as np↵import matplotlib.pyplot as plt↵plt.ion()↵fig, ax = plt.subplots()↵ax.scatter(*np.random.randn(2, 100), c=np.random.randn(100))↵ax.set(title="Wow, an interactive plot!")" # In[ ]: ws.send( json.dumps(execute_request) ) # In[161]: ws.recv() # In[162]: ws.recv() # In[201]: #https://github.com/sagemath/sagecell/blob/master/contrib/sagecell-client/sagecell-client.py """ A small client illustrating how to interact with the Sage Cell Server, version 2 Requires the websocket-client package: http://pypi.python.org/pypi/websocket-client """ import websocket import json import requests class MyBinderCell(object): def __init__(self, binderURL, timeout=10): response = with_urllib3(binderURL) # or with_requests(url) client = sseclient.SSEClient(response) for event in client.events(): #msg = json.loads(event.data) #pprint.pprint(msg) pass resp = json.loads(event.data) self._binder = resp randsessionId = random.getrandbits(32) r = requests.post('{}api/kernels?{}'.format(resp['url'], randsessionId), headers={'Authorization': 'token {}'.format(resp['token'])}) wss = 'wss://{}api/kernels/{}/channels?token={}'.format(resp['url'].split('//')[1], r.json()['id'], resp['token']) self.kernel_url = wss print(self.kernel_url) websocket.setdefaulttimeout(timeout) self._ws = websocket.WebSocket() self._ws.connect(wss) # initialize our list of messages self.shell_messages = [] self.iopub_messages = [] def _wait_on_response(self, response): # Wait until we get both a kernel status idle message and an execute_reply message got_execute_reply = False got_idle_status = False while not (got_execute_reply and got_idle_status): msg = json.loads(self._ws.recv()) if msg['channel'] == 'shell': self.shell_messages.append(msg) # an execute_reply message signifies the computation is done if msg['header']['msg_type'] == response: got_execute_reply = True elif msg['channel'] == 'iopub': self.iopub_messages.append(msg) # the kernel status idle message signifies the kernel is done if (msg['header']['msg_type'] == 'status' and msg['content']['execution_state'] == 'idle'): got_idle_status = True return {'shell': self.shell_messages, 'iopub': self.iopub_messages} def execute_request(self, code): # zero out our list of messages, in case this is not the first request self.shell_messages = [] self.iopub_messages = [] # Send the JSON execute_request message string down the shell channel msg = self._make_execute_request(code) self._ws.send(msg) return self._wait_on_response('execute_reply') def _make_execute_request(self, code): from uuid import uuid4 session = str(uuid4()) # Here is the general form for an execute_request message execute_request = { 'channel': 'shell', 'header': { 'msg_type': 'execute_request', 'msg_id': str(uuid4()), 'username': '', 'session': session, }, 'parent_header':{}, 'metadata': {}, 'content': { 'code': code, 'silent': False, 'stop_on_error': False, 'user_expressions': {}, 'allow_stdin': True, 'store_history': True, } } return json.dumps(execute_request) def _make_kernel_info_request(self): kernel_info_request = { 'channel': 'shell', 'header': { 'msg_type': 'kernel_info_request', 'msg_id': str(uuid4()), 'username': '', 'session': session, 'version': "5.2" }, 'parent_header':{}, 'metadata': {}, 'content': {} } return json.dumps(kernel_info_request) def kernel_info__request(self): # zero out our list of messages, in case this is not the first request self.shell_messages = [] self.iopub_messages = [] # Send the JSON execute_request message string down the shell channel msg = self._make_kernel_info_request() self._ws.send(msg) return self._wait_on_response('kernel_info_reply') def close(self): # If we define this, we can use the closing() context manager to automatically close the channels self._ws.close() # In[202]: b = MyBinderCell(binderURL) # In[203]: b._binder # In[204]: b.kernel_info__request() # In[205]: b.execute_request("print('hello world')") # In[ ]: