Quickstart ==================== .. _mozilla: https://developer.mozilla.org/en/docs/web/HTTP/Cookies .. _request: request.html .. _template: template.html .. _config: config.html .. _boring: https://github.com/horlarwumhe/boring .. _cheroot: https://cheroot.cherrypy.org .. _gunicorn: https://gunicorn.org 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. .. toctree:: config :maxdepth: 1 :caption: contents: .. On This Page ... .. .. contents:: .. :depth: 2 .. :local: 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; .. code:: python 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 :meth:`app.run ` for usage. Routing --------- .. code:: python from glass import GlassApp app = GlassApp() @app.route('/') def home(): return 'This is home page' .. code:: python 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. .. code:: python @app.route('//') def test(user): return 'Hello %s'%user @app.route('/reset/') def reset(code): # do_something_with(code) return 'Hello' The url rule can take optional converter. - int ```` will match integer only [0-9] - str ```` will match match all except ``/`` and space. - path ```` will match all except space. Usually for files. .. code:: python @app.route('/read/') def read(post_id): # /read/1 assert isinstance(post_id,int) return 'Hello' @app.route('/view/') def view(username): # /view/name return username @app.route('/view/') def read_file(file): # /view/a/b/c/d return FileResponse(file) # /read/ will match /read/82 # but not /read/fssj .. note:: - If the url rule doesn't end with slash e.g. ``'/post/'``, this url ``/post/1/`` will return ``404 Not Found``, but ``/post/1`` will match. - If the url rule ends with slash e.g. ``/user/login/``, user using this ``/user/login`` will be redirected to the original url ``/user/login/`` 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/') 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 :func:`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('//') def reset_password(username,code): return "hello" To build URL for this view, provide the URL parameters to the :func:`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/') 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 :func:`app.route`. :: @app.route('/u/',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//',view_name="view1") @app.route('/u/',view_name='view2') def view(user,code=None): if code is None: return user # /u/ else: # /u// return user+code # url_for('view1',user='username',code='code') # url_for('view2',user='username') The function :func:`url_for` is also available in the Glass template. :: @app.route('/') def home(): return render_template('home.html') @app.route('/u//') def reset_password(username,code): return "hello" @app.route('/posts/') def get_user_post(username): return "Hello" :: # home.html Login Reset My Posts 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``; .. code:: python @app.route('/') def home(): return 'Hello' Return response with status code; .. code:: python @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. .. code:: python @app.route('/') def home(): return {'name':'username','id':2},200 # returning code is optional .. _using-response: Response Object ----------------- To have more control over the response e.g. setting headers, cookies, content_type, use :class:`~glass.response.Response` object. .. code:: python 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 :class:`glass.response.JsonResponse` to return response object as json. :: from glass.response import JsonResponse @app.route('/api/user/') 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. .. code:: python 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. .. code:: python # 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 ---------- :meth:`before_request ` 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 :meth:`after_request ` 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, :class:`~glass.response.Response` object and returns the response object. .. _mount-app: Mounting The App ------------------ When handling request,Glass pushes the application to its internal stack (``list``) and bind :class:`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 :func:`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 :func:`mount` to use :class:`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

Hello {{user.username}}, you login at {{now}} from browser {{request.user_agent}} ip address: {{request.environ.REMOTE_ADDR}}

Working With Cookies ----------------------- You can set cookies and also get the cookies sent to the server. To set cookies, use :meth:`Response.set_cookie ` and :attr:`request.cookies ` to get cookies sent to the server. .. code:: python from glass import Response @app.route('/') def home(): resp = Response('Hello') resp.set_cookie('cookie1','value1') resp.set_cookie('cookie2','value2') resp.set_cookie('cookie3','value3', max_age=989,domain='domain.com', path='/',httponly=False,secure=False) return resp :meth:`~glass.response.BaseResponse.set_cookie` accepts the following keywords - expires (default to ``None``) - max_age (default to ``None``) - path (default to '/') - httponly (default to ``False``) - secure (default to ``False``) - domain (default to ``None``) - samesite (default to ``None``) Read more at `mozilla `_ for more details about these values. Get the cookies sent to the server; :: from glass import request @app.route('/') def home(): cookie = request.cookies.get('cookie_name') # do_something_with_cookie(cookie) return 'Hello' :attr:`request.cookies ` returns ``dict`` . Remove cookie; .. code:: python @app.route('/del') def clear(): resp = Response('Hello') resp.delete_cookie('cookie_name') return resp :meth:`~glass.response.BaseResponse.delete_cookie` accepts keywords as ``set_cookie`` .. _using-session: 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. .. code:: python 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 remove'%name @app.route('/set',methods=['GET','POST']) def set_name(): 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. :class:`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``. You can write your own session storage to manage session. .. code:: python 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 :ref:`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 %}

{{message}}

{% endfor %}
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; ::
.. code:: python 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. ::
.. code:: python 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 :class:`~glass.requests.Request` for other methods. User Authentication --------------------- You can use Glass :ref:`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 :doc:`configuration ` .. code:: python 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. .. _using-template: 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 :doc:`documentation