Pyxley In Production¶
Now that we’ve built an app, how do we deploy it? Because Pyxley is built on top of Flask, the process for deploying our app is no different than deploying any other Flask app. Rather than cover the basics of deploying web apps, this guide will cover some patterns and address some of Pyxley’s capabilities.
Data and DataFrames¶
Pyxley was developed specifically with Pandas in mind. Each widget has methods for transforming dataframes into JSON objects that the different JavaScript libraries can interpret. It’s not entirely obvious how to deal with issues such as reloading data.
Updating Data¶
Let’s revisit our MetricsGraphics example, specifically the
LineChart
object. Recall when we created the object,
we passed it a dataframe and it looked something like
the snippet below.
- ::
- # sf is our dataframe # fig is our figure object lc = LineChart(sf, fig, “Date”, [“value”], **kwargs)
When the dataframe, sf
, is passed into the constructor
it is passed into a function that Flask will use any time
it needs the chart data.
In the __init__
method you will find the following
snippet.
- ::
- if not route_func:
- def get_data():
args = {} for c in init_params:
- if request.args.get(c):
- args[c] = request.args[c]
- else:
- args[c] = init_params[c]
- return jsonify(LineChart.to_json(
- self.apply_filters(df, args), x, y, timeseries=timeseries
))
route_func = get_data
The default behavior for LineChart
is to use this
basic route function. Let’s focus on the data that
gets returned for a moment. Notice we call jsonify
which is a Flask function that turns a python dict
into a JSON object. The input dict
is returned
by the LineChart.to_json
method. to_json
takes
a dataframe as the input as well as a few other variables.
self.apply_filters
is a method belonging to the Chart
base class that takes a dataframe and a dict
containing
our filtering conditions.
As far as default behavior, this is perfectly reasonable, but
it makes a pretty rigid assumption that the underlying data
will not change. This default route function is created when
the LineChart
object is initialized and changing the data
would require us to recreate the object. In practice, it’s not
a good idea to have a dependency that requires an app to be
restarted.
Now notice the first line of our snippet: if not route_func
.
route_func
is a keyword argument in the __init__
method.
If a developer provides their own route function, they are no
longer restricted by the default behavior.
Reloading Dataframes¶
Let’s assume that we are perfectly comfortable with using Pandas as our data source, but we would like to be able to refresh the data on some cadence.
Returning to our LineChart
example, what if created
it in the following way:
- ::
# sf is our dataframe # fig is our figure object lc = LineChart(sf, fig, “Date”, [“value”],
route_func=some_other_route, **kwargs)
Now that we have passed in some_other_route
it will
be used instead and sf
will be ignored. So let’s
start building what the function looks like.
- ::
- class MyDataWrapper(object):
“”” We are building a simple wrapper for our data “”” def __init__(df, x, y, timeseries=True):
self.df = df self.x = x self.y = y self.timeseries = timeseries- def my_route_function(self):
# put args check here return jsonify(LineChart.to_json(
Chart.apply_filters(self.df, args), self.x, self.y, timeseries=self.timeseries))
So the first thing you should notice is that this
looks pretty similar to our default get_data
function
from above. The most obvious difference is that now
most of the variables are now member variables of our
MyDataWrapper
class. The main benefit from this is
that now we are calling a method in our MyDataWrapper
object that also manages the state of our dataframe
self.df
. Now that LineChart
no longer has anything
to do with our dataframe and how to parse it, we are free to
reload our data. You could imagine adding a set_data
method to our class that has logic about how and when to
reload data. The snippet below shows how to modify our example.
We will still pass in all of the necessary chart options,
but the important logic is now handled by our new object.
- ::
# sf is our dataframe myData = MyDataWrapper(sf, “Date”, [“value”]) # fig is our figure object lc = LineChart(sf, fig, “Date”, [“value”],
route_func=myData.my_route_function, **kwargs)
Databases¶
What if your data is too big. Say, for example, you would
like to use guincorn
and the idea of replicating your
data across multiple processes just isn’t feasible. If you
are using a relational database that works with SQLAlchemy
there’s not much to change. The snippet below shows one
possible change to our data wrapper from above. It’s also
important to remember that we just need to format our data
for the JavaScript libraries. We can freely customize the
methods to accommodate any data source.
- ::
- class MyDataWrapper(object):
“”” We are building a simple wrapper for our data “”” def __init__(sql_engine, x, y, timeseries=True):
self.engine = sql_engine self.x = x self.y = y self.timeseries = timeseries- def get_data(self, args):
- # make some sql query or use SQLAlchemy functions return pd.read_sql(‘select * from table’, self.engine)
- def my_route_function(self):
# put args check here df = get_data(args) return jsonify(LineChart.to_json(
Chart.apply_filters(df, args), self.x, self.y, timeseries=self.timeseries))
REST APIs¶
What if you want to hit some other API? Use requests
! Then
it’s the same game of just leveraging the other functions to
get the data in the format the chart needs.