Quickstart¶
Introduction¶
First, let’s put in context what we are doing: we have a laboratory that we want to make remotely available, and that we can control in Python (2.7 or 3.x). Let’s imagine something super-simple:
- A set of lights (and buttons to turn them on and off).
How this really works it’s up to you. You could as well have an Arduino, or a Raspberry Pi. Or you might be controlling a FPGA device or a regular microcontroller. Or you might be controlling a control application with a PLC. You may be managing some chemistry and have a set of switches so the hardware does something. All this depends on your setup. weblablib works on top of that. So let’s imagine a fake code as follows:
def clean_resources():
# Some code that turns off all the lights
pass
def switch_light_on(number, state):
# Some code (e.g., using GPIO or something)
# that turns a light on or off
pass
def is_light_on(number):
# Some code that checks if a light is on or off
return True
In particular, let’s create a version of this software that stores everything
in disk in a JSON document called lights.json
. It’s not thread safe, but
it is the easiest approach for this tutorial. Don’t bother reading this code,
it simply stores information in disk.
import os
import json
def clean_resources():
# Some code that turns off all the lights
for n in range(1, 11):
switch_light(n, False)
def switch_light(number, state):
# Some code (e.g., using GPIO or something)
# that turns a light on or off
if not os.path.exists('lights.json'):
lights = {
# 'light-1': False
}
for n in range(1, 11):
lights['light-{}'.format(n)] = False
else:
lights = json.load(open('lights.json'))
lights['light-{}'.format(number)] = state
json.dump(lights, open('lights.json', 'w'), indent=4)
def is_light_on(number):
# Some code that checks if a light is on or off
if not os.path.exists('lights.json'):
return False
return json.load(open('lights.json'))['light-{}'.format(number)]
And let’s put this code in a file called hardware.py
.
Creating a dummy Flask app¶
Note
If you already have certain experience with Flask, you can get the code in the examples/quickstart/step1 in the GitHub repository, and then continue to Adding weblablib.
The next step is to create some basic code to create the web interface. We are going to use Flask, and do it step by step, so you don’t need to have prior knowledge. However, if you are not familiar with and find problems following the quickstart, you could check their quickstart tutorial first.
For Flask, we’ll create first the application, called laboratory.py
:
from flask import Flask
app = Flask(__name__)
app.config.update({
'SECRET_KEY': 'something-random',
})
@app.route('/')
def index():
return "Welcome to the laboratory!"
This is enough for having a very simple website. The next step is to install. Please, proceed to read the Flask installation documentation. If you are familiar with Python, you simply have to create a virtualenv (not strictly required, though), and run:
$ pip install Flask
Once it is installed, you can run the following (you have more information in Flask CLI documentation):
$ export FLASK_DEBUG=1
$ export FLASK_APP=laboratory.py
$ flask run
* Serving Flask app "laboratory"
* Forcing debug mode on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 324-368-642
In Windows environments, you might need to run:
C:\...> set FLASK_DEBUG=1
C:\...> set FLASK_APP=laboratory.py
C:\...> flask run
From this point, you can use your web browser to go to http://localhost:5000/ and see the
message that was in the index
function. Since we used FLASK_DEBUG
, you can change
the index
code and you’ll see in the console how it restarts the web server automatically.
Now, the code right now does not do much. Let’s create a bit more! For example, if we want to
display 10 lights, we need to create some HTML code. So, create a templates
folder, and add
a file called lab.html
with the following contents:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- HTML5 shim and Respond.js for IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="row">
<h1>Welcome to <strong>mylab</strong>!</h1>
</div>
<div class="row">
<p>This is just an example laboratory using <a href="https://developers.labsland.com/weblablib/">weblablib</a>.</p>
<p>Time: <span id="timer"></span>.
</div>
<br><br>
<div id="panel">
<div class="row">
<h2>Lights: click on each light to change status (and read it in the console)</h2>
</div>
<br>
<div class="row">
{% for light in range(1, 11) %}
<div class="col-sm-1 text-center">
Light {{ light }}
<br>
<a href="javascript:turnOff({{ light }})">
<img width="50px" id="light_{{ light }}_on" src="https://files.labsland.com/pub/docs/weblablib/light-on.png">
</a>
<a href="javascript:turnOn({{ light }})">
<img width="50px" id="light_{{ light }}_off" src="https://files.labsland.com/pub/docs/weblablib/light-off.png">
</a>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<!-- Here we will have the scripts -->
</body>
</html>
The code itself is the standard Bootstrap template.
Bootstrap is a HTML, CSS and JavaScript framework that allows you to create interfaces very
quickly based on few principles, and compatible with other web frameworks. The key here
is that we have a container
(so there will be some empty space on the right and left),
and then some row
, and in one of them, there is something about lights. Let’s take a
closer look at that one:
<div class="row">
{% for light in range(1, 11) %}
<div class="col-sm-1 text-center">
Light {{ light }}
<br>
<a href="javascript:turnOff({{ light }})">
<img width="50px" id="light_{{ light }}_on" src="https://files.labsland.com/pub/docs/weblablib/light-on.png">
</a>
<a href="javascript:turnOn({{ light }})">
<img width="50px" id="light_{{ light }}_off" src="https://files.labsland.com/pub/docs/weblablib/light-off.png">
</a>
</div>
{% endfor %}
</div>
As you can see, this is not HTML code, but Jinja code. Jinja is the default
templating framework used by Flask. It just repeats 10 times some HTML code, where {{ light }}
will be 1, 2..10 in each iteration.
In this case we have 10 (1..10) lights in one block each. Each block has two images: one with
a light on and one with a light off. And each light has an id
which is light_1_on
or light_1_off
.
Also, if you click on any of those images, it will run the code javascript:turnOff(1)
or javascript:turnOn(1)
(being 1..10).
We had put the whole HTML code in templates/lab.html
, so it’s time to change the laboratory.py
code:
from flask import Flask, render_template
app = Flask(__name__)
app.config.update({
'SECRET_KEY': 'something-random',
})
@app.route('/')
def index():
return render_template("lab.html")
If we refresh the website, we will see all the bulbs, on and off.
Now we are only missing doing something when the lights are clicked, as well as keeping the state.
To do so, let’s take the hardware.py
and start calling it:
from flask import Flask, request, render_template, jsonify
import hardware
app = Flask(__name__)
app.config.update({
'SECRET_KEY': 'something-random',
})
@app.route('/')
def index():
return render_template("lab.html")
@app.route('/status')
def status():
# jsonify returns a JSON where {lights: result}
# plus adds the 'application/json' headers, etc.
return jsonify(lights=get_light_status(), error=False)
@app.route('/lights/<number>/')
def light(number):
# request.args is a dictionary with the
# query arguments (e.g., this checks ?state=true)
state = request.args.get('state') == 'true'
# We call the hardware with the state
hardware.switch_light(number, state)
# And return the whole status of everything
return jsonify(lights=get_light_status(), error=False)
def get_light_status():
lights = {}
for light in range(1, 11):
lights['light-{}'.format(light)] = hardware.is_light_on(light)
return lights
With this code, you can already test in your web browser going to URLs like:
- http://localhost:5000/lights/1/?state=true (turn on light 1)
- http://localhost:5000/lights/1/?state=false (turn off light 1)
- http://localhost:5000/lights/1/?state=true (turn on light 1)
- http://localhost:5000/lights/1/?state=false (turn off light 1)
- http://localhost:5000/status (see current status, without changing anything)
As you can see, by entering in those URLs, you change the state of the fake lights.
You can also see that a file called lights.json
has been automatically created,
and you can open it and read it.
The final step is to call from the HTML code to these URLs. We’ll first create a
static
folder and inside we’ll put a lab.js
JavaScript file, with the following
contents:
function turnOn(number) {
turnLight(number, true);
return false;
}
function turnOff(number) {
turnLight(number, false);
return false;
}
function turnLight(num, state) {
var url = LIGHT_URL.replace("LIGHT", num) + "?state=" + state;
$.get(url).done(parseStatus);
}
function clean() {
// Not yet
}
function parseStatus(newStatus) {
if (newStatus.error == false) {
for (var i = 1; i < 11; i++) {
if(newStatus["lights"]["light-" + i]) {
$("#light_" + i + "_off").hide();
$("#light_" + i + "_on").show();
} else {
$("#light_" + i + "_on").hide();
$("#light_" + i + "_off").show();
}
}
}
}
var STATUS_INTERVAL = setInterval(function () {
$.get(STATUS_URL).done(parseStatus).fail(clean);
}, 1000);
$.get(STATUS_URL).done(parseStatus);
As you can see, there are two variables which are not defined in the JavaScript file:
LIGHT_URL
(which will be the URL of the lights)STATUS_URL
(Which will be the URL of the /status)
- Other than that, the code does the following:
turnOn
andturnOff
callturnLight(num, state)
, which calls the/lights/1/?state=true
web that you were calling before.
parseStatus
receives a status (which is what any of the webs we- implemented return) and for each light, if it’s true, it hides one, and if it’s off it hides the other light.
Therefore, every time we press on a light which is on, it will call
turnOff(number)
, which will call the server, will modify the
lights.json
file, and when it obtains the status
, it will hide the
light on image and display the light off image. We are only missing how to
include this static/lab.js
file in the HTML.
To do this, in the end of the lab.html
file there are the following lines:
<!-- Here we will have the scripts -->
</body>
</html>
Under the Here we will...
, you must place the following code:
<!-- Here we will have the scripts -->
<script>
var STATUS_URL = "{{ url_for('status') }}";
var LIGHT_URL = "{{ url_for('light', number='LIGHT') }}";
</script>
<script src="{{ url_for('static', filename='lab.js') }}"></script>
</body>
</html>
url_for
is the way Flask provides to point to URLs without having to hardcode them.
If where it says @app.route('/status')
tomorrow you change the URL, all the code will
be updated, as long as the name of the function is the same.
Warning
This approach (modifying state through a GET request, no special CSRF check) is
very insecure and you shouldn’t use it. Check the examples/advanced
folder
in the GitHub weblablib repository
for a better approach. This example is just focused on having a very simple basis
where we can rely for explaining weblablib.
Adding weblablib¶
Why using weblablib¶
As you’ve seen in the previous example, you have a very simple website that does something
with certain hardware (faked in hardware.py
). It works quite well: you turn a light
off, and even if you refresh the website, stop the server and restart it, you’ll see the
light off.
However, this code is very far from being usable as a remote laboratory:
- There is no authentication neither authorization. Who is the user? Why does he/she have access?
- What if we need an scheduling system (such as a queue)? How do we ensure that students come once at a time instead of multiple students accessing at the same time? In this particular case it might not be very important, but if for example we had a microcontroller, we would need a queue so when one student is .
- How does the teacher know who and when used the lab?
- How do you integrate the laboratory in a Learning Management System such as Moodle, Sakai, EdX or similar?
- How does the administrator establish how long the student can access, etc.?
For all these things, you can either implement and test everything by yourself, or rely on a remote laboratory management system, such as WebLab-Deusto. Most of those features (administration, analytics, scheduling) are covered by WebLab-Deusto, but at some point WebLab-Deusto delegates on the particular laboratories by sending them users. So for example a user will be authenticated in WebLab-Deusto, and attempt to access the laboratory, and still WebLab-Deusto will be dealing with the queue of users. When the user finally has permission to use the laboratory in that particular time, then WebLab-Deusto contacts the laboratory telling it in a secure way “I’m WebLab-Deusto, I have this particular student, get ready for it”.
The laboratory still has to implement this protocol and life cycle. And here is where weblablib enters, by implementing the protocol and making it easy for laboratory developers to focus on the laboratory.
Note
You can download the code of this section in the github repository, folder examples/quickstart/step2.
Note that in it, we have also included some time-related code in the JavaScript file.
Adding WebLab¶
To use weblablib, you have to create a WebLab
instance and initialize it with the Flask app. You can
either do both at once:
from weblablib import WebLab
weblab = WebLab(app)
or do it in two phases:
from weblablib import WebLab
weblab = WebLab()
# Later
weblab.init_app(app)
What is important is that the configuration is loaded in Flask before init_app
(or WebLab(app)
).
Additionally, weblablib
has more interesting features. For example, you may want to establish that a
particular Flask view is only available for WebLab users:
from weblablib import requires_active
@app.route('/')
@requires_active
def index():
# ...
@app.route('/status')
@requires_active
def status():
# ...
@app.route('/lights/<number>/')
@requires_active
def light(number):
# ...
In this case, you are defining that the three views status
and light
can only be accessed by
active WebLab users (users who have been assigned and whose time in the laboratory is not over).
Additionally, we need to tell WebLab-Deusto what is the landing page when an experiment has been reserved, and we do it by adding the following code:
from flask import url_for
@weblab.initial_url
def initial_url():
# Being 'index' the name of the
# view ("def index():") where the
# user has to land
return url_for('index')
This way, WebLab-Deusto will be managing students and the queue of students, and whenever it’s time to
access the laboratory, it will contact the laboratory, create a session for the user, initialize it in the
user browser, and redirect the user to that (the one defined in initial_url
).
Polling¶
In WebLab-Deusto the administrator has assigned a time for the students. Let’s imagine that it’s 10 minutes per user. Typically, students leave earlier than the assigned time. For example, if they’re learning how to write code for a robot, they might see that the robot has already failed in 30 seconds and might not need to continue suing it. However, if it works, they might need to stay 3 minutes or so. And if the lesson is a very complex one, then they might need the 10 minutes.
For this reason, you might need to keep track of when is the user using the laboratory, and whenever the
student leaves, report WebLab-Deusto that the user left. With weblablib, this process is almost
automatic: you have to call a poll
function in less than 15 seconds (or whatever you setup in
the WEBLAB_TIMEOUT
configuration). There are different ways to achieve this. The simplest is adding
weblab_poll_script
in your template file as follows:
<!-- Here we will have the scripts -->
<script>
var STATUS_URL = "{{ url_for('status') }}";
var LIGHT_URL = "{{ url_for('light', number='LIGHT') }}";
</script>
<script src="{{ url_for('static', filename='lab.js') }}"></script>
{{ weblab_poll_script() }}
</body>
</html>
Internally, it will create a JavaScript script that will call a poll
method every few seconds. The only
important thing is that this is called AFTER including jQuery
since it relies on jQuery
.
This way, whenever the student leaves the laboratory (actively, or because there was a network issue on his side, or for any other reason), the laboratory marks him as logged out, and therefore WebLab-Deusto assigns the laboratory to someone else.
However, if you want to make this process faster, you can implement a method such as:
from weblablib import logout
@app.route('/logout')
@requires_active
def logout_view():
logout()
return jsonify(result="ok")
This way, you can create in HTML a code that actively tells in a faster way to WebLab-Deusto that the user is finished.
You can also call this automatically when the web browser closes or moves to another link by adding this
parameter to weblab_poll_script
:
{{ weblab_poll_script(logout_on_close=True) }}
Additionally, by default, weblablib installs a code that for any call in your Flask app, it will call
automatically poll
. You can disable this by configuring WEBLAB_AUTOPOLL
to False
in the
configuration, and then call poll
manually:
from weblablib import poll
@app.route('/poll')
@requires_active
def poll():
poll()
return jsonify(result='ok')
Checking user data¶
In weblablib there is an special object called weblab_user
. It is never None
, and it can be either:
AnonymousUser
: when someone who has never been logged in (or not in a long time) accesses your app.CurrentUser
: when a user has been assigned to use the laboratory, and only during that time (and if the user does not log out, or stopped polling)ExpiredUser
: when a user has been previuosly assigned, but the assigned time elapsed, or the user clicked on log out or stopped polling.
So as to distinguish the type of user, you have two properties:
is_anonymous
:True
if it’s anAnonymousUser
active
:True
if it’s anCurrentUser
.
Additionally, both the CurrentUser
and the ExpiredUser
have the following properties:
username
: the username in the original system. For example,tom
. Note that this is not unique: if your WebLab-Deusto is sharing the laboratory with a Moodle, there might be atom
in WebLab-Deusto and anothertom
in Moodle, and in both casesweblab_user.username
will betom
. So you can use it to talk to the username, but not for saving information for this user.username_unique
: a full, unique, identifier of the user. For example,tom@school1@labsland
. Compared to the previous version, this is guaranteed to be unique.back
: this is the URL where the user should go after finishing using the laboratory. For example, if the student was in a Moodle system, theback
will be a link to the particular page in that Moodle system.requires_active
by default redirects the user to that URL when the user has been logged in and not anymore.time_left
: this is the time, in seconds, to finish the session. If the user was assigned 10 minutes, and 2 minutes have been passed, it will return something like478.3
(seconds). Take into account that for this number to be accurate, both the laboatory server and the WebLab-Deusto server must have the same date and time, so use any time synchronization tool (e.g., a ntp server) to make sure this is the case.start_date
: UNIX timestamp of the second (according to WebLab) when the reservation started.data
: this is some data that you can store for passing between the different methods, tasks, etc. This information must be basic data types (such as dicts, lists, numbers, strings… anything you can encode in JSON), and not your own objects or similar. By default, if you use it in a Flask view or inon_start
, whatever you put there will be automatically stored at the end of the view. You can force to store / retrieve the data by callingweblab_user.data.store()
orweblab_user.data.retrieve()
.locale
: this represents the language according to WebLab-Deusto (which will delegate in external systems too). For example, if you are using Moodle in Spanish, it will tell WebLab-Deusto that it’s the case, which will establish herees
. You can use this with the proper library (such as Flask-Babel or Flask-BabelEx), as explained in Internationalization (i18n).request_client_data
andrequest_server_data
: both provided by WebLab-Deusto (one coming from the client, the other from curated data by the WebLab-Deusto server; same as data inon_start
).
For example:
from weblablib import requires_active, weblab_user
@app.route('/status')
@requires_active
def status():
# jsonify returns a JSON where {lights: result}
# plus adds the 'application/json' headers, etc.
return jsonify(lights=get_light_status(),
time_left=weblab_user.time_left,
error=False)
In this case, we have added to the response of status the time in seconds that is left to the
current user. You do not need to work on checking if weblab_user
is anonymous or expired
since the method already has a @requires_active
decorator (so if it’s an anonymous user
the user will see “access forbidden”, and if he is a user whose time already passed, will be
redirected to his weblab_user.back
URL).
If the behavior of requires_active
is too strict for your case, you also have a
requires_login
decorator. The difference is that while the former requires the user to
have access to the laboratory right now, the latter also accepts users which were using the
laboratory and now they can not use it anymore. For example, if the user was doing some
exercises, you may want to let the student to download the exercises for some time. In this
case, you may use requires_active
to status
or light
(since whoever calls must
be assigend to the laboratory), but requires_login
to index
(and there, depending on
if the user is active or not, show one thing or another):
from weblablib import requires_login, weblab_user
@app.route('/')
@requires_login
def index():
if weblab_user.active:
# Show something for current users
else:
# Show something for past users
Additionally, in the templates you have access to weblab
and weblab_user
, so you can
simply run:
from weblablib import requires_login
@app.route('/')
@requires_login
def index():
return render_template('index.html')
And in the HTML code display:
{% if not weblab_user.active %}
<a href="{{ weblab_user.back }}">Back</a>
{# ... #}
<div class="alert alert-warning">
<h1>You don't have access to the laboratory anymore but
you can download the following resources</h1>
{# ... #}
</div>
{% endif %}
With this information, we can improve our example app by adding this in the JavaScript code:
var TIMER_INTERVAL = null;
var TIME_LEFT = null;
// Instead of the previously existing clean() function
function clean() {
clearInterval(STATUS_INTERVAL);
clearInterval(TIME_LEFT);
$("#panel").hide();
$("#timer").text("session is over");
}
// Instead of the previuosly existing parseStatus
function parseStatus(newStatus) {
if (newStatus.error == false) {
for (var i = 1; i < 11; i++) {
if(newStatus.lights["light-" + i]) {
$("#light_" + i + "_off").hide();
$("#light_" + i + "_on").show();
} else {
$("#light_" + i + "_on").hide();
$("#light_" + i + "_off").show();
}
}
if (TIMER_INTERVAL == null) {
TIME_LEFT = Math.round(newStatus.time_left);
$("#timer").text("" + TIME_LEFT + " seconds");
TIMER_INTERVAL = setInterval(function () {
TIME_LEFT = TIME_LEFT - 1;
if (TIME_LEFT >= 0) {
$("#timer").text("" + TIME_LEFT + " seconds");
} else {
clean();
}
}, 1000);
}
} else {
clean();
}
}
This way, in the beginning TIMER_INTERVAL
is null
, but whenever we parse a status
from the server side (and therefore we receive a time_left
value), we can start
a new interval that every second it changes the time left.
Adding basic settings¶
There are two mandatory variables that you have to configure in weblablib:
WEBLAB_USERNAME |
WebLab-Deusto has a pair of credentials
representing a particular WebLab-Deusto
instance in a particular laboratory.
These pair are a username and a
password, but they represent
WebLab-Deusto, not the particular
user coming from WebLab-Deusto. In
WebLab-Deusto, this property is called
http_experiment_username . |
WEBLAB_PASSWORD |
Same as WEBLAB_USERNAME , but this
property representing the
http_experiment_password
configuration value of WebLab-Deusto. |
So, for example, we could use:
app = Flask(__name__)
app.config.update({
'SECRET_KEY': 'something-random',
'WEBLAB_USERNAME': 'weblabdeusto',
'WEBLAB_PASSWORD': 'password',
})
Initializing and cleaning resources¶
Very often, you need to prepare the laboratory for the user before he uses it, and/or clean resources after the user has used them.
To do so, you can modify the hardware.py
file to support:
from laboratory import weblab
from weblablib import weblab_user
@weblab.on_start
def start(client_data, server_data):
print("Initializing {}".format(weblab_user))
@weblab.on_dispose
def dispose():
print("Disposing {}".format(weblab_user))
clean_resources()
And, in laboratory.py
, move import hardware
to the end of the file.
This way, for each user, this code will be run once at the beginning. If an error is produced in your start
method,
the user will not be redirected and WebLab-Deusto will consider the laboratory as broken for a while before sending other
user.
The @weblab.on_dispose
method is guaranteed to be run at some point in a matter of seconds or minutes after the user
is not using the laboratory anymore, as long as the process is running. However, there are situations (for example, you
configure Redis to not persist data in disk periodically and suddenly the computer where this code is running is restarted)
where it may not be run. For this reason, it is recommended that you run your clean_resources
before running gunicorn or
whatever server you use later.
For example, you can add this code in laboratory.py
:
import hardware
@app.cli.command('clean-resources')
def clean_resources_command():
hardware.clean_resources()
so later you can run:
$ flask clean-resources
When the computer is restarted or before running the script that runs your laboratory or similar (see examples/advanced
for more).
Warning
Note that this code is run outside the web browser, so Flask objects like session
will not work.
Also note that, in general, it is a bad idea to store global information in a Flask script. If you are
later running several processes (as it is normal), your laboratory will not work. If you need to store
anything, rely on Redis see examples/advanced
) or a database or similar, not memory.
So, for example, do not do this:
# DO NOT DO THIS
lights_on = False
@weblab.on_start
def start(client_data, server_data):
global lights_on
lights_on = True
If you later run this in gunicorn
or similar with multiple workers, the same variable will have
different values in different servers.
Running weblablib¶
So far, you have seen what code to put, but you have not even installed weblablib, so you could not run it. Also, there are two ways to run this code: from WebLab-Deusto or, in development environments, from the console directly. This section is focused on installing and running this code.
Installing weblablib¶
Earlier, you installed Flask by creating an virtual environment (or not) following the instructions of the Flask installation documentation. So as to install weblablib, you only have to activate the same virtual environment (if you were using any) and run:
$ pip install weblablib
Note
If you have installed WebLab-Deusto in this computer, please use a different virtual environment to avoid any potential conflict. For example, if you installed WebLab-Deusto in a virtualenv called weblab, create another virtualenv for weblablib:
$ mkvirtualenv wlib
(wlib) % pip install weblablib
Installing redis¶
weblablib relies on Redis, an Open Source, in-memory data structure store. In Linux distributions you can typically install it from the repositories:
$ sudo apt-get install redis-server
In Microsoft Windows, you can use Redis for Windows, supported by Microsoft. You can either download the installer (beware that you download the Latest release and not a Pre-release which might come with bugs), or use nuget.
In Mac OS X, you can install it manually or use Homebrew.
Development¶
Once weblablib is installed, running the script is exactly as before:
$ export FLASK_DEBUG=1
$ export FLASK_APP=laboratory.py
$ flask run
However, if you open the web browser and go to the laboratory site:
You will only see Access forbidden
. So as to access the laboratory, you have to either:
- Install WebLab-Deusto, follow these instructions (in particular, the Unmanaged server section, and configure with the following parameters: *
http_experiment_url: http://localhost:5000/
*http_experiment_username: weblabdeusto
(or whatever you used inWEBLAB_USERNAME
) *http_experiment_password: password
(or whatever you used inWEBLAB_PASSWORD
)- OR use the command line interface for debugging.
The former is mandatory for the production mode, but the latter is the easiest version when working with. In this case, you simply run the following in a different terminal:
$ export FLASK_DEBUG=1
$ export FLASK_APP=laboratory.py
$ flask weblab fake new
This fakes a request from WebLab-Deusto, creating a new user. By default, it opens a session your default web browser (check more information on how thisworks if it uses a web browser you don’t want). You can avoid this by adding --dont-open-browser
.
As you see, you will end in http://localhost:5000/ but with a working valid WebLab-Deusto session. weblab fake new
uses some default parameters, that you can change:
$ flask weblab fake new --help
Usage: flask weblab fake new [OPTIONS]
Create a fake WebLab-Deusto user session.
This command creates a new user session and stores the session in disk, so
you can use other commands to check its status or delete it.
Options:
--name TEXT First and last name
--username TEXT Username passed
--username-unique TEXT Unique username passed
--assigned-time INTEGER Time in seconds passed to the laboratory
--back TEXT URL to send the user back
--locale TEXT Language
--dont-open-browser Do not open the fake use in a web browser
--help Show this message and exit.
So, for example, you could also run:
$ flask weblab fake new --name "Homer Simpson" --username hsimpson \
--username-unique "hsimpson@labsland" \
--assigned-time 600 \
--back https://en.wikipedia.org/wiki/Homer_Simpson \
--locale en
--dont-open-browser
Note
If it tells you that the command does not exist:
$ flask weblab fake new
Usage: flask [OPTIONS] COMMAND [ARGS]...
Error: No such command "weblab".
$
It means that there is another error in another layer. For example:
- You may have forgotten to do the
export FLASK_APP=laboratory.py
(quite common)- There might be an error that causes your code to raise an exception. Try to run:
$ python laboratory.py
to see if there is an unhandled error you’re missing.
You can also fake stopping the current session by running:
$ flask weblab fake dispose
It will delete the current session, so in the next weblab_user
, it will be already an ExpiredUser
.
You can also fake what’s the current status as WebLab-Deusto does, contacting your laboratory every few seconds:
$ flask weblab fake status
Which will return a number indicating when you should contact again, in seconds.
Production¶
In a production environment, you should always use WebLab-Deusto, and you can rely on the Flask deployment documentation to see how to deploy it.
In the advanced example in the github repository you have two scripts to run it using the popular gunicorn web server. These files are wsgi_app.py and gunicorn_start.sh, and you can install gunicorn by running:
$ pip install gunicorn