Tangelo Web Services¶
Tangelo’s special power lies in its ability to run user-created web services as part of a larger web application. Essentially, each Python file in Tangelo’s web space is associated to a URL; requesting this URL (e.g., by visiting it in a browser) will cause Tangelo to load the file as a Python module, run a particular function found within it, and return the output as the content for the URL.
In other words, Tangelo web services mean that Python code can become web resources. Python is a flexible and powerful programming language with a comprehensive standard library and a galaxy of third-party modules providing access to all kinds of APIs and software libraries.
General Services¶
Here is a simple example of a web service. Suppose
/home/riker/tangelo_html/calc.py
reads as follows:
import tangelo
allowed = ["add", "subtract", "multiply", "divide"]
@tangelo.types(a=float, b=float)
def run(operation, a=None, b=None):
if a is None:
return "Parameter 'a' is missing!"
elif b is None:
return "Parameter 'b' is missing!"
try:
if operation == "add":
return a + b
elif operation == "subtract":
return a - b
elif operation == "multiply":
return a * b
elif operation == "divide":
return a / b
else:
return "Unsupported operation: %s\nAllowed operations are: %s" % (operation, ", ".join(allowed))
except ValueError:
return "Could not %s '%s' and '%s'" % (operation, a, b)
except ZeroDivisionError:
return "Can't divide by zero!"
This is a Python module named calc
, implementing a very rudimentary
four-function calculator in the run()
function. Tangelo will respond to a
request for the URL http://localhost:8080/examples/calculator/calc/add?a=33&b=14
(without the trailing .py
) by loading calc.py
as a Python module,
executing its run()
function, and returning the result - in this case, the
string 47
- as the contents of the URL.
The run()
function takes three arguments: a positional argument named
operation
, and two keyword arguments named a
and b
. Tangelo maps
the positional arguments to any “path elements” found after the name of the
script in the URL (in this case, add
), while keyword arguments are mapped to
query parameters (33
and 14
in this case). In other words, the example
URL is the equivalent of running the following short Python script:
import calc
print calc.run("add", "33", "14")
Note that all arguments are passed as strings. This is due to the way URLs
and associated web technologies work - the URL itself is simply a string, so it
is chunked up into tokens which are then sent to the server. These arguments
must therefore be cast to appropriate types at run time. The
tangelo.types()
decorator offers a convenient way to perform this type
casting automatically, but of course you can do it manually within the service
itself if it is necessary.
Generally speaking, the web endpoints exposed by Tangelo for each Python file
are not meant to be visited directly in a web browser; instead, they provide
data to a web application using Ajax calls to retrieve the data. Suppose we
wish to use calc.py
in a web calculator application, which includes an HTML
file with two fields for the user to type inputs into, and four buttons, one for
each arithmetic operation. An associated JavaScript file might have code like
the following:
function do_arithmetic(op) {
var a_val = $("#input-a").val();
var b_val = $("#input-b").val();
$.ajax({
url: "calc/" + op,
data: {
a: a_val,
b: b_val
},
dataType: "text",
success: function (response) {
$("#result").text(response);
},
error: function (jqxhr, textStatus, reason) {
$("#result").html(reason);
}
});
}
$("#plus").click(function () {
do_arithmetic("add");
});
$("#minus").click(function () {
do_arithmetic("subtract");
});
$("#times").click(function () {
do_arithmetic("multiply");
});
$("#divide").click(function () {
do_arithmetic("divide");
});
The do_arithmetic()
function is called whenever the operation buttons are
clicked; it contains a call to the JQuery ajax()
function, which prepares a
URL with query parameters then retrieves data from it. The success
callback
then takes the response from the URL and places it on the webpage so the user
can see the result. In this way, your web application front end can connect to
the Python back end via Ajax.
Return Types¶
The type of the value returned from the run()
function determines how
Tangelo creates content for the associated web endpoint. Since web server
communication occurs via textual data, all values returned by web services must
eventually be converted to strings. By default, Tangelo accomplishes this by
considering all such values to be JSON-encoded. For example, in the calculator
example, the run()
function returns Python int
value 47
; Tangelo
takes this value and applies the standard function json.dump()
to it,
resulting in the string "47"
, which is delivered to the client for further
processing. Similarly, a service that returns a Python dict
value will be
converted to a general JSON-object, making it easy to return structured
information from any given web service.
This means that, in the most general case, you can create your own types, equipped with methods for JSON encoding them, and use those are direct return values (see the Python documentation for information on custom JSON encoding). Attempting to return a type that is not JSON-serializable results in a 400 error.
The only exception to the default conversion behavior is that if the service
returns a string directly, this value will not be JSON encoded (which entails
surrounding it with double-quotes), but simply passed along unchanged. This
“escape hatch” enables a service to return any kind of data by encoding it as a
string. The tangelo.content_type()
utility function can be used to specify
the intended type of the returned data. For instance,
tangelo.content_type("text/plain")
followed by return "hello, world"
will result in a text result being sent to the client. More complex types are
also possible; e.g., a service might compute a PNG image, then send the PNG data
back as a string after calling tangelo.content_type("application/png")
.
Specifying a Custom Return Type Converter¶
Similarly to the tangelo.types()
decorator mentioned above, services
can specify a custom return type via the tangelo.return_type()
decorator. It takes a single argument, a function to convert the object
returned from the service function to a string or JSON-serializable value (see
Return Types):
import tangelo
def excited(s):
return s + "!!!"
@tangelo.return_type(excited)
def run(name):
return "hello %s" % (name)
Given Data
as an input, this service will return the string Hello
Data!!!
to the client.
A more likely use case for this decorator is special-purpose JSON converters,
such as Pymongo’s bson.json_util.dumps()
function, which can handle certain
non-standard objects such as Python datetime
objects when converting to JSON
text.
HTTP Status Codes¶
When something goes wrong during execution of a web service, you may wish to
signal to the client what happened. The tangelo.http_status()
function can
be used to set the status code to indicate the class of problem. For instance,
if the service invocation does not include the proper required arguments, the
service might signal the error by the following:
tangelo.http_status(400, "Required Argument Missing")
Many HTTP status codes have standard meanings, including default
titles (e.g., the default title for 400 is “Bad Request”); invoking
tangelo.http_status()
with only a numerical code will use such a default
title. Otherwise, you may include a second string argument to provide a more
specific description.
Errors are generally signaled with 4xx and 5xx codes. In these cases, the
response body may be useful for providing specific information about the error
to the client. Such information can be provided as JSON, plain text, HTML, or
any other feasible format. Just make sure to call tangelo.content_type()
to
specify the MIME type of the response before using return
to prepare and
send the response.
RESTful Services¶
Tangelo also supports the creation of REST services. Instead of placing
functionality in a run()
function, such a service has one function per
desired REST verb. For example, a rudimentary service to manage a collection of
databases might look like the following:
import tangelo
import lcarsdb
@tangelo.restful
def get(dbname, query):
db = lcarsdb.connect("enterprise.starfleet.mil", dbname)
if not db:
return None
else:
return db.find(query)
@tangelo.restful
def put(dbname):
connection = lcarsdb.connect("enterprise.starfleet.mil")
if not connection:
return "FAIL"
else:
success = connection.createDB(dbname)
if success:
return "OK"
else:
return "FAIL"
The tangelo.restful()
decorator is used to explicitly mark the
functions that are part of the RESTful interface so as to avoid (1) restricting
REST verbs to just the set of commonly used ones and (2) exposing every function
in the service as part of a REST interface (since some of those could simply be
helper functions).
Bear in mind that a function named run()
will always take precedence over
any functions marked with @tangelo.restful
. This is because run()
is
meant to be agnostic to the HTTP method that was used to invoke it, and as such,
has higher precedence when Tangelo is looking for a function to invoke.
Configuring Web Services¶
You can optionally include a configuration file alongside the service itself. For instance, suppose the following service is implemented in autodestruct.py:
import tangelo
import starship
def run(officer=None, code=None, countdown=20*60):
config = tangelo.config()
if officer is None or code is None:
return {"status": "failed",
"reason": "missing officer or code argument"}
if officer != config["officer"]:
return {"status": "failed",
"reason": "unauthorized"}
elif code != config["code"]:
return {"status": "failed",
"reason": "incorrect code"}
starship.autodestruct(countdown)
return {"status": "complete",
"message": "Auto destruct in %d seconds!" % (countdown)}
Via the tangelo.config()
function, this service attempts to match the
input data against credentials stored in the module level configuration, which
is stored in autodestruct.yaml a YAML file containing an associative array
(i.e., a key-value store) at its top level:
officer: picard
code: echo november golf alpha golf echo four seven enable
The two files must have the same base name (autodestruct in this case) and be
in the same location. Any time the module for a service is loaded, the
configuration file will be parsed and loaded as well. Changing either file will
cause the module to be reloaded the next time it is invoked. The
tangelo.config()
function returns a copy of the configuration dictionary, to
prevent an errant service from updating the configuration in a persistent way.
For this reason, it is advisable to only call this function once, capturing the
result in a variable, and retrieving values from it as needed.
Persistent Storage for Web Services¶
In contrast to the read-only service configuration, each service also has access
to a persistent data store that remembers changes made to it from invocation
to invocation. This may be accessed by invoking tangelo.store()
within a
service function. Like tangelo.config()
, the store is a Python dictionary,
but anything stored in it will be accessible from a subsequent invocation of the
service.
A very simple example would increment tangelo.store()["count"]
on each
invocation, allowing the service to “know” how many times it has been invoked
before.