Fork me on GitHub

PY4WEB

Different, yet cute, and a memorable evolutionary step.

Dashboard Documentation Examples Tutorials Source Discuss


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...

LICENSE

3-clause BSD

USEFUL LINKS

A minimalist Facebook clone
A minimalist Twitter clone (with Vue)