WHAT IS PY4WEB?
py4web is a framework for rapid development of secure database driven web applications.
It is the successor of web2py but much improved.
Install it and start it
$ pip install py4web # install it (but use a venv or Nix)
$ py4web setup apps # answer yes to all questions
$ py4web set_password # pick a password for the admin
$ cp -r apps/_scaffold apps/myapp # make a new app
$ py4web run apps # start py4web
Each subfolder of apps/ with an __init__.py is its own app. One py4web can run multiple apps.
You just copy the _scaffold app to make a new one.
The basic functions/objects are imported from the py4web module.
from py4web import action, redirect, request, URL, Field
Use @action to map URLs into functions (aka actions). Actions can return strings or dictionaries.
# http://127.0.0.1:8000/myapp/index
@action("index")
def index():
return "hello world"
Actions can map path_info items into variables
# http://127.0.0.1:8000/myapp/index/1
@action("index/<x:int>")
def index(x):
return f"x = {x}"
py4web uses a request object from ombott, compatible with
bottlepy
# http://127.0.0.1:8000/myapp/index/?x=1
@action("index")
def index():
x = request.query.get("x")
return f"x = {x}"
It can parse JSON from POST requests for example
# http://127.0.0.1:8000/myapp/index POST {x: 1}
@action("index", method="POST")
def index():
x = request.json.get("x")
return {"x": x}
A page can redirect to another page
@action("index")
def index():
redirect("http://example.com")
We use URL to generate the urls of internal pages
@action("index")
def index():
redirect(URL("other_page"))
We have a built-in session object which by default stores the session data, signed, in a cookie. Optionally it can be stored in db, redis,
or other custom storage. Session is a fixture
and it must be declared with @action.uses.
Think of fixtures as per action (as opposed to per app) middleware.
@action("index")
@action.uses(session)
def index():
session.x = (session.x or 0) + 1
return f"x = {x}"
An action can return a dictionary and use a template to render the dictionary into HTML. A template is also a fixture and it must be declared with @action.uses.
@action("index")
@action.uses("index.html")
def index():
x = 1
return locals()
A template can be any text but typically it is HTML. Templates can extend and include other templates. Templetes can embed variables with [[=x]] and they can also embed python code (without limitations) with double square brakets. Indentation does not matter. [[pass]] closes [[ if ... ]] and [[ for ... ]].
[[extend "layout.html"]]
x = [[=x]]
[[ for i in range(10): ]][[ if i % 2==0: ]]
[[=i]] is even
[[ pass ]][[ pass ]]
Py4web comes with a built-in auth object that generates all the pages
required for user registration, login, email verification, retrieve and change password, edit profile, single sign on with OAuth2 and more.
auth is also a fixture which exposed the current user to the action. Notice that fixtures have dependencies, and by including
auth its dependencies (db, session, flash) are also included automatically.
@action("index")
@action.uses("generic.html", auth)
def index():
user = auth.get_user()
if user:
message = f"Hello {user['first_name']}"
else:
message = "Hello, you are not logged in"
return {"message": message}
auth.user is a different fixture which requires a logged-in user and blocks access otherwise
@action("index")
@action.uses("generic.html", auth.user)
def index():
user = auth.get_user()
message = f"Hello {user['first_name']}"
return {"message": message}
More complex policies are possible using the built-in tagging
system combined with auth.
Condition is another fixture, if False it raises a 404 error page by default.
is_manager = Condition(lambda: "manager" in groups.get(auth.user_id))
@action("index")
@action.uses("generic.html", auth.user, is_manager)
def index():
user = auth.get_user()
message = f"Hello {user['first_name']} (manager!)"
return {"message": message}
Py4web has a built-in Database Abstraction Layer (support for sqlite, postgres, mysql, oracle, and more).
It is integrated with auth and with form generation logic. It follows a declarative pattern and
provides automatic migrations to create/alter tables. For example the following code creates a "thing" table with a "name" field and and an "image" and an additional standard signature fields ("created_by", "created_on", "modified_by", "modified_on"). Field types are more complex than basic database types as they have logic for validation and for handling content (such as uploading and downloading images).
db.define_table(
"thing",
Field("name", requires=IS_NOT_EMPTY()),
Field("image", "upload", download_url = lambda fn: URL(f"download/{fn}")),
auth.signature)
Given the object db.thing defined above py4web can automatically generate forms including validation.
Here is a create form
@action("create_thing")
@action.uses("generic.html", auth.user)
def create_thing():
form = Form(db.thing)
if form.accepted:
# record created
redirect(URL("index"))
return locals()
Here is an edit form
@action("edit_thing/<thing_id:int>")
@action.uses("generic.html", auth.user)
def edit_thing(thing_id):
form = Form(db.thing, thing_id)
if form.accepted:
# record updated
redirect(URL("index"))
return locals()
py4web can also generate a grid from a database query.
The grid shows selected records with pagination and, optionally, enables creating, editing, deleting records, with multiple options for customization
@action("my_things")
@action("my_things/<path:path>")
@action.uses("generic.html", auth.user)
def my_things(path=None):
form = Grid(path,
db.thing.created_by==auth.user_id,
editable=True, create=True, deletable=True)
return locals()
The DAL also makes it very easy to create APIs. Here is a GET API example
@action("api/things", method="GET")
@action.uses(db)
def api_GET_things():
return {"things": db(db.thing).select().as_list()}
POST API example
@action("api/things", method="POST")
@action.uses(db)
def api_POST_things():
return db.thing.validate_and_insert(**request.json)
PUT API example
@action("api/things/<thing_id:int>", method="PUT")
@action.uses(db)
def api_PUT_things(thing_id):
return db.thing.validate_and_update(thing_id, **request.json)
DELETE API example
@action("api/things/<thing_id:int>", method="DELETE")
@action.uses(db)
def api_DELETE_things(thing_id):
return {"deleted": db(db.thing.id==thing_id).delete()}
These are just the basics. There is a lot more to it, including...
- A built-in Web UI.
- A built-in ticketing system to track and retrieve bugs in production.
- It provides full record versioning (remembers all changes to every record).
- internationalization and pluralization with a translation UI.
- Automatic generation of RESTful APIs for your database.
- Built-in logic for user impersonation.
- Integrated SSO support for Google, Github, Facebook, Okta, LDAP, PAM
- A built-in scheduler for submitting and managing async background tasks.