score.jsapi

This module provides a convenient API for exposing python functions into javascript. The humble intent of this module is to make basic communication between a javascript client and a score server as easy as possible. It is not meant to provide means for constructing APIs which can be accessed by outsiders.

Quickstart

Create an endpoint:

from score.jsapi import UrlEndpoint

math = UrlEndpoint('math')

Annotate all python functions that you want to expose with your endpoint’s op property:

@math.op
def add(ctx, num1, num2):
    return num1 + num2

@math.op
def divide(ctx, dividend, divisor):
    return dividend / divisor

Initialize the module and configure it to use your newly defined endpoint object:

[score.init]
modules =
    ...
    score.jsapi

[jsapi]
endpoint = path.to.your.endpoint
# Important: the next value exposes internal information, like stack
# traces, for debuggin purposes. On production systems, this value should
# be omitted or set to False!
expose = True

Your functions can now be accessed through the auto-generated javascript files:

<script>
    // assuming an AMD environment where all javascript
    // is loaded automatically:
    require(['score.jsapi'], function(api) {
        api.add(40, 2).then(function(result) {
            console.log('40 + 2 = ' + result);
        });
        api.divide(40, 0).catch(function(error) {
            console.error(error);
        });
    });
</script>

Configuration

score.jsapi.init(confdict, ctx, tpl, http)[source]

Initializes this module acoording to our module initialization guidelines with the following configuration keys:

endpoint None
Optional single endpoint value that can be provided for convenience. See endpoints, below, for details.
endpoints list()
A list of dotted paths pointing to any amount of Endpoints. The registered functions of these Endpoints will be available in javascript.
expose False
Whether security critical data may be exposed through the API. This value should be left at its default value in production, but may be switched to True during development to receive Exceptions and stacktraces in the browser console.
serve.outdir None
A folder, where this module’s score.serve worker will dump all javascript files required to make use of this module in a javascript environment.

Details

Javascript access

All auto-generated javascript is wrapped in a UMD block, which means that you can access everything in your favorite manner:

// AMD
require(['score/jsapi'], function(api) {
    api.add(40, 2).then(...);
});

// CommonJS
var api = require('score/jsapi');
api.add(40, 2).then(...);

// browser globals
var api = score.jsapi.unified;
api.add(40, 2).then(...);

You can retrieve a list of all javascript files using the configured jsapi object’s tpl_loader member, which is a template loader for score.tpl. The templates provided by the loader are in the correct order to be included by your javascript loader without causing any dependency errors:

>>> list(jsapi.tpl_loader.iter_paths())
['score/jsapi/exception.js',
 'score/jsapi/excformat.js',
 'score/jsapi/endpoint.js',
 'score/jsapi/queue.js',
 'score/jsapi/unified.js',
 'score/jsapi/endpoint/url.js',
 'score/jsapi/endpoints/math.js',
 'score/jsapi/exceptions.js',
 'score/jsapi.js']

Exceptions

This module will not send python exceptions to the javascript by default. If an unexpected error occurs, the promise will be rejected with an instance of this module’s Exception class, which extends javascript’s builtin Error class, but does not contain an error message by default:

api.divide(1, 0).then(function(result) {
    // we're dividing by zero, so this block will never be executed
}).catch(function(error) {
    console.log(error.message);  // will log the empty string
});

These conservative defaults were chosen to prevent accidental information leaks during production. You can enable exception logging manually, if you want to (see the next section).

For cases, where you want to pass exception information from python to javascript, you need to define SafeExceptions <SafeException>:

from score.jsapi import SafeException

class MyZeroDivisionException(SafeException):

    def __init__(self, dividend):
        super().__init__('Trying to divide %d by 0' % (dividend,))

@math.op
def divide(ctx, dividend, divisor):
    try:
        return dividend / divisor
    except ZeroDivisionError:
        raise MyZeroDivisionException(dividend)

You can then handle these exceptions in your javascript client:

api.divide(1, 0).then(function(result) {
    // we're dividing by zero, so this block will never be executed
}).catch(function(error) {
    console.log(error.message);  // "Trying to divide 1 by 0"
});

All SafeException subclasses detected during initialization will also be exposed to javascript. This allows usage like the following:

api.divide(1, 0).then(function(result) {
    // we're dividing by zero, so this block will never be executed
}).catch(function(error) {
    if (error instanceof api._exceptions.MyZeroDivisionException) {
        alert("Cannot divide by 0")
    } else {
        throw error;
    }
});

Exposing debugging information

The configuration value expose will send stack traces from the python backend to the javascript client and log them into the browser console:

api.divide(1, 0).then(function(result) {
    // we're dividing by zero, so this block will never be executed
    //
    // instead, the console will contain the python stack trace, even if
    // this is not a SafeException:
    //
    //   Error in jsapi call divide ( 1 , 0 )
    //   Traceback (most recent call last):
    //     File "/path/to/math.py", line "3", in divide
    //       return dividend / divisor
    //
    //   ZeroDivisionError: division by zero
});

Versioning

After deploying an application using score.jsapi, the maintenance work will require you to alter the implementation of a python function at some point. You can achieve this in a backward-compatible manner by creating versions of your functions:

@math.op
def divide(ctx, dividend, divisor):
    return dividend / divisor

@divide.version(2)
def divide(ctx, dividend, divisor):
    try:
        return dividend / divisor
    except ZeroDivisionError:
        raise MyZeroDivisionException(dividend)

The javascrtipt client will always call the latest version it knows of by default. This means that older javascript clients will keep accessing the initial version of the function, while newer clients will use version 2 of your function.

All version names are converted to string internally to determine the ordering of your versions. The initial implementation always has the empty string as version name, which will always cause it be regarded as the first version after sorting through the version strings.

API

Configuration

score.jsapi.init(confdict, ctx, tpl, http)[source]

Initializes this module acoording to our module initialization guidelines with the following configuration keys:

endpoint None
Optional single endpoint value that can be provided for convenience. See endpoints, below, for details.
endpoints list()
A list of dotted paths pointing to any amount of Endpoints. The registered functions of these Endpoints will be available in javascript.
expose False
Whether security critical data may be exposed through the API. This value should be left at its default value in production, but may be switched to True during development to receive Exceptions and stacktraces in the browser console.
serve.outdir None
A folder, where this module’s score.serve worker will dump all javascript files required to make use of this module in a javascript environment.
class score.jsapi.ConfiguredJsapiModule(ctx, tpl, http, endpoints, expose, serve_outdir)[source]

This module’s configuration class.

The object also provides a worker for score.serve, which will dump all javascript files generated by this module into a folder that was configured as serve.outdir.

tpl_loader

An instance of score.tpl.loader.Loader, that provides all templates required to use this module in the correct order.

Endpoints

class score.jsapi.Endpoint(name)[source]

An endpoint capable of handling requests from javascript.

op(func)[source]

Registers an operation with this Endpoint. It will be available with the same name and the same number of arguments in javascript. Note that javascript has no support for keyword arguments and keyword-only parameters will confuse this function.

call(name, version, arguments, ctx_members={})[source]

Calls function with given name and the given list of arguments.

It is also possible to set some context members before calling the actual handler for the operation.

Will return a tuple consisting of a boolean success indicator and the actual response. The response depends on two factors:

  • If the call was successfull (i.e. no exception), it will contain the return value of the function.
  • If a non-safe exception was caught (i.e. one that does not derive from SafeException) and the module was configured to expose internal data (via the init configuration value “expose”), the response will consist of the json-convertible representation of the exception, which is achievede with the help of exc2json()
  • If a SafeException was caught and the module was configured not to expose internal data, it will convert the exception type and message only (again via exc2json()). Thus, the javascript part will not receive a stack trace.
  • The last case (non-safe exception, expose is False), the result part will be None.
class score.jsapi.UrlEndpoint(name, *, method='POST')[source]

An Endpoint, which can be accessed via AJAX from javascript.

handle(requests, ctx_members={})[source]

Handles all functions calls passed with a request.

The provided requests variable needs to be a list of “calls”, where each call is a list containing the function name as first entry, the version of the operation as second entry, and the arguments for the invocation as the rest of the list. Example value for requests with a call to the initial version of an addition function and a call to the second version of a division function:

[["add", "", 40, 2],
 ["divide", "2", 42, 0]]

The return value will be a list with two elements, one containing success values, the other containing results:

[[True, False], [42, None]]

See Endpoint.call() for details on the result values, especially the explanation of the None value, above.

The input and output is already in the correct format for communication with the javascript part, so the result can be sent as “application/json”-encoded response to the calling javascript function.

class score.jsapi.SafeException[source]

An Exception type, which indicates that the exception is safe to be transmitted to the client—even in production. The javascript API will reject the call promise with an instance of score.jsapi.Exception, or its equivalent “score/jsapi/Exception” in AMD and CommonJS.

Example in python …

class CustomError(SafeException):
    pass

@endpoint.op
def divide(ctx, dividend, divisor):
    if not divisor:
        raise CustomError('Cannot divide by zero')
    return dividend / divisor

… and javascript:

api.divide(1, 0).then(function(result) {
    console.log('1 / 0 = ' + result);
}).catch(function(exc) {
    console.error('Error (' + exc.name + '): ' + exc.message);
    // will output:
    //   Error (CustomError): Cannot divide by zero
});