Creating your first 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 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:
mkdir apps/myapp
echo '' > apps/myapp/__init__.py
Notice that for Windows, you must use backslashes (i.e. ’’) instead of slashes. Also, an empty init.py file is not strictly needed since Python 3.3, but it will be useful later on. 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. An 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 bottle static_file method for serving static files, which means 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()
Restart py4web or press the Dashboard “Reload Apps” button, 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
@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```````
And these are possible filters (only `:
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
.
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 a Bottle request object.
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.
From _scaffold
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.
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:
├── __init__.py # imports everything else
├── common.py # defines useful objects
├── controllers.py # your actions
├── databases # your sqlite databases and metadata
│ └── README.md
├── models.py # your pyDAL table model
├── settings.py # any settings used by the app
├── settings_private.py # (optional) settings that you want to keep private
├── static # static files
│ ├── README.md
│ ├── components # py4web's vue auth component
│ │ ├── auth.html
│ │ └── auth.js
│ ├── css # CSS files, we ship bulma because it is JS agnostic
│ │ └── no.css # we used bulma.css in the past
│ ├── favicon.ico
│ └── js # JS files, we ship with these but you can replace them
│ ├── axios.min.js
│ ├── sugar.min.js
│ ├── utils.js
│ └── vue.min.js
├── tasks.py
├── templates # your templates go here
│ ├── README.md
│ ├── auth.html # the auth page for register/logic/etc (uses vue)
│ ├── generic.html # a general purpose template
│ ├── index.html
│ └── layout.html # a bulma layout example
└── translations # internationalization/pluralization files go here
└── it.json # py4web internationalization/pluralization files are in JSON, this is an italian example
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 by Bottleredirect
andURL
are similar to their web2py counterpartshelpers (
A
,DIV
,SPAN
,IMG
, etc) must be imported fromyatl.helpers
. They work pretty much as in web2pydb
,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.
App Watchdog
Py4web facilitates a development server’s setup that automatically reloads an app when its Python source files change. Any other files inside an app can be watched by setting a handler function using ``@app_watch_handler`` decorator.
--watch [off|sync|lazy] Watch python changes and reload apps
automatically, modes: off (default), sync,
lazy
Two examples of its usage are reported now. Do not worry if you don’t fully undestand 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.