Creating an app
From scratch
Apps can be created using the dashboard or directly from the filesystem. Here, we are going to do it manually, as the Dashboard is already described in its own chapter.
Keep in mind that an app is a Python module; therefore it needs only a
folder and a __init__.py
file in that folder.
Note
An empty __init__.py file is not strictly needed since Python 3.3, but it will be useful later on.
Open a command prompt and go to your main py4web folder. Enter the following simple commands in order to create a new empty myapp app:
mkdir apps/myapp
echo '' > apps/myapp/__init__.py
Tip
for Windows, you must use backslashes (i.e. \
) instead of
slashes.
If you now restart py4web or press the “Reload Apps” in the Dashboard, py4web will find this module, import it, and recognize it as an app, simply because of its location. By default py4web runs in lazy watch mode (see the run command option) for automatic reloading of the apps whenever it changes, which is very useful in a development environment. In production or debugging environment, it’s better to run py4web with a command like this:
py4web run apps --watch off
A py4web app is not required to do anything. It could just be a container for static files or arbitrary code that other apps may want to import and access. Yet typically most apps are designed to expose static or dynamic web pages.
Static web pages
To expose static web pages you simply need to create a static
subfolder, and any file in there will be automatically published:
mkdir apps/myapp/static
echo 'Hello World' > apps/myapp/static/hello.txt
The newly created file will be accessible at
http://localhost:8000/myapp/static/hello.txt
Notice that static
is a special path for py4web and only files under
the static
folder are served.
Important
Internally py4web uses the ombott (One More BOTTle) web server, which is a minimal and fast bottlepy spin-off. It supports streaming, partial content, range requests, and if-modified-since. This is all handled automatically based on the HTTP request headers.
Dynamic Web Pages
To create a dynamic page, you must create a function that returns the
page content. For example edit the myapp/__init__.py
as follows:
import datetime
from py4web import action
@action('index')
def page():
return "hello, now is %s" % datetime.datetime.now()
Reload the app, and this page will be accessible at
http://localhost:8000/myapp/index
or
http://localhost:8000/myapp
(notice that index is optional)
Unlike other frameworks, we do not import or start the webserver within
the myapp
code. This is because py4web is already running, and it
may be serving multiple apps. py4web imports our code and exposes
functions decorated with @action()
. Also notice that py4web prepends
/myapp
(i.e. the name of the app) to the url path declared in the
action. This is because there are multiple apps, and they may define
conflicting routes. Prepending the name of the app removes the
ambiguity. But there is one exception: if you call your app
_default
, or if you create a symlink from _default
to myapp
,
then py4web will not prepend any prefix to the routes defined inside the
app.
On return values
py4web actions should return a string or a dictionary. If they return a
dictionary you must tell py4web what to do with it. By default py4web
will serialize it into json. For example edit __init__.py
again and
add at the end
@action('colors')
def colors():
return {'colors': ['red', 'blue', 'green']}
This page will be visible at
http://localhost:8000/myapp/colors
and returns a JSON object {"colors": ["red", "blue", "green"]}
.
Notice we chose to name the function the same as the route. This is not
required, but it is a convention that we will often follow.
You can use any template language to turn your data into a string. PY4WEB comes with yatl, a full chapter will be dedicated later and we will provide an example shortly.
Routes
It is possible to map patterns in the URL into arguments of the function. For example:
@action('color/<name>')
def color(name):
if name in ['red', 'blue', 'green']:
return 'You picked color %s' % name
return 'Unknown color %s' % name
This page will be visible at
http://localhost:8000/myapp/color/red
The syntax of the patterns is the same as the Bottle routes. A route wildcard can be defined as
<name>
or<name:filter>
or<name:filter:config>
And these are possible filters (only :re
has a config):
:int
matches (signed) digits and converts the value to integer.:float
similar to :int but for decimal numbers.:path
matches all characters including the slash character in a non-greedy way, and may be used to match more than one path segment.:re[:exp]
allows you to specify a custom regular expression in the config field. The matched value is not modified.
The pattern matching the wildcard is passed to the function under the
specified variable name
.
Note that the routing is implemented in ombott as radix-tree hybrid router. It is declaration-order-independent and it prioritizes static route-fragment over dynamic one, since this is most expected behavior.
This results in some constraints, such as one cannot have more than one route that has dynamic fragment of different types (int, path) in the same place.. Hence something like this is incorrect and will result in errors:
@action('color/<code:int>')
def color(code):
return f'Color code: {code}'
@action('color/<name:path>')
def color(name):
return f'Color name: {name}'
Instead, to accomplish a simmilar result, one needs to handle all the logic in one action:
@action('color/<color_identifier:path>')
def color(color_identifier):
try:
msg = f'Color code: {int(color_identifier)}'
except:
msg = f'Color name: {color_identifier}'
return msg
Also, the action decorator takes an optional method
argument that
can be an HTTP method or a list of methods:
@action('index', method=['GET','POST','DELETE'])
You can use multiple decorators to expose the same function under multiple routes.
The request
object
From py4web you can import request
from py4web import request
@action('paint')
def paint():
if 'color' in request.query:
return 'Painting in %s' % request.query.get('color')
return 'You did not specify a color'
This action can be accessed at:
http://localhost:8000/myapp/paint?color=red
Notice that the request object is equivalent to a Bottle request object. with one additional attribute:
request.app_name
Which you can use the code to identify the name and the folder used for the app.
Templates
In order to use a yatl template you must declare it. For example create a file apps/myapp/templates/paint.html
that contains:
<html>
<head>
<style>
body {background:[[=color]]}
</style>
</head>
<body>
<h1>Color [[=color]]</h1>
</body>
</html>
then modify the paint action to use the template and default to green.
@action('paint')
@action.uses('paint.html')
def paint():
return dict(color = request.query.get('color', 'green'))
The page will now display the color name on a background of the corresponding color.
The key ingredient here is the decorator @action.uses(...)
. The
arguments of action.uses
are called fixtures. You can specify
multiple fixtures in one decorator or you can have multiple decorators.
Fixtures are objects that modify the behavior of the action, that may
need to be initialized per request, that may filter input and output of
the action, and that may depend on each-other (they are similar in scope
to Bottle plugins but they are declared per-action, and they have a
dependency tree which will be explained later).
The simplest type of fixture is a template. You specify it by simply
giving the name of the file to be used as template. That file must
follow the yatl syntax and must be located in the templates
folder
of the app. The object returned by the action will be processed by the
template and turned into a string.
You can easily define fixtures for other template languages. This is described later.
Some built-in fixtures are:
the DAL object (which tells py4web to obtain a database connection from the pool at every request, and commit on success or rollback on failure)
the Session object (which tells py4web to parse the cookie and retrieve a session at every request, and to save it if changed)
the Translator object (which tells py4web to process the accept-language header and determine optimal internationalization/pluralization rules)
the Auth object (which tells py4web that the app needs access to the user info)
They may depend on each other. For example, the Session may need the DAL (database connection), and Auth may need both. Dependencies are handled automatically.
The _scaffold app
Most of the times, you do not want to start writing code from scratch.
You also want to follow some sane conventions outlined here, like not
putting all your code into __init__.py
. PY4WEB provides a
Scaffolding (_scaffold) app, where files are organized properly and many
useful objects are pre-defined. Also, it shows you how to manage users and
their registration.
Just like a real scaffolding in a building construction site, scaffolding
could give you some kind of a fast and simplified structure for your project,
on which you can rely to build your real project.
You will normally find the scaffold app under apps, but you can easily create a new clone of it manually or using the Dashboard.
Here is the tree structure of the _scaffold
app:
The scaffold app contains an example of a more complex action:
from py4web import action, request, response, abort, redirect, URL
from yatl.helpers import A
from . common import db, session, T, cache, auth
@action('welcome', method='GET')
@action.uses('generic.html', session, db, T, auth.user)
def index():
user = auth.get_user()
message = T('Hello {first_name}'.format(**user))
return dict(message=message, user=user)
Notice the following:
request
,response
,abort
are defined byombott
.redirect
andURL
are similar to their web2py counterparts.helpers (
A
,DIV
,SPAN
,IMG
, etc) must be imported fromyatl.helpers
. They work pretty much as in web2py.db
,session
,T
,cache
,auth
are Fixtures. They must be defined incommon.py
.@action.uses(auth.user)
indicates that this action expects a valid logged-in user retrievable byauth.get_user()
. If that is not the case, this action redirects to the login page (defined also incommon.py
and using the Vue.js auth.html component).
When you start from scaffold, you may want to edit settings.py
,
templates
, models.py
and controllers.py
but probably you
don’t need to change anything in common.py
.
In your html, you can use any JS library that you want because py4web is
agnostic to your choice of JS and CSS, but with some exceptions. The
auth.html
which handles registration/login/etc. uses a vue.js
component. Hence if you want to use that, you should not remove it.
Copying the _scaffold app
The scaffold app is really useful, and you will surely use it a lot as a starting point for testing and even developing full features new apps.
It’s better not to work directly on it: always create new apps copying it. You can do it in two ways:
using the command line: copy the whole apps/_scaffold folder to another one (apps/my_app for example). Then reload py4web and it will be automatically loaded.
using the Dashboard: select the button
Create/Upload App
under the “Installed Applications” upper section. Just give the new app a name and check that “Scaffold” is selected as the source. Finally press theCreate
button and the dashboard will be automatically reloaded, along with the new app.
Watch for files change
As described in the run command option, Py4web facilitates a
development server’s setup by automatically reloads an app when its
Python source files change (by default).
But in fact any other files inside an app can be watched by setting a
handler function using the @app_watch_handler
decorator.
Two examples of this usage are reported now. Do not worry if you don’t
fully understand them: the key point here is that even non-python code
could be reloaded automatically if you explicit it with the
@app_watch_handler
decorator.
Watch SASS files and compile them when edited:
from py4web.core import app_watch_handler
import sass # https://github.com/sass/libsass-python
@app_watch_handler(
["static_dev/sass/all.sass",
"static_dev/sass/main.sass",
"static_dev/sass/overrides.sass"])
def sass_compile(changed_files):
print(changed_files) # for info, files that changed, from a list of watched files above
## ...
compiled_css = sass.compile(filename=filep, include_paths=includes, output_style="compressed")
dest = os.path.join(app, "static/css/all.css")
with open(dest, "w") as file:
file.write(compiled)
Validate javascript syntax when edited:
import esprima # Python implementation of Esprima from Node.js
@app_watch_handler(
["static/js/index.js",
"static/js/utils.js",
"static/js/dbadmin.js"])
def validate_js(changed_files):
for cf in changed_files:
print("JS syntax validation: ", cf)
with open(os.path.abspath(cf)) as code:
esprima.parseModule(code.read())
Filepaths passed to @app_watch_handler
decorator must be
relative to an app. Python files (i.e. “*.py”) in a list passed to the
decorator are ignored since they are watched by default. Handler
function’s parameter is a list of filepaths that were changed. All
exceptions inside handlers are printed in terminal.
Domain-mapped apps
In production environments it is often required to have several apps being served by a single py4web server, where different apps are mapped to different domains.
py4web can easily handle running multiple apps, but there is no build-in mechanism for mapping domains to specific applications. Such mapping needs to be done externally to py4web – for instance using a web reverse-proxy, such as nginx.
While nginx or other reverse-proxies are also useful in production environments for handling SSL termination, caching and other uses, we cover only the mapping of domains to py4web applications here.
An example nginx configuration for an application myapp
mapped to
a domain myapp.example.com
might look like that:
server {
listen 80;
server_name myapp.example.com;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-PY4WEB-APPNAME /myapp;
location / {
proxy_pass http://127.0.0.1:8000/myapp$request_uri;
}
}
This is an example server
block of nginx configuration. One would have to create
a separate such block for each app/each domain being served by py4web server. Note some important aspects:
server_name
defines the domain mapped to the appmyapp
,proxy_http_version 1.1;
directive is optional, but highly recommended (otherwise nginx uses HTTP 1.0 to talkto the backend-server – here py4web – and it creates all kinds of issues with buffering and otherwise),
proxy_set_header Host $host;
directive ensures that the correctHost
is passed to py4web – heremyapp.example.com
proxy_set_header X-PY4WEB-APPNAME /myapp;
directive ensures that py4web (and ombott) knows which app to serveand also that this application is domain-mapped – pay specific attention to the slash (
/
) in front of themyapp
name – it is required to ensure correct parsing of URLs on ombott level,
- finally
proxy_pass http://127.0.0.1:8000/myapp$request_uri;
ensures that the request is passed in its integrity ($request_uri
) to py4web server (here:
127.0.0.1:8000
) and the correct app (/myapp
).
- finally
Such configuration ensures that all URL manipulation inside ombott and py4web - especially in modules such as Auth
, Form
,
and Grid
are done correctly using the domain to which the app is mapped to.
Custom error pages
py4web provides default error pages. For instance, if none of the routes in an app matches the request, a default 404 error page will be shown. By default all HTTP error codes are handled automatically by py4web.
It is however possible to override this behaviour. It can be done either per HTTP error code, or even for all errors.
Here is an example for overriding HTTP code 404 (not found):
from py4web.core import ERROR_PAGES
ERROR_PAGES[404] = f"Page not found!"
If one wants to replace _all_ default error pages, a special qualifier
"*"
should be used. Also, the returned value may contain HTML code as
well:
from py4web import URL
from py4web.core import ERROR_PAGES
from yatl.helpers import A
ERROR_PAGES["*"] = f"We have encountered an error! (try: {A('Main Page', _href=URL("/",scheme=True))})"
Note that this setup is global. This means that it is defined once for all apps on a given py4web instance. This is because, when an error is encountered, it could be because the request has not matched any of the apps. Hence, this configuration should only be done in one of the apps.