Quickstart¶
This page shows introduction to Glass. The content of this page is short, as Glass is just micro-framework. The code examples are self explanatory and it is easy to understand.
contents:
Introduction¶
First, install from pypi:
$ pip install glass-web
Upgrade to latest version.
$ pip install --upgrade glass-web
You can clone it from github with git.
$ git clone https://github.com/horlarwumhe/glass.git
$ cd glass
$ pip install -r requirements.txt
$ python setup.py install
Your first code;
from glass import GlassApp
app = GlassApp()
@app.route('/')
def home():
return 'This is home page'
Starting The Server¶
You can use any wsgi web server to start the app. gunicorn, cheroot, or boring.
$ pip install boring
$ boring first:app --reload
Assuming the source is saved as first.py.
With gunicorn ;
$ gunicorn first:app --reload
Note
To use the builtin server, add app.run to your code.
Use the builtin server only for development. Dont use in production.
# file first.py
from glass import GlassApp
app = GlassApp()
@app.route('/')
def home():
return "Hello"
app.run()
$ python first.py
check app.run for usage.
Routing¶
from glass import GlassApp
app = GlassApp()
@app.route('/')
def home():
return 'This is home page'
from glass import GlassApp
app = GlassApp()
@app.route('/login/')
def login():
return 'This is login page'
@app.get('/')
def do_get():
# for GET method
return 'Hello'
@app.post('/post/')
def do_post():
# for POST method
return "Hello"
Using variable url rule.
@app.route('/<user>/')
def test(user):
return 'Hello %s'%user
@app.route('/reset/<code>')
def reset(code):
# do_something_with(code)
return 'Hello'
The url rule can take optional converter.
- int
<int:param>will match integer only [0-9]
- str
<str:name>will match match all except/and space.
- path
<path:file>will match all except space. Usually for files.
@app.route('/read/<int:post_id>')
def read(post_id):
# /read/1
assert isinstance(post_id,int)
return 'Hello'
@app.route('/view/<str:username>')
def view(username):
# /view/name
return username
@app.route('/view/<path:file>')
def read_file(file):
# /view/a/b/c/d
return FileResponse(file)
# /read/<int:post_id> will match /read/82
# but not /read/fssj
Note
If the url rule doesn’t end with slash e.g.
'/post/<post_id>', this url/post/1/will return404 Not Found, but/post/1will match.If the url rule ends with slash e.g.
/user/login/, user using this/user/loginwill be redirected to the original url/user/login/
Optional URL parameters
You can make a parameter of a URL optional by appending ? to the parameter
# <token> is optional
@app.route('/reset/<int:user_id>/<token>?')
def reset(user_id,token):
if not token:
#/reset/<user_id>
else:
# /reset/<user_id>/<token>
return "hello"
#<post_title> is optional
@app.route('/<post_id>/<post_title>?/read')
def read(post_id,post_title):
if post_title:
# /<post_id/read
else:
# /<post_id/<post_title>/read
return "hello"
Custom Converter.
You can write a converter to match this (/1-2-3-4-5) and convert it to list of integers.
class IntList:
name = 'int_list'
regex = r'(\d+\-?)+'
def to_python(self,value):
values = values.strip().split('-')
return list(map(int,values))
def to_url(self,value):
if isinstance(value,str):
return value
return '-'.join(map(str,value))
app = GlassApp()
# register the converter
app.use_converter(IntList)
#this will match integers seperated by - (1-2-78-3)
#
#http://domain.com/nums/1-23-34
@app.route('/nums/<int_list:values>')
def test(values):
assert isinstance(values,list)
for i in values:
assert isinstance(i,int)
return "Hello"
By default, each url function allows GET request method. Glass will return 405 Method Not Allowed if any other request method is used. You need to provide other methods to the view.
from glass import request
@app.route('/login/',methods=['GET','POST','PUT'])
def login():
if request.method == 'POST':
do_something()
elif request.method == 'PUT':
do_other_thing()
else:
# GET method
do()
return 'Hello'
@app.route('/delete/',methods=['GET',"DELETE"])
def delete():
if request.method == 'DELETE':
do_delete()
else:
# GET
do_get()
return "Hello"
URL Building
To build url for a specific function, use the url_for. Using url_for makes it easy to change url for your view without the needs to look for where the url is used.
from glass import GlassApp
from glass import url_for
from glass import request,redirect
app = GlassApp()
@app.route('/index')
def home():
if not request.user:
return reirect(url_for('login'))
return "Hello"
@app.route('/user/login')
def login():
if request.user:
return redirect(url_for('home'))
return render_template('login.html')
Building URLs with parameters.
@app.route('/<username>/<code>')
def reset_password(username,code):
return "hello"
To build URL for this view, provide the URL parameters to the url_for().
with app.mount():
path = url_for('reset_password',username="siteuser",code="user-reset-code")
print(path)
# /siteuser/user-reset-code
You can also build url with query string by providing the query string keys and values.
@app.route('/u/<username>')
def get_user_post(username):
# /u/username?sort=true&max=10
sort = request.query.get('sort')
max = request.query.get('max')
with app.mount():
path = url_for('get_user_post',username='username',max='10',sort='true')
print(path)
# /u/username?max=10&sort=true
Building URL with fragment or target.
http://domain.com/a/b#target
with app.mount():
path = url_for('login',_fragment="Loginform")
print(path)
# /user/login#Loginform
If SERVER_NAME is available in the app configuration, url_for will return the full url.
app.config['SERVER_NAME'] = 'https://mysite.com'
with app.mount():
path = url_for('login')
print(path)
# 'https://mysite.com/user/login
with app.mount():
path = url_for('get_user_post',username='username',max='10',sort='true')
print(path)
# 'https://mysite.com/u/username?max=10&sort=true'
with app.mount():
path = url_for('get_user_post',username='user name')
print(path)
# https://mysite.com/u/user%20name
Instead of using the function as the name of the view, you can pass view_name to app.route().
@app.route('/u/<user>',view_name='profile')
def get_user_details(user):
return "hello"
with app.mount():
path = url_for('profile',user="username")
print(path)
# /u/username
This is useful when using one view for different urls.
@app.route('/u/<user>/<code>',view_name="view1")
@app.route('/u/<user>',view_name='view2')
def view(user,code=None):
if code is None:
return user
# /u/<user>
else:
# /u/<user>/<code>
return user+code
# url_for('view1',user='username',code='code')
# url_for('view2',user='username')
The function url_for() is also available in the Glass template.
@app.route('/')
def home():
return render_template('home.html')
@app.route('/u/<username>/<code>')
def reset_password(username,code):
return "hello"
@app.route('/posts/<username>')
def get_user_post(username):
return "Hello"
# home.html
<a href='{% url_for "login" %}'>Login </a>
<a href='{% url_for "reset_password" username=user.name code=user.code %}'>Reset </a>
<a href='{% url_for "get_user_post" username="username" max="10" sort="true" %}'>My Posts </a>
Note: there should be no space between the keywords and value;
{% url_for "view" key1=value1 key2=value3 %}
This will raise an exception;
{% url_for "view" key1= value1 key2=value3 %}
Response¶
Each function handling a url rule must return a valid response. The view can return the following valid responses.
Return str;
@app.route('/')
def home():
return 'Hello'
Return response with status code;
@app.route('/')
def home():
return 'Hello',200
Return dict.
If the view return dict, the response headers Content_Type will be set to application/json
and the response dict will be converted to json.
@app.route('/')
def home():
return {'name':'username','id':2},200
# returning code is optional
Response Object¶
To have more control over the response e.g. setting headers, cookies, content_type, use Response object.
from glass import Response
from glass import GlassApp
app = GlassApp()
@app.route('/')
def home():
# set headers
headers = {'header1':'value1','header2':'value2'}
# or as list
# headers = [('header1', 'value1'), ('header2', 'value2')]
# set content_type
content_type = 'text/plain'
# set response code
code = 200
response = Response('Hello',headers=headers,
content_type=content_type,status_code=code)
return response
Return json
Use
glass.response.JsonResponseto return response object as json.
from glass.response import JsonResponse
@app.route('/api/user/<int:user_id/')
def get_user(user_id):
user = get_user_from_db(user_id)
data = {
'name':user.name,
'email':user.email
}
return JsonResponse(user)
JsonResponse takes same arguments as Response.
Redirect¶
To redirect users to another url, use redirect().
from glass import url_for
from glass import redirect
from glass import session
@app.route('/')
def home():
name = session.get('name')
if not name:
return redirect(url_for('login'))
return "Hello %s"%name
@app.route('/login',methods=['GET',"POST"])
def login():
name = request.post.get('name','')
session['name'] = name
return 'Hello'
Sending Files¶
from glass.response import FileResponse
@app.route('/file/<path:filename>')
def send_file(filename):
return FileResponse(filename)
Handling Errors¶
In case an exception occurs in the application, you can register a function to call when the error occurs.
The function can be registered using error code or exception class.
from glass import GlassApp, request
app = GlassApp()
@app.error(404)
def not_found(error):
r = 'The url %s not found'%request.path
return r, 404
@app.error(500)
def internal_error(error):
return 'Hoooops, Internal Error ', 500
Handling error with exception class.
# you must set DEBUG to False for this work
app.config['DEBUG'] = False
@app.error(NameError)
def handle(exc):
assert isinstance(exc,NameError)
return 'Hoooops, that is NameError....'
from somemodule.exceptions import FooError
@app.error(FooError)
def foo_handler(exc):
return "FooError occurs"
@app.error(Exception)
def err_handler(exc):
return "%s error occurs"%exc.__class__
App API¶
Use this decorator to register function(s) to call before each app request. This function can be used to open db connection or load logged in user.
from glass import GlassApp, request,session
app = GlassApp()
@app.route('/')
def home():
if request.user is None:
user = 'Guest'
else:
user = request.user.username
return 'Hello %s'%user
@app.before_request
def load_user():
id = session.get('user_id')
if id:
user = get_user_from_db(id)
request.user = user
else:
request.user = None
# make sure set request.user = value
# to avoid getting AttributeError in the
# view function
If the any of the functions of .before_request returns response, the response will be used and the view function of the request url will not be called.
@app.before_request
def unavailable():
return "The site is under development"
The functions will be called in order they are created.
@app.before_request
def first():
# this will be called first
pass
@app.before_request
def second():
pass
Use this decorator to register a fuction(s) to call after each request.
@app.after_request
def after(response):
response.set_header('name','value')
return response
The function takes one argument, Response object and returns the response object.
Mounting The App¶
When handling request,Glass pushes the application to its internal stack (list) and bind request to the environ from the wsgi web server. Then, you can access current app and request from any where in the app, as long as there is active request. Likewise, some functions in Glass require active http request to work. Functions like render_template()
from glass import GlassApp, current_app,render_template
app = GlassApp()
@app.route('/')
def home():
headers = request.headers
config = current_app.config
return render_template("index.html")
The following will raise RuntimeError.
from glass import GlassApp, current_app,render_template
app = GlassApp()
# raise RuntimeError
res = render_template('index.html')
# raise RuntimeError
host = request.host
# raise RuntimeError
db = current_app.config("DB_ENGINE")
If there is need to use these functions when not handling request, you can manually mount the app.
from glass import GlassApp, current_app,render_template
app = GlassApp()
with app.mount():
render_template('index.html')
current_app.config is app.config
print(url_for('home'))
with app.mount():
init_db()
You can pass environ argument to mount() to use request object.
from glass import GlassApp, current_app,render_template,request
app = GlassApp()
environ = {} # wsgi environ
with app.mount(environ):
request.headers
request.host
current_app and request are only available from thread where they are initailized. If you create another thread, then you mount the app again.
from glass import GlassApp, current_app,render_template
from threading import Thread
app = GlassApp()
@app.route('/login')
def login():
login_user(user)
Thread(target=send_login_mail,args=(request.environ,user)).start()
return "Hello"
def send_login_mail(environ,user):
# this is another thread, re-mount the app
now = str(datetime.utcnow())
with app.mount(environ):
body = render_template('login_email.html',user=user,now=now)
send_mail(user.email,body)
# login_email.html
<h3> Hello {{user.username}}, you login at {{now}} from
browser {{request.user_agent}} ip address: {{request.environ.REMOTE_ADDR}} </h3>
Session¶
The session object allows you to store information about a request. The data store in session are different for different requests. Only JSON serializable object can be stored in the session.
To use session, you need to set app secret_key.
from glass import session,request
from glass import GlassApp
from glass import redirect
from glass import url_for
app = GlassApp()
#
app.config['SECRET_KEY'] = 'some secret'
@app.route('/')
def home():
name = session.get('name')
if not name:
return 'Hello guest'
return 'Hello %s <a href="/del">remove</a>'%name
@app.route('/set',methods=['GET','POST'])
def set_name():
form = '''
<form method='POST'>
<input type='text' name='username'>
<input type='submit'>
</form>
'''
if request.method == 'POST':
name = request.post.get('username')
if name:
session['name'] = name
return redirect(url_for('home'))
return form
@app.route('/del')
def remove_name():
session.pop('name')
return redirect('/')
Session class is dict object, so all methods of dict are available.
session API docs.
Note
Like flask, session data are stored in the cookie sent to the browser, unlike django which save session data inside database.
The default method used to encode session data only guarantee the integrity of the cookie. Anyone can decode and see the content of the cookie, but it can`t be modified, because the sha1 hash of cookie is sent with it. If an hacker modified the cookie, it will be imposible to recompute the hash value unless the hacker has access to the app secret key.
Glass support redis as session storage.
from glass import GlassApp
from glass.sessions import RedisSessionManager
app = GlassApp()
app.session_cls = RedisSessionManager(host='localhost',port=6379,db=1)
You should set SESSION_COOKIE_MAXAGE in your config so redis can auto remove expire cookies.
app.config['SESSION_COOKIE_MAXAGE'] = 60 * 5 # 5 minutes
You can write your own session storage to manage session.
from glass import session
from glass import current_app
from glass import request
class MySessionManager:
# must define two methods, open() and save()
def open(self):
# session_cookie_name
name = current_app.config["SESSION_COOKIE_NAME"]
cookie = request.cookies.get(name)
if cookie:
# you implement this function
# get the session data from where it is stored
data = get_session_data(cookie)
# data must be dict
# bind the data to the current request
session.bind(data)
else:
session.bind({})
def save(self,response):
# save current session data and get cookie
# you implement this function
cookie = save_data(session.session_data)
name = current_app.config['SESSION_COOKIE_NAME']
# you need to set cookies attributes
# expires, max-age,httponly,secure,samesite
#
expires = current_app.config['SESSION_COOKIES_EXPIRE']
max_age = current_app.config['SESSION_COOKIE_MAXAGE']
response.set_cookie(name,cookie,...)
app = GlassApp()
app.session_cls = MySessionManager()
See Session Configuration on how to configure session cookie.
Note
Session data are threading.local instance. This make the data thread safe on multi-thread web server.
Message Flashing¶
from glass import GlassApp, flash
from glass.templating import render_template as render
@app.route('/')
def home():
return render('index.html')
@app.route('/form',method=['GET','POST']):
def form():
if request.method == 'POST':
name = request.post.get('name')
if len(name) < 10:
flash('name too short')
else:
flash('Hello %s'%name)
return render('form.html')
use get_flash_messages in the template
# form.html
{% for message in get_flash_messages %}
<p> {{message}} </p>
{% endfor %}
<form method='POST'>
<input type='text' name='name'>
<button>go</button>
<form>
Working With Request Data¶
Request object contains current request data. Such as request headers, method, cookies, HTML form data and files sent to the server.
HTML form;
<form method='POST'>
<input type='text' name='username'>
<input type='password' name='password'>
</form>
from glass import request
@app.route('/home',methods=['GET','POST'])
def home():
if request.method == 'POST':
name = request.post.get('username')
return 'Hello %s'%name
return 'this is home'
Working With Files¶
To upload files, dont forget to set enctype="multipart/form-data" in your html form.
<form enctype="multipart/form-data" method="POST">
<input type='file' name='userpic'>
<button> go</button>
</form>
from glass import request
@app.route('/home',methods=['GET','POST'])
def home():
if request.method == 'POST':
file = request.files.get('userpic')
# get the filename
# name = file.filename
if file:
file.save_as('/location/on/filesytem')
return 'Hello'
Note
request is made as global object. Despite being global,the object is thread safe. The request relies on WSGI environ which is attached to the request with threading.local.
Read more on Request for other methods.
User Authentication¶
You can use Glass session object for user authentication.
Here is a simple example of how to authenticate user.
from glass import request,session,redirect
from glass import GlassApp
from your_app.db import get_user, auth_user
app = GlassApp()
@app.route('/home')
def home():
if request.user is None:
username = 'Guest'
else:
username = request.user.username
return 'Hello %s'%username
@app.route('/login',methods=['GET','POST'])
def login():
error = ''
if request.method == 'POST':
username = request.post.get('username')
password = request.post.get('password')
user = auth_user(username,password)
if user:
next_page = request.args.get('next','/')
login_user(user)
return redirect(next_page)
error = 'Invalid username or password'
return render_template('login.html',error=error)
@app.route('/logout')
def logout():
# logout user
if request.user is None:
return redirect('/')
logout_user(request.user)
return redirect('/')
def login_user(user):
# save user id in the session
session['user_id'] = user.id
def logout_user():
session.pop('user_id')
# del session['user_id']
@app.before_request
def load_user():
# this function will be called before calling view function
# for the request url
#load user from session
request.user = None
user_id = session.get('user_id')
if user_id:
user = get_user(user_id)
if not user:
# user not found. delete user_id from session
session.pop('user_id')
else:
#
request.user = user
Here is example of how to allow only authenticated user access a view function using decorator.
from functools import wraps
def admin_only(view_func):
@wraps(view_func)
def inner(*args,**kwargs):
if request.user is None:
return redirect('/login')
if not request.user.is_admin:
flash("Admin only")
return redirect('/')
return view_func(*args,**kwargs)
return inner
def login_require(view_func):
@wraps(view_func)
def inner(*args,**kwargs):
if request.user is None:
return redirect('/login')
return view_func(*args,**kwargs)
return inner
@app.route('/admin')
@admin_only
def admin():
return "Welcome admin"
@app.route('/view')
@login_require
def view():
return "Hello"
Configuration¶
The configuration pattern used is similar to flask. All the config values are stored in dict.
Glass has some predefined configurations. Read more at configuration
app = GlassApp()
class CONFIG:
DEBUG = True
KEY = 'value'
DB_ENGINE = 'postgresql://user:password@localhost:5432/glass'
app.config.from_object(CONFIG)
# config from dict
config = {'DEBUG':True,'KEY':'value'}
app.config.from_dict(config)
# config from json
app.config.from_json('path/to/file.json')
# the configurations can be accesed as
app.config['DEBUG']
app.config['KEY']
Static Files¶
Glass will look for static folder in the current working to serve static files (css,js,images,…). You can set another directory to find static files.
app = GlassApp()
app.config['STATIC_FOLDER'] ='/path/to/files'
Like flask and django, default url for static files is /static/.
Note
For performance purpose,do not use the app to serve static files.
Template¶
Glass comes with template engine. The docs here show how to use the template engine with Glass. The full docs is available in the template documentation.
Template Quickstart¶
The template syntax is very similar to django template.
from glass import render_template as render, import render_string
from glass.response import Response
@app.route('/')
def home():
return render('index.html',context={})
@app.route('/<user>')
def get_user(user):
template = '''
Hello <b> {{user}} </b>
'''
return render_string(template,user=user)
@app.route('/login',methods=['GET',"POST"])
def login():
name = email = ''
if request.method == 'POST':
name = request.post.get('name')
email = request.post.get('email')
res = render('login.html',name=name,email=email)
return Response(res)
You can also TemplateResponse.
from glass.response import TemplateResponse
app = GlassApp()
@app.route('/index')
def login():
name = email = ''
if request.method == 'POST':
name = request.post.get('name')
email = request.post.get('email')
return TemplateResponse('login.html',name=name,email=email)
TemplateResponse() takes same arguments as Response
TemplateResponse('login.html',name=name,email=email,
status_code=200,headers={'header':'value'})
<!-- file login.html -->
<body>
{% if name %}
<div> Hello {{name}} </div>
<div> Your email is {{email}} </div>
{%else %}
Hello guest
{% endif %}
<form method='POST'>
<input type='text' name='name'>
<input type='email' name='email'>
<button>submit</button>
</form>
<body>
Using Python dict or list.
{{ dict.key }} # dict[key]
{{list.0}} list[0]
{{ list.2.upper }} # list[2].upper()
Glass will look for templates folder in the current working directory to load templates. You can set another directory for templates.
app = GlassApp()
app.config['TEMPLATES_FOLDER'] = '/path/to/templates'
set multiple directories;
app.config['TEMPLATES_FOLDER'] = ('/path/1/templates','/path/2/templates')
The template is configured with the following global variables. They can be accessed in the template.
create global variable;
from datetime import datetime as dt
app.template_env.globals['date'] = dt.now
in the template file;
<b> {{date}} </b>
Template Filters¶
Filters are python function which modify variable in the template.
<b> {{name|upper}} </b>
The filter here is upper which modifies the value of name.
New template filter can be added to the template.
using decorator;
app = GlassApp()
@app.template_env.filter('upper')
def upper(value):
return value.upper()
@app.template_env.filter('secret')
def secret(value):
return value[:5]+'******'
manually register the filter;
def secret(value):
return value[:5]+'******'
app.template_env.filters['secret'] = secret
in the template file;
<div> {{user.username|upper}}</div>
<b> {{user.email|secret}}</b>
Filters can take optional arguments;
<div> {{user.email | secret("#")}} </div>
def secret(value,by=None):
if by is None:
return value[:5]+'******'
return value[:5]+by*5
Logging¶
Glass emits messages when incidents that need attention occur in the app. Such as unhandle exceptions. By default, Glass writes logs to process stdout. You can log app messages to another stream using logging module.
# this will log all app messages to file 'app.log'.
import logging
# the logger name used is 'glass.app'
logger = logging.getLogger('glass.app')
handler = logging.FileHandler('app.log')
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s:%(levelname)s: %(message)s', datefmt='%d/%m/%Y %H:%M:%S %p')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.setLevel(logging.DEBUG)
app = GlassApp()
@app.route('/')
def home():
return "Hello"
@app.error(500)
def error(e):
logger.info('error occurs')
return render_template('500.html')
check Python logging module documentation.
Using Jinja2¶
If you want to use jinja2 instead of the builtin template, there is wrapper method for jinja2 API.
# configure your app to use jinja2
app = GlassApp()
app.config['TEMPLATE_BACKEND'] = 'jinja'
app.jinja_env returns JinjaEnvironment which is subclass of jinja2.Environment.
Create a filter for jinja2
@app.jinja_env.register_filter('split')
def join(value,param=''):
return value.split(param)
{{name | split(param=',')}}
Global Value¶
app.jinja_env.globals['name'] = 'global_value'
Working with database¶
Glass does not come with ORM. You can use sqlalchemy,ponyorm,sqlobject or use pure SQL using sqlite3 .
Full example¶
from glass import GlassApp, request,session
from glass.response import Redirect as redirect
from glass.templating import render_template
from glass import flash
app = GlassApp()
app.config['SECRET_KEY'] = 'some secret'
@app.route('/')
def home():
return render_template('index.html')
@app.route('/logout')
def logout():
session.pop('user_id')
return redirect('/')
@app.route('/login',methods=['GET','POST'])
def login():
error = ''
if request.method == 'POST':
name = request.post.get('username')
if name:
user = db.get(name)
if user:
login_user(user)
return redirect('/')
else:
error = 'invalid username'
else:
error = 'provide username to login'
if error:
flash(error)
return render_template('login.html')
def login_user(user):
session['user_id'] = user.id
@app.before_request
def load_user():
# to avoid getting attribute error
# make sure to set request.user = value
user_id = session.get('user_id')
if user_id:
# get user from db
# you have to implement your own db
# either using sqlalchemy or other orm
user = db.get(id=user_id)
if user:
request.user = user
else:
request.user = None
else:
request.user = None
def login_require(func):
def inner(*args,**kwargs):
if request.user is None:
return redirect('/login')
return func(*args,**kwargs)
return inner
@app.route('/secret')
@login_require
def view():
return 'Hello, you are authenticated'