The flask-restless-client is the second part of a two part library. The first being the flask-restless-datamodel. Together with the flask-restless-datamodel, this library serves as a goal to provide a CRUD/RPC client for Flask/SQLAlchemy applications, over HTTP using flask-restless.
Taking advantage of the easy integration offered by Flask-Restless to expose a REST CRUD interface over HTTP, this library uses its power to provide a dynamic python client. Reading the data format generated by the flask-restless-datamodel, the restless-client is able to build itself and provide you with objects that aim to mirror an SQLAlchemy-like interface on the client side.
This includes RPC possibilities to run object methods of the SQLA models defined server-side. Some setup is required to achieve a smooth interaction with the RPC part of this library.
The developer will be required to:
- write serializers from and to python natives to transfer complex objects to the server
- overwrite authentication method if the chosen authentication method is not supported by this library.
As such, it's advised to use this library as a base for a custom client for your application. Most likely, it will be a thin layer on top of the flask-restless-client setting up some configuration.
pip install flask-restless-client
The first step is to enable the flask-restless-datamodel on the server side. You can visit flask-restless-datamodel to see how to do this.
As this library is intented to be useable out of the box, some built in authentication is provided. Current out of the box authentication types are Bearer and Basic Authentication.
By default, the client will use the Bearer session, but the Basic Authentication session is importable from restless_client.ext.auth
.
You are also able to give your own (pre-authenticated) session as a parameter when initializing the client.
Environment variables can be set to speed up authentication setup. Using the prefix RESTLESS_CLIENT_
you can set anything involving authentication, including setting which type of session to use.
Part of supporting an RPC-like client is making sure the objects arrive at their destination in the same way they are sent from the source. We all know deserialisation isn't always true to what you initially put in. Therefore you can register your own (de)serializer for complex objects.
from cereal_lazer import register_class
import pandas as pd
register_class(
'DataFrame', # Register the object as this name
pd.DataFrame, # Register the class
lambda x: x.to_json(), # Register a serializer
lambda x: pd.read_json(x) # Register a deserializer
)
These objects are registered in a global context using the cereal_lazer
library. The client is then using the library to (de)serialize.
It's important tot re-iterate on the fact that this is a self-building client. That means the way you interract with this client depends on external input. To have a practical example, consider the following SQLA models defined server-side:
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode, unique=True)
birth_date = db.Column(db.Date)
class Computer(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode, unique=True)
vendor = db.Column(db.Unicode)
purchase_time = db.Column(db.DateTime)
owner_id = db.Column(db.Integer, db.ForeignKey('person.id'))
owner = db.relationship(
'Person', backref=db.backref('computers', lazy='dynamic'))
owner_name = association_proxy('owner', 'name')
peers = association_proxy('owner', 'computers')
Based on these models, flask-restless-datamodel will generate the input for the client to build itself, allowing for an SQLA-like interface.
if we hook up our client to our server app, we'll be able to do all neat kinds of stuff
Now that we have some server side models exposed, the models will be available on the client side and we can jump right in and create some objects.
from restless_client import Client
c = Client(url='http://localhost:5000/api')
maurice = c.Person(name='Maurice')
roy = c.Person(name='Roy')
beast = c.Computer(name='TheBeast', vendor='Pear', owner=maurice)
server = c.Computer(name='Server', vendor='Pingu', owner=maurice)
pc = c.Computer(name='pc', vendor='Doors', owner=roy)
# Save objects on the server
c.save()
# Alternatively, you can save on a per-instance basis
beast.save()
Note that if we disregard the c.save()
statement, and run beast.save()
instead, that the maurice
instance is a dependency of beast.owner
and will be unsaved at the time we call beast.save()
.
The client should be able to resolve these unsaved dependencies and will save them first
Loading objects can be done in serveral ways. The object models have a query
attribute that is accessible to perform all read operations
everyone = c.Person.query.all()
maurice = c.Person.query.get(1)
Due to all
and get
being often used methods, they have been enabled with a shorthand on the object model itself
everyone = c.Person.all()
maurice = c.Person.get(1)
maurice = c.Person.query.filter(c.Person.name == 'Maurice')
maurice = c.Person.query.filter_by(name='Maurice')
# limit the results to 3
some_people = c.Person.query.limit(3).all()
# offset results, ignoring the first 2
some_people = c.Person.query.offset(2).all()
# order by name
everyone = c.Person.query.order_by(name='asc').all()
# get the first instance
maurice = c.Person.query.first()
# get the last instance
maurice = c.Person.query.last()
# expect only one result
maurice = c.Person.query.one()
# expect only one result, or no result
maurice = c.Person.query.one_or_none()
# filtering over relations, get all people that own a computer with Pear vendor
maurice = c.Person.query.filter(c.Person.computers.has_(c.Computer.vendor == 'Pear'))
Updating is just as easy as creating objects. The library is built in a way that it flags dirty attributes, and only sends the necessary data to the server.
cmptr = c.Computer.query.one()
cmptr.vendor = 'Robot'
cmptr.save()
cmptr = c.Computer.query.one()
cmptr.delete()
Note that executing delete
is instant, and calling the save is not needed.
As promised, this library provides an RPC-like feature that allows you to run the methods defined on your SQLA models. It's nearly nowhere as advanced as other RPCs out there, but it at least provides a way to emulate the interaction on models as if you were working with them on a server context.
The sending and receiving of complex objects does require some setup, but once this is done, doing remote method calls should run smoothly. (Although there are plenty of scenarios where remote execution might fail).
Anyway here's wonderwall
On the server we would define a model with the following method
class Person(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.Unicode, unique=True)
def speak(self, what_to_say):
return "Errr... uh... ehm... {}?".format(what_to_say)
Which we can then remotely run by doing
maurice = c.Person.query.filter(c.Person.name == 'Maurice')
print(maurice.speak("I'd rather send an email"))
Currently the client will crash if it tries to (de)serialize a complex object that is not yet registered. Going forward, it would be desired to apply a "no-crash" policy. The idea behind this is that the data is there, and it's not because (de)serialization failed, that the program should halt execution.
If a (de)serializer was not registered for a complex object, one will be emulated from the data available. Accessing data that is known to it will allow you to interact with it without issue, accessing functions or data that is unknown to the emulated object will result in an exception.