PECAN(1)
π NAME
okay so bestie, welcome to the pecan manual page β yes, that's PECAN(1) in the man-page world, and no, it's not about the nut (though the vibe is β¨nutritiousβ¨). This is the documentation for Pecan, a lean Python web framework inspired by CherryPy, TurboGears, and Pylons. Pecan was originally created by the developers of ShootQ (http://shootq.com) while working at Pictage (http://pictage.com). iykyk π
Pecan was created to fill a void in the Python web-framework world β a very lightweight framework that provides object-dispatch style routing. fr fr, that's the whole point. Pecan does not aim to be a "full stack" framework, and therefore includes no out of the box support for things like sessions or databases (although tutorials are included for integrating these yourself in just a few lines of code). So don't come for her for not having a database ORM built in β she's not that kind of girl. Pecan instead focuses on HTTP itself. main character energy, honestly. π
Although it is lightweight, Pecan does offer an extensive feature set for building HTTP-based applications, including:
- Object-dispatch for easy routing
- Full support for REST-style controllers
- Extensible security framework
- Extensible template language support
- Extensible JSON support
- Easy Python-based configuration
like?? the range?? π
β¨ INSTALLATION β¨
π§ Stable Version
We recommend installing Pecan with pip (http://www.pip-installer.org/), but you can also try with easy_install. Creating a spot in your environment where Pecan can be isolated from other packages is best practice. no cap, do this, your future self will thank you π«Ά
To get started with an environment for Pecan, we recommend creating a new virtual environment (http://www.virtualenv.org) using virtualenv (http://www.virtualenv.org):
$ virtualenv pecan-env
$ cd pecan-env
$ source bin/activate
The above commands create a virtual environment and activate it. This will isolate Pecan's dependency installations from your system packages, making it easier to debug problems if needed. literally a glow-up for your dependency tree π
Next, let's install Pecan:
$ pip install pecan
slay, that's it, you're in. π¦
π Development (Unstable) Version
If you want to run the latest development version of Pecan you will need to install git and clone the repo from GitHub:
$ git clone https://github.com/pecan/pecan.git
Assuming your virtual environment is still activated, call setup.py to install the development version.:
$ cd pecan
$ python setup.py develop
Note: The master development branch is volatile and is generally not recommended for production use. π like she's gorgeous but she is unhinged, do not deploy her to prod, i'm begging.
Alternatively, you can also install from GitHub directly with pip.:
$ pip install -e git://github.com/pecan/pecan.git#egg=pecan
peak convenience honestly π₯
πΈ CREATING YOUR FIRST PECAN APPLICATION πΈ
Okay bestie, letβs make a small sample project with Pecan. Itβs giving βfirst day of school but make it WSGI.β π
Note: This guide does not cover the installation of Pecan. If you need instructions for installing Pecan, refer to Installation <# installation>.
π Base Application Template π
Pecan includes a basic template for starting a new project. From your shell, type:
$ pecan create test_project
This example uses test_project as your project name, but you can replace it with any valid Python package name you like. Slay, customizable.
Go ahead and change into your newly created project directory:
$ cd test_project
Youβll want to deploy it in βdevelopment modeβ, such that itβs available on sys.path, yet can still be edited directly from its source distribution:
$ python setup.py develop
Your new project contains these files:
$ ls
βββ MANIFEST.in
βββ config.py
βββ public
β βββ css
β β βββ style.css
β βββ images
βββ setup.cfg
βββ setup.py
βββ test_project
βββ __init__.py
βββ app.py
βββ controllers
β βββ __init__.py
β βββ root.py
βββ model
β βββ __init__.py
βββ templates
β βββ error.html
β βββ index.html
β βββ layout.html
βββ tests
βββ __init__.py
βββ config.py
βββ test_functional.py
βββ test_units.py
The number of files and directories may vary based on the version of Pecan, but the above structure should give you an idea of what to expect. No cap, itβs a whole little ecosystem.
Letβs review the files created by the template.
publicβ All your static files (like CSS, Javascript, and images) live here. Pecan comes with a simple file server that serves these static files as you develop. Iconic.
Pecan app structure generally follows the MVC pattern (http://en.wikipedia.org/wiki/Modelβviewβcontroller). The directories under test_project encompass your models, controllers and templates.
test_project/controllersβ The container directory for your controller files.test_project/templatesβ All your templates go in here.test_project/modelβ Container for your model files.
Finally, a directory to house unit and integration tests:
test_project/testsβ All of the tests for your application.
The test_project/app.py file controls how the Pecan application will be created. This file must contain a setup_app() function which returns the WSGI application object. Generally you will not need to modify the app.py file provided by the base application template unless you need to customize your app in a way that cannot be accomplished using config. See Python-Based Configuration below. π«Ά
To avoid unneeded dependencies and to remain as flexible as possible, Pecan doesnβt impose any database or ORM (Object Relational Mapper http://en.wikipedia.org/wiki/Object-relational_mapping). If your project will interact with a database, you can add code to model/__init__.py to load database bindings from your configuration file and define tables and ORM definitions. Very βbring your own database,β tbh.
π Running the Application π
The base project template creates the configuration file with the basic settings you need to run your Pecan application in config.py. This file includes the host and port to run the server on, the location where your controllers and templates are stored on disk, and the name of the directory containing any static files.
If you just run pecan serve, passing config.py as the configuration file, it will bring up the development server and serve the app:
$ pecan serve config.py
Starting server in PID 000.
serving on 0.0.0.0:8080, view at http://127.0.0.1:8080
The location for the configuration file and the argument itself are very flexible β you can pass an absolute or relative path to the file. Highkey convenient. β¨
π Python-Based Configuration π
For ease of use, Pecan configuration files are pure Python β theyβre even saved as .py files. Not me being obsessed with Python dictionaries at 2 a.m.
This is how your default (generated) configuration file should look:
# Server Specific Configurations
server = {
'port': '8080',
'host': '0.0.0.0'
}
# Pecan Application Configurations
app = {
'root': '${package}.controllers.root.RootController',
'modules': ['${package}'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/${package}/templates',
'debug': True,
'errors': {
'404': '/error/404',
'__force_dict__': True
}
}
logging = {
'loggers': {
'root' : {'level': 'INFO', 'handlers': ['console']},
'${package}': {'level': 'DEBUG', 'handlers': ['console']}
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
}
}
}
# Custom Configurations must be in Python dictionary format::
#
# foo = {'bar':'baz'}
#
# All configurations are accessible at::
# pecan.conf
You can also add your own configuration as Python dictionaries. Literally your config, your rules. π
Thereβs a lot to cover here, so weβll come back to configuration files in a later chapter (Configuring Pecan Applications <#configuration>).
π± The Application Root π±
The Root Controller is the entry point for your application. You can think of it as being analogous to your applicationβs root URL path (in our case, http://localhost:8080/).
This is how it looks in the project template (test_project.controllers.root.RootController):
from pecan import expose
from webob.exc import status_map
class RootController(object):
@expose(generic=True, template='index.html')
def index(self):
return dict()
@index.when(method='POST')
def index_post(self, q):
redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q)
@expose('error.html')
def error(self, status):
try:
status = int(status)
except ValueError:
status = 0
message = getattr(status_map.get(status), 'explanation', '')
return dict(status=status, message=message)
You can specify additional classes and methods if you need to do so, but for now, letβs examine the sample project, controller by controller:
@expose(generic=True, template='index.html')
def index(self):
return dict()
The index() method is marked as publicly available via the expose() decorator (<# pecan.decorators.expose>) (which in turn uses the index.html template) at the root of the application (http://127.0.0.1:8080/), so any HTTP GET that hits the root of your application (/) will be routed to this method.
Notice that the index() method returns a Python dictionary. This dictionary is used as a namespace to render the specified template (index.html) into HTML, and is the primary mechanism by which data is passed from controller to template. Main character data flow, honestly. π
@index.when(method='POST')
def index_post(self, q):
redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q)
The index_post() method receives one HTTP POST argument (q). Because the argument method to @index.when() has been set to 'POST', any HTTP POST to the application root (in the example project, a form submission) will be routed to this method.
Finally, we have the error() method, which allows the application to display custom pages for certain HTTP errors (404, etcβ¦). Cute custom error pages? We love to see it. π
@expose('error.html')
def error(self, status):
try:
status = int(status)
except ValueError:
status = 0
message = getattr(status_map.get(status), 'explanation', '')
return dict(status=status, message=message)
π§ͺ Running the Tests For Your Application π§ͺ
Your application comes with a few example tests that you can run, replace, and add to. To run them:
$ pip install tox && tox -e py
The tests themselves can be found in the tests module in your project. Go off, test suite. π¦
βοΈ Deploying to a Web Server βοΈ
Ready to deploy your new Pecan app? Take a look at Deploying Pecan in Production <#deployment>.
π CONTROLLERS AND ROUTING π
okay so bestie, routing is literally the main character of your Pecan app. Pecan uses a routing strategy known as object-dispatch to map an HTTP request to a controller, and then the method to call. Object-dispatch begins by splitting the path into a list of components and then walking an object path, starting at the root controller. You can imagine your application's controllers as a tree of objects (branches of the object tree map directly to URL paths). It's giving filesystem but make it web. π³β¨
Let's look at a simple bookstore application:
from pecan import expose
class BooksController(object):
@expose()
def index(self):
return "Welcome to book section."
@expose()
def bestsellers(self):
return "We have 5 books in the top 10."
class CatalogController(object):
@expose()
def index(self):
return "Welcome to the catalog."
books = BooksController()
class RootController(object):
@expose()
def index(self):
return "Welcome to store.example.com!"
@expose()
def hours(self):
return "Open 24/7 on the web."
catalog = CatalogController()
A request for /catalog/books/bestsellers from the online store would begin with Pecan breaking the request up into catalog, books, and bestsellers. Next, Pecan would lookup catalog on the root controller. Using the catalog object, Pecan would then lookup books, followed by bestsellers. What if the URL ends in a slash? Pecan will check for an index method on the last controller object. iconic behavior tbh. π«Ά
To illustrate further, the following paths:
βββ /
βββ /hours
βββ /catalog
βββ /catalog/books
βββ /catalog/books/bestsellers
route to the following controller methods:
βββ RootController.index
βββ RootController.hours
βββ CatalogController.index
βββ BooksController.index
βββ BooksController.bestsellers
β¨ Exposing Controllers β¨
You tell Pecan which methods in a class are publically-visible via expose() (pecan.decorators.expose). If a method is not decorated with expose(), Pecan will never route a request to it. no cap, it's invisible to the router. π
expose() can be used in a variety of ways. The simplest case involves passing no arguments. In this scenario, the controller returns a string representing the HTML response body.
from pecan import expose
class RootController(object):
@expose()
def hello(self):
return 'Hello World'
A more common use case is to specify a template and a namespace (templates):
from pecan import expose
class RootController(object):
@expose('html_template.mako')
def hello(self):
return {'msg': 'Hello!'}
<!-- html_template.mako -->
<html>
<body>${msg}</body>
</html>
Pecan also has built-in support for a special JSON renderer (#expose-json), which translates template namespaces into rendered JSON text:
from pecan import expose
class RootController(object):
@expose('json')
def hello(self):
return {'msg': 'Hello!'}
expose() calls can also be stacked, which allows you to serialize content differently depending on how the content is requested:
from pecan import expose
class RootController(object):
@expose('json')
@expose('text_template.mako', content_type='text/plain')
@expose('html_template.mako')
def hello(self):
return {'msg': 'Hello!'}
You'll notice that we called expose() three times, with different arguments. let's break down the stack, bestie:
@expose('json')β the first tells Pecan to serialize the response namespace using JSON serialization when the client requests/hello.jsonor if anAccept: application/jsonheader is present.@expose('text_template.mako', content_type='text/plain')β the second tells Pecan to use thetext_template.makotemplate file when the client requests/hello.txtor asks fortext/plainvia anAcceptheader.@expose('html_template.mako')β the third tells Pecan to use thehtml_template.makotemplate file when the client requests/hello.html. If the client requests/hello, Pecan will use thetext/htmlcontent type by default; in the absense of an explicit content type, Pecan assumes the client wants HTML.
See also:
pecan.decoratorsβ Pecan Decorators (<#pecan-decorators>)
πΈ Specifying Explicit Path Segments πΈ
Occasionally, you may want to use a path segment in your routing that doesn't work with Pecan's declarative approach to routing because of restrictions in Python's syntax. For example, if you wanted to route for a path that includes dashes, such as /some-path/, the following is not valid Python:
class RootController(object):
@pecan.expose()
def some-path(self):
return dict()
To work around this, pecan allows you to specify an explicit path segment in the expose() decorator:
class RootController(object):
@pecan.expose(route='some-path')
def some_path(self):
return dict()
In this example, the pecan application will reply with an HTTP 200 for requests made to /some-path/, but requests made to /some_path/ will yield an HTTP 404. rude but fair. π
route() (pecan.routing.route) can also be used explicitly as an alternative to the route argument in expose():
class RootController(object):
@pecan.expose()
def some_path(self):
return dict()
pecan.route('some-path', RootController.some_path)
Routing to child controllers can be handled similarly by utilizing route():
class ChildController(object):
@pecan.expose()
def child(self):
return dict()
class RootController(object):
pass
pecan.route(RootController, 'child-path', ChildController())
In this example, the pecan application will reply with an HTTP 200 for requests made to /child-path/child/. slay. π
π₯ Routing Based on Request Method π₯
The generic argument to expose() provides support for overloading URLs based on the request method. In the following example, the same URL can be serviced by two different methods (one for handling HTTP GET, another for HTTP POST) using generic controllers:
from pecan import expose
class RootController(object):
# HTTP GET /
@expose(generic=True, template='json')
def index(self):
return dict()
# HTTP POST /
@index.when(method='POST', template='json')
def index_POST(self, **kw):
uuid = create_something()
return dict(uuid=uuid)
π¦ Pecan's Routing Algorithm π¦
Sometimes, the standard object-dispatch routing isn't adequate to properly route a URL to a controller. Pecan provides several ways to short-circuit the object-dispatch system to process URLs with more control, including the special _lookup(), _default(), and _route() methods. Defining these methods on your controller objects provides additional flexibility for processing all or part of a URL. it's giving escape hatch, and honestly? obsessed. β¨
π Routing to Subcontrollers with _lookup π
The _lookup() special method provides a way to process a portion of a URL, and then return a new controller object to route to for the remainder.
A _lookup() method may accept one or more arguments, segments of the URL path to be processed (split on /). _lookup() should also take variable positional arguments representing the rest of the path, and it should include any portion of the path it does not process in its return value. The example below uses a *remainder list which will be passed to the returned controller when the object-dispatch algorithm continues.
In addition to being used for creating controllers dynamically, _lookup() is called as a last resort, when no other controller method matches the URL and there is no _default() method.
from pecan import expose, abort
from somelib import get_student_by_name
class StudentController(object):
def __init__(self, student):
self.student = student
@expose()
def name(self):
return self.student.name
class RootController(object):
@expose()
def _lookup(self, primary_key, *remainder):
student = get_student_by_primary_key(primary_key)
if student:
return StudentController(student), remainder
else:
abort(404)
An HTTP GET request to /8/name would return the name of the student where primary_key == 8. iykyk. π
π Falling Back with _default π
The _default() method is called as a last resort when no other controller methods match the URL via standard object-dispatch.
from pecan import expose
class RootController(object):
@expose()
def english(self):
return 'hello'
@expose()
def french(self):
return 'bonjour'
@expose()
def _default(self):
return 'I cannot say hello in that language'
In the example above, a request to /spanish would route to RootController._default(). it's the catch-all bestie of the routing world. π«Ά
π Defining Customized Routing with _route π
The _route() method allows a controller to completely override the routing mechanism of Pecan. Pecan itself uses the _route() method to implement its RestController (pecan.rest.RestController). If you want to design an alternative routing system on top of Pecan, defining a base controller class that defines a _route() method will enable you to have total control. main character energy, no notes. π
π INTERACTING WITH THE REQUEST AND RESPONSE OBJECT π
Okay so bestie, for every HTTP request, Pecan keeps a thread-local reference (contextlocals) to the request and response object: pecan.request and pecan.response. These are instances of pecan.Request and pecan.Response, respectively, and you can interact with them from inside Pecan controller code:
@pecan.expose()
def login(self):
assert pecan.request.path == '/login'
username = pecan.request.POST.get('username')
password = pecan.request.POST.get('password')
pecan.response.status = 403
pecan.response.text = 'Bad Login!'
Pecan abstracts away a lot of the need to touch these objects directly, but sometimes you <highkey>do</highkey> want in, like when you're:
- Inspecting components of the URI
- Determining aspects of the request, such as the user's IP address, or the referer header
- Setting specific response headers
- Manually rendering a response body
β¨ Specifying a Custom Response β¨
Set a specific HTTP response code (such as 203 Non-Authoritative Information) by modifying the status attribute of the response object. It's giving main character response energy:
from pecan import expose, response
class RootController(object):
@expose('json')
def hello(self):
response.status = 203
return {'foo': 'bar'}
Use the utility function abort() (pecan.core.abort) to raise HTTP errors. No cap, it's iconic for bailing out cleanly:
from pecan import expose, abort
class RootController(object):
@expose('json')
def hello(self):
abort(404)
abort() (pecan.core.abort) raises an instance of WSGIHTTPException which is used by Pecan to render default response bodies for HTTP errors. This exception is stored in the WSGI request environ at pecan.original_exception, where it can be accessed later in the request cycle (by, for example, other middleware or Custom Error Documents (errors)).
If you'd like to return an explicit response, you can do so using Response (pecan.core.Response):
from pecan import expose, Response
class RootController(object):
@expose()
def hello(self):
return Response('Hello, World!', 202)
π¦ Extending Pecan's Request and Response Object π¦
The request and response implementations provided by WebOb are powerful, but sometimes it's useful to extend application-specific behavior onto your request and response (such as specialized parsing of request headers or customized response body serialization). To do so, define custom classes that inherit from pecan.Request and pecan.Response, respectively:
class MyRequest(pecan.Request):
pass
class MyResponse(pecan.Response):
pass
and modify your application configuration to use them:
from myproject import MyRequest, MyResponse
app = {
'root' : 'project.controllers.root.RootController',
'modules' : ['project'],
'static_root' : '%(confdir)s/public',
'template_path' : '%(confdir)s/project/templates',
'request_cls': MyRequest,
'response_cls': MyResponse
}
πΈ Mapping Controller Arguments πΈ
In Pecan, HTTP GET and POST variables that are not consumed during the routing process can be passed onto the controller method as arguments. Vibe check: yes, your controller can just catch them.
Depending on the signature of the method, these arguments can be mapped explicitly to arguments:
from pecan import expose
class RootController(object):
@expose()
def index(self, arg):
return arg
@expose()
def kwargs(self, **kwargs):
return str(kwargs)
$ curl http://localhost:8080/?arg=foo
foo
$ curl http://localhost:8080/kwargs?a=1&b=2&c=3
{u'a': u'1', u'c': u'3', u'b': u'2'}
or can be consumed positionally:
from pecan import expose
class RootController(object):
@expose()
def args(self, *args):
return ','.join(args)
$ curl http://localhost:8080/args/one/two/three
one,two,three
The same effect can be achieved with HTTP POST body variables:
from pecan import expose
class RootController(object):
@expose()
def index(self, arg):
return arg
$ curl -X POST "http://localhost:8080/" -H "Content-Type: application/x-www-form-urlencoded" -d "arg=foo"
foo
π Static File Serving π
Because Pecan gives you direct access to the underlying Request, serving a static file download is as simple as setting the WSGI app_iter and specifying the content type. Obsessed with the simplicity:
import os
from random import choice
from webob.static import FileIter
from pecan import expose, response
class RootController(object):
@expose(content_type='image/gif')
def gifs(self):
filepath = choice((
"/path/to/funny/gifs/catdance.gif",
"/path/to/funny/gifs/babydance.gif",
"/path/to/funny/gifs/putindance.gif"
))
f = open(filepath, 'rb')
response.app_iter = FileIter(f)
response.headers[
'Content-Disposition'
] = 'attachment; filename="%s"' % os.path.basename(f.name)
If you don't know the content type ahead of time (for example, if you're retrieving files and their content types from a data store), you can specify it via response.headers rather than in the expose() (pecan.decorators.expose) decorator:
import os
from mimetypes import guess_type
from webob.static import FileIter
from pecan import expose, response
class RootController(object):
@expose()
def download(self):
f = open('/path/to/some/file', 'rb')
response.app_iter = FileIter(f)
response.headers['Content-Type'] = guess_type(f.name)
response.headers[
'Content-Disposition'
] = 'attachment; filename="%s"' % os.path.basename(f.name)
π Handling File Uploads π
Pecan makes it easy to handle file uploads via standard multipart forms. Simply define your form with a file input:
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="file" />
<button type="submit">Upload</button>
</form>
You can then read the uploaded file off of the request object in your application's controller:
from pecan import expose, request
class RootController(object):
@expose()
def upload(self):
assert isinstance(request.POST['file'], cgi.FieldStorage)
data = request.POST['file'].file.read()
π§΅π Thread-Safe Per-Request Storage ππ§΅
For convenience, Pecan provides a Python dictionary on every request which can be accessed and modified in a thread-safe manner throughout the life-cycle of an individual request. Literally so handy:
pecan.request.context['current_user'] = some_user
print pecan.request.context.items()
This is particularly useful in situations where you want to store metadata/context about a request (e.g., in middleware, or per-routing hooks) and access it later (e.g., in controller code).
For more fine-grained control of the request, the underlying WSGI environ for a given Pecan request can be accessed and modified via pecan.request.environ. iykyk.
πβ¨ Helper Functions β¨π
Pecan also provides several useful helper functions for moving between different routes. The redirect() (pecan.core.redirect) function allows you to issue internal or HTTP 302 redirects.
See also: Theredirect()utility, along with several other useful helpers, are documented inpecan.core-- Pecan Core (pecan-core).
πΊοΈπ Determining the URL for a Controller π πΊοΈ
Given the ability for routing to be drastically changed at runtime, it is not always possible to correctly determine a mapping between a controller method and a URL. It's giving chaos, but make it routing.
For example, in the following code that makes use of _lookup() to alter the routing depending on a condition:
from pecan import expose, abort
from somelib import get_user_region
class DefaultRegionController(object):
@expose()
def name(self):
return "Default Region"
class USRegionController(object):
@expose()
def name(self):
return "US Region"
class RootController(object):
@expose()
def _lookup(self, user_id, *remainder):
if get_user_region(user_id) == 'us':
return USRegionController(), remainder
else:
return DefaultRegionController(), remainder
This logic depends on the geolocation of a given user and returning a completely different class given the condition. A helper to determine what URL USRegionController.name belongs to would fail to do it correctly. π
TEMPLATING IN PECAN π¨β¨
Okay so here's the tea: Pecan is literally besties with a whole bunch of templating engines, and it also lets you plug in your own new ones with barely any effort. Currently the lineup is:
- Mako (
http://www.makotemplates.org/) β renderer namemako - Genshi (
http://genshi.edgewall.org/) β renderer namegenshi - Kajiki (
http://kajiki.pythonisito.com/) β renderer namekajiki - Jinja2 (
http://jinja.pocoo.org/) β renderer namejinja - JSON β renderer name
json
The default template system is mako, but you can absolutely switch that up by passing the default_renderer key in your application's configuration:
app = {
'default_renderer' : 'kajiki',
# ...
}
Using Template Renderers π
pecan.decorators defines a decorator called expose(), and it's the icon that flags a method as a public controller. The expose() decorator takes a template argument, which you use to specify the path to the template file for the controller method you're exposing:
class MyController(object):
@expose('path/to/mako/template.html')
def index(self):
return dict(message='I am a mako template')
So expose() will use the default template engine β unless the path is prefixed by another renderer name. That's your cue to pick a different one, iykyk:
@expose('kajiki:path/to/kajiki/template.html')
def my_controller(self):
return dict(message='I am a kajiki template')
See also:
pecan.decoratorsβ Pecan Decoratorspecan.coreβ Pecan Core- Controllers and Routing
Overriding Templates π¦
override_template() lets you override the template set for a controller method when it's exposed. And the way it works is so clean: when override_template() is called within the body of the controller method, it changes the template that will be used for that invocation of the method. Just that one. Main character energy, no leftovers.
class MyController(object):
@expose('template_one.html')
def index(self):
# ...
override_template('template_two.html')
return dict(message='I will now render with template_two.html')
Manual Rendering π
Sometimes you just wanna do it yourself, and render() has your back β it lets you manually render output using the Pecan templating framework. Pass the template path and the values to go into the template, and render() hands you back the rendered output as text. Slay.
@expose()
def controller(self):
return render('my_template.html', dict(message='I am the namespace'))
The JSON Renderer π
Pecan also provides a JSON renderer, which you can use by exposing a controller method with @expose('json'). That's it. That's the whole vibe.
See also:
- JSON Serialization
pecan.jsonifyβ Pecan JSON Support
Defining Custom Renderers π
Wanna build your own renderer? Totally doable β just create a class that follows the renderer protocol:
class MyRenderer(object):
def __init__(self, path, extra_vars):
'''
Your renderer is provided with a path to templates,
as configured by your application, and any extra
template variables, also as configured
'''
pass
def render(self, template_path, namespace):
'''
Lookup the template based on the path, and render
your output based upon the supplied namespace
dictionary, as returned from the controller.
'''
return str(namespace)
To enable your custom renderer, define a custom_renderers key in your application's configuration:
app = {
'custom_renderers' : {
'my_renderer' : MyRenderer
},
# ...
}
...and then specify the renderer in the expose() method, like so:
class RootController(object):
@expose('my_renderer:template.html')
def index(self):
return dict(name='Bob')
WRITING RESTFUL WEB SERVICES WITH GENERIC CONTROLLERS π₯π
Pecan simplifies RESTful web services by providing a way to overload URLs based on the request method. For most API's, the use of generic controller definitions give you everything you need to build out robust RESTful interfaces β and ngl, it's the recommended approach to writing RESTful web services in pecan:
from pecan import abort, expose
# Note: this is *not* thread-safe. In real life, use a persistent data store.
BOOKS = {
'0': 'The Last of the Mohicans',
'1': 'Catch-22'
}
class BookController(object):
def __init__(self, id_):
self.id_ = id_
assert self.book
@property
def book(self):
if self.id_ in BOOKS:
return dict(id=self.id_, name=BOOKS[self.id_])
abort(404)
# HTTP GET /<id>/
@expose(generic=True, template='json')
def index(self):
return self.book
# HTTP PUT /<id>/
@index.when(method='PUT', template='json')
def index_PUT(self, **kw):
BOOKS[self.id_] = kw['name']
return self.book
# HTTP DELETE /<id>/
@index.when(method='DELETE', template='json')
def index_DELETE(self):
del BOOKS[self.id_]
return dict()
class RootController(object):
@expose()
def _lookup(self, id_, *remainder):
return BookController(id_), remainder
# HTTP GET /
@expose(generic=True, template='json')
def index(self):
return [dict(id=k, name=v) for k, v in BOOKS.items()]
# HTTP POST /
@index.when(method='POST', template='json')
def index_POST(self, **kw):
id_ = str(len(BOOKS))
BOOKS[id_] = kw['name']
return dict(id=id_, name=kw['name'])
Fr fr though: that BOOKS dict is not thread-safe, and the comment in the code says so loud and clear. In real life, use a persistent data store. π
π WRITING RESTFUL WEB SERVICES WITH RESTCONTROLLER π
okay so for compatability with the TurboGears2 library (http://turbogears.org) β yes, that spelling is in the original, weβre keeping it β Pecan also gives you a class-based solution to RESTful routing, RestController:
from pecan import expose
from pecan.rest import RestController
from mymodel import Book
class BooksController(RestController):
@expose()
def get(self, id):
book = Book.get(id)
if not book:
abort(404)
return book.title
πΊοΈ URL Mapping πΊοΈ
By default, RestController routes like this, bestie β method, description, example method(s)/URL(s):
get_oneβ Display one record. Example method(s)/URL(s):GET /books/1get_allβ Display all records in a resource. Example method(s)/URL(s):GET /books/getβ A combo ofget_oneandget_all. Example method(s)/URL(s):GET /books/GET /books/1β standalone row in the original mapping table, with no description and no example method(s)/URL(s) listed.newβ Display a page to create a new resource. Example method(s)/URL(s):GET /books/neweditβ Display a page to edit an existing resource. Example method(s)/URL(s):GET /books/1/editpostβ Create a new record. Example method(s)/URL(s):POST /books/putβ Update an existing record. Example method(s)/URL(s):POST /books/1?_method=putPUT /books/1β standalone row in the original mapping table, with no description and no example method(s)/URL(s) listed.get_deleteβ Display a delete confirmation page. Example method(s)/URL(s):GET /books/1/deletedeleteβ Delete an existing record. Example method(s)/URL(s):POST /books/1?_method=deleteDELETE /books/1β standalone row in the original mapping table, with no description and no example method(s)/URL(s) listed.
Pecanβs RestController uses the ?_method= query string to work around the lack of support for the PUT and DELETE verbs when submitting forms in most current browsers.
In addition to handling REST, the RestController also supports the index(), _default(), and _lookup() routing overrides.
β οΈ Warning: If you need to override_route(), make sure to callRestController._route()at the end of your custom method so that the REST routing described above still occurs.
πͺ Nesting RestController πͺ
RestController instances can be nested so that child resources receive the parameters necessary to look up parent resources.
For example:
from pecan import expose
from pecan.rest import RestController
from mymodel import Author, Book
class BooksController(RestController):
@expose()
def get(self, author_id, id):
author = Author.get(author_id)
if not author_id:
abort(404)
book = author.get_book(id)
if not book:
abort(404)
return book.title
class AuthorsController(RestController):
books = BooksController()
@expose()
def get(self, id):
author = Author.get(id)
if not author:
abort(404)
return author.name
class RootController(object):
authors = AuthorsController()
Accessing /authors/1/books/2 invokes BooksController.get() with author_id set to 1 and id set to 2.
To determine which arguments are associated with the parent resource, Pecan looks at the get_one() then get() method signatures, in that order, in the parent controller. If the parent resource takes a variable number of arguments, Pecan will pass it everything up to the child resource controller name (e.g., books in the above example).
β¨ Defining Custom Actions β¨
In addition to the default methods defined above, you can add additional behaviors to a RestController by defining a special _custom_actions dictionary.
For example:
from pecan import expose
from pecan.rest import RestController
from mymodel import Book
class BooksController(RestController):
_custom_actions = {
'checkout': ['POST']
}
@expose()
def checkout(self, id):
book = Book.get(id)
if not book:
abort(404)
book.checkout()
_custom_actions maps method names to the list of valid HTTP verbs for those custom actions. In this case checkout() supports POST.
πΈ CONFIGURING PECAN APPLICATIONS πΈ
okay bestie, configuring Pecan is genuinely so easy it's almost suspicious π Like, as long as you follow a few lil conventions, using, setting and dealing with configuration is gonna feel super intuitive. No cap.
The big vibe: Pecan configuration files are pure Python. Every "section" of the configuration is just a dictionary assigned to a variable name in the configuration module. That's it. That's the whole thing. πβ¨
π Default Values π
here's the complete list of default values the framework uses, so you know what you're starting with (it's giving "factory settings"):
server = {
'port' : '8080',
'host' : '0.0.0.0'
}
app = {
'root' : None,
'modules' : [],
'static_root' : 'public',
'template_path' : ''
}
π Application Configuration π
The app configuration values are what Pecan uses to wrap your application into a valid WSGI app <http://www.wsgi.org/en/latest/what.html> (iykyk). The app configuration is specific to your application, and it includes stuff like where your root controller class lives.
A typical application configuration is gonna look something like this:
app = {
'root' : 'project.controllers.root.RootController',
'modules' : ['project'],
'static_root' : '%(confdir)s/public',
'template_path' : '%(confdir)s/project/templates',
'debug' : True
}
okay let's go through each value one by one, bestie, no skipping π
modulesβ a list of modules where pecan will search for applications. Generally this should contain a single item, the name of your project's python package. At least one of the listed modules must contain anapp.setup_appfunction, which is called to create the WSGI app. In other words, this package should be where yourapp.pyfile is located, and that file should contain asetup_appfunction. literally that's the requirement π«Άrootβ the root controller of your application. Remember to give a string representing a Python path to some callable (e.g.,"yourapp.controllers.root.RootController").static_rootβ the directory where your static files can be found (relative to the project root). Pecan comes with middleware that can be used to serve static files (like CSS and Javascript files) during development. cute, right? πtemplate_pathβ points to the directory where your template files live (relative to the project root).debugβ enables the ability to display tracebacks in the browser and interactively debug during development.
β οΈ Warning: app is a reserved variable name for that section of the configuration, so make sure you don't override it. please. for me. π
β οΈ Warning: Make suredebugis always set toFalsein production environments. this is not a drill, bestie. π
See also:
- Base Application Template <#app-template>
π Server Configuration π
Pecan provides some sane defaults π§ Change these to alter the host and port your WSGI app is served on.
server = {
'port' : '8080',
'host' : '0.0.0.0'
}
π¦ Additional Configuration π¦
Your application may need access to other configuration values at runtime (like third-party API credentials, ooh fancy). Put these settings in their own blocks in your configuration file. separate blocks, separate vibes β¨
twitter = {
'api_key' : 'FOO',
'api_secret' : 'SECRET'
}
β Accessing Configuration at Runtime β
You can access any configuration value at runtime via pecan.conf. This includes custom, application, and server-specific values. all of them. the whole menu π
For example, if you needed to specify a global administrator, you could do so like this within the configuration file:
administrator = 'foo_bar_user'
And it would be accessible in pecan.conf as:
>>> from pecan import conf
>>> conf.administrator
'foo_bar_user'
look at that. iconic. π
π Dictionary Conversion π
In certain situations you might want to deal with keys and values, but in strict dictionary form. The Config <#pecan.configuration.Config> object has a helper method for this exact purpose that will return a dictionary representation of the configuration, including nested values. nested. values. yes. π
Below is a representation of how you can access the to_dict() <#pecan.configuration.Config.to_dict> method and what it returns as a result (shortened for brevity):
>>> from pecan import conf
>>> conf
Config({'app': Config({'errors': {}, 'template_path': '', 'static_root': 'public', [...]
>>> conf.to_dict()
{'app': {'errors': {}, 'template_path': '', 'static_root': 'public', [...]
π§ Prefixing Dictionary Keys π§
to_dict() <#pecan.configuration.Config.to_dict> allows you to pass an optional string argument if you need to prefix the keys in the returned dictionary. optional! but so handy π
>>> from pecan import conf
>>> conf
Config({'app': Config({'errors': {}, 'template_path': '', 'static_root': 'public', [...]
>>> conf.to_dict('prefixed_')
{'prefixed_app': {'prefixed_errors': {}, 'prefixed_template_path': '', 'prefixed_static_root': 'prefixed_public', [...]
π₯ Dotted Keys, Non-Python Idenfitiers, and Native Dictionaries π₯
Sometimes you want to specify a configuration option that includes dotted keys or is not a valid Python idenfitier, such as (). (yes, that's a thing, stay with me π) These situations are especially common when configuring Python logging. By passing a special key, __force_dict__, individual configuration blocks can be treated as native dictionaries. lowkey magic β¨
logging = {
'root': {'level': 'INFO', 'handlers': ['console']},
'loggers': {
'sqlalchemy.engine': {'level': 'INFO', 'handlers': ['console']},
'__force_dict__': True
},
'formatters': {
'custom': {
'()': 'my.package.customFormatter'
}
}
}
from myapp import conf
assert isinstance(conf.logging.loggers, dict)
assert isinstance(conf.logging.loggers['sqlalchemy.engine'], dict)
see how the asserts pass? that's the __force_dict__ doing its thing π«Ά
π SECURITY AND AUTHENTICATION
Okay so real talk: Pecan ships with no out-of-the-box authentication. Zero. None. π But what it does give you is the actual toolkit to handle authentication and authorization however you want. It's giving "we trust your vision" energy.
π secure Decorator Basics
You can wrap entire controller subtrees β or just individual method calls β with access controls using the secure() decorator. Bestie, it's so clean.
To decorate a method, use one argument:
secure('<check_permissions_method_name>')
To secure a class, invoke with two arguments:
secure(object_instance, '<check_permissions_method_name>')
from pecan import expose
from pecan.secure import secure
class HighlyClassifiedController(object):
pass
class UnclassifiedController(object):
pass
class RootController(object):
@classmethod
def check_permissions(cls):
if user_is_admin():
return True
return False
@expose()
def index(self):
#
# This controller is unlocked to everyone,
# and will not run any security checks.
#
return dict()
@secure('check_permissions')
@expose()
def topsecret(self):
#
# This controller is top-secret, and should
# only be reachable by administrators.
#
return dict()
highly_classified = secure(HighlyClassifiedController(), 'check_permissions')
unclassified = UnclassifiedController()
π«Ά SecureController
Alternatively (iconic that there's options), you can pull off the exact same thing by subclassing Pecan's SecureController. Your implementation should extend the check_permissions() class method so it returns True when the user has permission to that controller branch, and False when they absolutely do not. π
from pecan import expose
from pecan.secure import SecureController, unlocked
class HighlyClassifiedController(object):
pass
class UnclassifiedController(object):
pass
class RootController(SecureController):
@classmethod
def check_permissions(cls):
if user_is_admin():
return True
return False
@expose()
@unlocked
def index(self):
#
# This controller is unlocked to everyone,
# and will not run any security checks.
#
return dict()
@expose()
def topsecret(self):
#
# This controller is top-secret, and should
# only be reachable by administrators.
#
return dict()
highly_classified = HighlyClassifiedController()
unclassified = unlocked(UnclassifiedController())
Also peep the unlocked() decorator up there π β you use it basically the same way to explicitly unlock a controller for public access with no security checks at all. Main character exemption fr.
π§ Writing Authentication/Authorization Methods
Your check_permissions() method is where you figure out user authentication and authorization. And ngl, the code you write in there can be anything β from the simplest session assertions (like "the existing user is authenticated as an administrator") all the way to connecting to an LDAP service. Your world, your rules. β¨
π More on secure
The secure() method has a few advanced uses that let you build genuinely robust security policies for your app. Let's go.
One: you can pass the name of a class method or an instance method of the controller, via a string, to use as the check_permissions() method. Instance methods are especially useful when you want to authorize access to attributes of a model instance. Consider this little virtual filesystem moment:
from pecan import expose
from pecan.secure import secure
from myapp.session import get_current_user
from myapp.model import FileObject
class FileController(object):
def __init__(self, name):
self.file_object = FileObject(name)
def read_access(self):
self.file_object.read_access(get_current_user())
def write_access(self):
self.file_object.write_access(get_current_user())
@secure('write_access')
@expose()
def upload_file(self):
pass
@secure('read_access')
@expose()
def download_file(self):
pass
class RootController(object):
@expose()
def _lookup(self, name, *remainder):
return FileController(name), remainder
Two: secure() also accepts a function argument. But bestie, heads up β οΈ β when you pass a function, make sure it's imported from another file, or defined in the same file before the class definition. Otherwise you'll likely get an error during module import. Not cute.
from pecan import expose
from pecan.secure import secure
from myapp.auth import user_authenitcated
class RootController(object):
@secure(user_authenticated)
@expose()
def index(self):
return 'Logged in'
Three: you can also use secure() to change how a SecureController behaves. Decorating a method or wrapping a subcontroller tells Pecan to use a different security function instead of the default controller method. Super useful when you want a different level or type of security somewhere.
from pecan import expose
from pecan.secure import SecureController, secure
from myapp.auth import user_authenticated, admin_user
class ApiController(object):
pass
class RootController(SecureController):
@classmethod
def check_permissions(cls):
return user_authenticated()
@classmethod
def check_api_permissions(cls):
return admin_user()
@expose()
def index(self):
return 'logged in user'
api = secure(ApiController(), 'check_api_permissions')
So in that example above, Pecan will only call admin_user() when a request comes in for /api/. Selective security, we love to see it. π₯
π¦ Multiple Secure Controllers
Secure controllers can be nested to give you increasing levels of security on subcontrollers. In the example below, when a request comes in for /admin/index/, Pecan first calls check_permissions() on the RootController, and then calls check_permissions() on the AdminController. Layered like a cute little security parfait. π
from pecan import expose
from pecan.secure import SecureController
from myapp.auth import user_logged_in, is_admin
class AdminController(SecureController):
@classmethod
def check_permissions(cls):
return is_admin()
@expose()
def index(self):
return 'admin dashboard'
class RootController(SecureController):
@classmethod
def check_permissions(cls):
return user_logged_in
@expose()
def index(self):
return 'user dashboard'
πͺ PECAN HOOKS
So even though using WSGI middleware with Pecan is easy, it can be hard β sometimes literally impossible π β to get at Pecan's internals from inside middleware. Pecan Hooks are the answer: a way to interact with the framework without writing a whole separate middleware situation.
Hooks let you run code at key points throughout the life cycle of your request:
on_route()β called before Pecan attempts to route a request to a controllerbefore()β called after routing, but before controller code runsafter()β called after controller code has been runon_error()β called when a request generates an exception
π Implementating a Pecan Hook
In the example below, a simple hook gathers some info about the request and prints it to stdout. Very demure, very observant.
Your hook implementation needs to import PecanHook so it can be used as a base class. From there, you'll want to override the on_route(), before(), after(), or on_error() methods to define your behavior.
from pecan.hooks import PecanHook
class SimpleHook(PecanHook):
def before(self, state):
print "\nabout to enter the controller..."
def after(self, state):
print "\nmethod: \t %s" % state.request.method
print "\nresponse: \t %s" % state.response.status
on_route(), before(), and after() each get passed a shared state object packed with useful info β think the request and response objects, plus which controller Pecan's routing selected. Look at all this:
class SimpleHook(PecanHook):
def on_route(self, state):
print "\nabout to map the URL to a Python method (controller)..."
assert state.controller is None # Routing hasn't occurred yet
assert isinstance(state.request, webob.Request)
assert isinstance(state.response, webob.Response)
assert isinstance(state.hooks, list) # A list of hooks to apply
def before(self, state):
print "\nabout to enter the controller..."
if state.request.path == '/':
#
# `state.controller` is a reference to the actual
# `@pecan.expose()`-ed controller that will be routed to
# and used to generate the response body
#
assert state.controller.__func__ is RootController.index.__func__
assert isinstance(state.arguments, inspect.Arguments)
print state.arguments.args
print state.arguments.varargs
print state.arguments.keywords
assert isinstance(state.request, webob.Request)
assert isinstance(state.response, webob.Response)
assert isinstance(state.hooks, list)
...and the exception just gets handed over as the second arg, no biggie. So: on_error() is passed a shared state object and the original exception. And here's the fun part β if an on_error() handler returns a Response object, that response gets sent straight to the end user, and no further on_error() hooks run at all. Like, the chain just stops there. Iconic behavior honestly:
class CustomErrorHook(PecanHook):
def on_error(self, state, exc):
if isinstance(exc, SomeExceptionType):
return webob.Response('Custom Error!', status=500)
Attaching Hooks
Okay so how do you actually attach these things? Two ways bestie, two ways π
First up, project-wide: just drop a list of hooks into your project's configuration file.
app = {
'root' : '...'
# ...
'hooks': lambda: [SimpleHook()]
}
Second way β and this is the lowkey cute one β you can apply hooks selectively to controllers and all their sub-controllers using the __hooks__ attribute on one or more controllers, and subclassing HookController.
from pecan import expose
from pecan.hooks import HookController
from my_hooks import SimpleHook
class SimpleController(HookController):
__hooks__ = [SimpleHook()]
@expose('json')
def index(self):
print "DO SOMETHING!"
return dict()
Now that SimpleHook is in the mix, let's see what happens when we run the app and browse it from our web browser π
pecan serve config.py
serving on 0.0.0.0:8080 view at http://127.0.0.1:8080
about to enter the controller...
DO SOMETHING!
method: GET
response: 200 OK
Look at that. The hook fires, the controller does its thing, everybody wins π
And plot twist β hooks can be inherited from a parent class or from mixins. Just make sure you subclass from HookController. Here's the whole vibe:
from pecan import expose
from pecan.hooks import PecanHook, HookController
class ParentHook(PecanHook):
priority = 1
def before(self, state):
print "\nabout to enter the parent controller..."
class CommonHook(PecanHook):
priority = 2
def before(self, state):
print "\njust a common hook..."
class SubHook(PecanHook):
def before(self, state):
print "\nabout to enter the subcontroller..."
class SubMixin(object):
__hooks__ = [SubHook()]
# We'll use the same instance for both controllers,
# to avoid double calls
common = CommonHook()
class SubController(HookController, SubMixin):
__hooks__ = [common]
@expose('json')
def index(self):
print "\nI AM THE SUB!"
return dict()
class RootController(HookController):
__hooks__ = [common, ParentHook()]
@expose('json')
def index(self):
print "\nI AM THE ROOT!"
return dict()
sub = SubController()
So let's run it and watch the magic. First, loading the root controller:
pecan serve config.py
serving on 0.0.0.0:8080 view at http://127.0.0.1:8080
GET / HTTP/1.1" 200
about to enter the parent controller...
just a common hook
I AM THE ROOT!
Then loading the sub controller:
pecan serve config.py
serving on 0.0.0.0:8080 view at http://127.0.0.1:8080
GET /sub HTTP/1.1" 200
about to enter the parent controller...
just a common hook
about to enter the subcontroller...
I AM THE SUB!
Note: Make sure to set proper priority values for nested hooks in order to get them executed in the desired order. β
Warning: Two hooks of the same type will be added/executed twice, if passed as different instances to a parent and a child controller. If passed as one instance variable β will be invoked once for both controllers. So like, don't be shocked when your hook fires double, that's just how it works π
Hooks That Come with Pecan
Good news: Pecan ships some hooks right in its core, no extra install needed π This section is gonna walk you through what each one does, how to configure them, and some common real-world scenarios. Strap in.
RequestViewerHook
This hook is literally your debugging bestie. It has access to every attribute the response object has, plus a few extra ones that are specific to the framework. Very main character energy.
There are two main ways this hook can feed you info about a request:
- Terminal or logging output (via an file-like stream like stdout)
- Custom header keys in the actual response.
By default, both outputs are enabled. Double the info, double the slay β¨
See also:
pecan.hooksβ Pecan Hooks
Configuring RequestViewerHook
There are a few ways to get this hook properly configured and running. But here's the thing β it's useful to know that no actual configuration is needed to have it up and running. It just works. Queen behavior π
By default it will output information about these items:
pathβ Displays the url that was used to generate this responsestatusβ The response from the server (e.g. '200 OK')methodβ The method for the request (e.g. 'GET', 'POST', 'PUT or 'DELETE')controllerβ The actual controller method in Pecan responsible for the responseparamsβ A list of tuples for the params passed in at request timehooksβ Any hooks that are used in the app will be listed here.
The default configuration will show those values in the terminal via stdout, and it will also add them to the response headers (in the form of X-Pecan-item_name).
This is how the terminal output might look for a /favicon.ico request:
path - /favicon.ico
status - 404 Not Found
method - GET
controller - The resource could not be found.
params - []
hooks - ['RequestViewerHook']
In the above case, the file was not found, and the information was printed to stdout. Additionally, the following headers would be present in the HTTP response:
X-Pecan-path /favicon.ico
X-Pecan-status 404 Not Found
X-Pecan-method GET
X-Pecan-controller The resource could not be found.
X-Pecan-params []
X-Pecan-hooks ['RequestViewerHook']
The configuration dictionary is flexible (none of the keys are required, so no pressure bestie) and can hold two keys: items and blacklist.
This is how the hook would look if configured directly (shortened for brevity):
...
'hooks': lambda: [
RequestViewerHook({'items':['path']})
]
Modifying Output Format
The items list specifies the information that the hook will return. Sometimes you'll need a specific piece of information, or a certain bunch of them, depending on what you're developing β so the defaults need to change and you specify a list of items.
Note: When specifying a list of items, this list overrides completely the defaults. So if a single item is listed, only that item will be returned by the hook. No leftovers, no extras π
The hook has access to every single attribute the request object has β not just the default ones that are displayed β so you can fine tune the information displayed. Go off, customize it π
okay so here's the lore bestie β every single attribute the hook gets to touch, and yes these are directly from webob, no middleman π :
acceptaccept_charsetaccept_encodingaccept_languageapplication_urlas_stringauthorizationblankbodybody_filebody_file_rawbody_file_seekablecache_controlcall_applicationcharsetcontent_lengthcontent_typecookiescopycopy_bodycopy_getdatedecode_param_namesenvironfrom_filefrom_stringget_responseheadershosthost_urlhttp_versionif_matchif_modified_sinceif_none_matchif_rangeif_unmodified_sinceis_body_readableis_body_seekableis_xhrmake_body_seekablemake_tempfilemax_forwardsmethodparamspathpath_infopath_info_peekpath_info_poppath_qspath_urlpostvarspragmaquery_stringqueryvarsrangerefererreferrerrelative_urlremote_addrremote_userremove_conditional_headersrequest_body_tempfile_limitschemescript_nameserver_nameserver_portstr_GETstr_POSTstr_cookiesstr_paramsstr_postvarsstr_queryvarsunicode_errorsupath_infourlurlargsurlvarsuscript_nameuser_agent
like, all of them. that's the whole roster, no cap π
and then these are the specific ones from Pecan and the hook itself, the little homegrown faves π«Ά:
controllerhooksparamsβ okay soparamsis actually available from webob too, but the hook parses it for you so it's way easier to read (iykyk)
π Blacklisting Certain Paths
ngl, sometimes getting the tea on every single request is justβ¦ a lot. sensory overload fr π so if you wanna quiet it down, pass the list of URL paths you do not want data for as the blacklist.
β οΈ the matching happens at the start of the URL path, so be careful with this one bestie β it's giving "sneaky" π
for example, if you pass a configuration like this one:
{ 'blacklist': ['/f'] }
it would not show any url that starts with f, effectively behaving like a globbing regular expression (but not quite as powerful, so don't get too excited π
).
and for however many blocks you need, just keep adding items β go off queen π
{ 'blacklist' : ['/favicon.ico', '/javascript', '/images'] }
again, the blacklist key can be used alongside the items key or not β it is not required, so do what feels right πΈ
β¨ JSON SERIALIZATION β¨
okay so Pecan literally comes with a whole simple, easy-to-use system for generating and serving JSON and it's giving built-in luxury π
to get started, make a file in your project called json.py and import it in your project's app.py β that's it, that's the vibe π
your json module is gonna hold a series of rules for generating JSON from objects you return in your controller. think of it as your personal translator bestie π«Ά
let's say we have a controller in our Pecan application which we wanna use to return JSON output for a User object:
from myproject.lib import get_current_user
class UsersController(object):
@expose('json')
def current_user(self):
'''
return an instance of myproject.model.User which represents
the current authenticated user
'''
return get_current_user()
for this controller to actually function, Pecan needs to know how to convert the User object into data types compatible with JSON. one way to tell Pecan how to convert an object into JSON is to define a rule in your json.py:
from pecan.jsonify import jsonify
from myproject import model
@jsonify.register(model.User)
def jsonify_user(user):
return dict(
name = user.name,
email = user.email,
birthday = user.birthday.isoformat()
)
in this example, when an instance of the model.User class is returned from a controller which is configured to return JSON, the jsonify_user() rule will be called to convert the object to JSON-compatible data π₯ note that the rule does not generate a JSON string, but rather generates a Python dictionary which contains only JSON friendly data types β important distinction, don't forget it bestie π
alternatively, the rule can be specified on the object itself, by specifying a __json__() method in the class:
class User(object):
def __init__(self, name, email, birthday):
self.name = name
self.email = email
self.birthday = birthday
def __json__(self):
return dict(
name = self.name,
email = self.email,
birthday = self.birthday.isoformat()
)
the benefit of using a json.py module is having all of your JSON rules defined in a central location, but some projects prefer the simplicity of keeping the JSON rules attached directly to their model objects. both are valid, pick your fighter πΈ
π CONTEXT/THREAD-LOCALS VS. EXPLICIT ARGUMENT PASSING π
in any pecan application, the module-level pecan.request and pecan.response are proxy objects that always refer to the request and response being handled in the current thread. no cap, that's the whole magic trick β¨
this thread locality ensures that you can safely access a global reference to the current request and response in a multi-threaded environment without constantly having to pass object references around in your code β it's a feature of pecan that makes writing traditional web applications easier and less verbose π
some people feel thread-locals are too implicit or magical, and that explicit reference passing is much clearer and more maintainable in the long run. additionally, the default implementation provided by pecan uses threading.local() to associate these context-local proxy objects with the thread identifier of the current server thread. in asynchronous server models β where lots of tasks run for short amounts of time on a single shared thread β supporting this mechanism involves monkeypatching threading.local() to behave in a greenlet-local manner π (yes, monkeypatching. we said what we said.)
π¦ Disabling Thread-Local Proxies π¦
if you're certain that you do not want to utilize context/thread-locals in your project, you can do so by passing the argument use_context_locals=False in your application's configuration file:
app = {
'root': 'project.controllers.root.RootController',
'modules': ['project'],
'static_root': '%(confdir)s/public',
'template_path': '%(confdir)s/project/templates',
'debug': True,
'use_context_locals': False
}
additionally, you'll need to update all of your pecan controllers to accept positional arguments for the current request and response:
class RootController(object):
@pecan.expose('json')
def index(self, req, resp):
return dict(method=req.method) # path: /
@pecan.expose()
def greet(self, req, resp, name):
return name # path: /greet/joe
it is imperative that the request and response arguments come after self and before any positional form arguments π€ like, this is not a suggestion, this is the rule. break it and your arguments will be a whole mess fr fr π
π COMMAND LINE PECAN π
Okay so bestie, listen β every single Pecan app can be driven and inspected straight from the command line with the built-in pecan command. It's giving main character energy honestly. π
Quick note before we go: all the usage examples in this doc are meant to be run from your project's root directory. Don't @ me if you run them from somewhere random. π
π Serving a Pecan App For Development π
Pecan ships with a lil' lightweight WSGI development server bundled right in β it's based on Python's wsgiref.simple_server module (docs live at https://docs.python.org/3/library/wsgiref.html#module-wsgiref.simple_server).
Serving your app is literally as easy as running pecan serve:
$ pecan serve config.py
Starting server in PID 000.
serving on 0.0.0.0:8080, view at http://127.0.0.1:8080
...and then just pop it open in your browser. Iconic. π
The server host and port in your configuration file can be changed as described in Server Configuration. π οΈ
π Reloading Automatically as Files Change π
Pausing to restart your dev server every five seconds while you work? So interruptive, so not the vibe. That's why pecan serve gives you a --reload flag to make life easier. π
To pull this off, Pecan uses the Python watchdog library (https://pypi.python.org/pypi/watchdog). You'll need to install it for development use before continuing:
$ pip install watchdog
Downloading/unpacking watchdog
...
Successfully installed watchdog
$ pecan serve --reload config.py
Monitoring for changes...
Starting server in PID 000.
serving on 0.0.0.0:8080, view at http://127.0.0.1:8080
As you work, Pecan will listen for any file or directory modification events in your project and silently restart your server process in the background. No cap, it just does it. β¨
π The Interactive Shell π
Pecan apps also come with an interactive Python shell β you can use it to execute expressions in an environment very similar to the one your app actually runs in. Chef's kiss. π
To invoke it, use the pecan shell command:
$ pecan shell config.py
Pecan Interactive Shell
Python 2.7.1 (r271:86832, Jul 31 2011, 19:30:53)
[GCC 4.2.1 (Based on Apple Inc. build 5658)
The following objects are available:
wsgiapp - This project's WSGI App instance
conf - The current configuration
app - webtest.TestApp wrapped around wsgiapp
>>> conf
Config({
'app': Config({
'root': 'myapp.controllers.root.RootController',
'modules': ['myapp'],
'static_root': '/Users/somebody/myapp/public',
'template_path': '/Users/somebody/myapp/project/templates',
'errors': {'404': '/error/404'},
'debug': True
}),
'server': Config({
'host': '0.0.0.0',
'port': '8080'
})
})
>>> app
<webtest.app.TestApp object at 0x101a830>
>>> app.get('/')
<200 OK text/html body='<html>\n ...\n\n'/936>
Press Ctrl+D to exit the interactive shell (or Ctrl+Z on Windows). Bye bestie! π«Ά
πΈ Using an Alternative Shell πΈ
pecan shell has optional support for the IPython (http://ipython.org/) and bpython (http://bpython-interpreter.org/) alternative shells β each one gets specified with the --shell flag (or its abbreviated alias, -s), like so:
$ pecan shell --shell=ipython config.py
$ pecan shell -s bpython config.py
Pick your fave, they're all slay. π
π Configuration from an environment variable π
In every example so far you've seen the pecan commands take a file path to the configuration file. An alternative? Specify the config file in an environment variable instead β PECAN_CONFIG. π
This is completely optional, btw. If a file path is passed in explicitly, Pecan will honor that before looking for an environment variable. Explicit wins, always.
For example, to serve a Pecan application, a variable could be exported and then re-used when no path is passed in:
$ export PECAN_CONFIG=/path/to/app/config.py
$ pecan serve
Starting server in PID 000.
serving on 0.0.0.0:8080, view at http://127.0.0.1:8080
Heads up though β the path needs to reference a valid pecan configuration file, otherwise the command will error out with a message saying the path is invalid (like, for instance, if you passed in a directory). And if PECAN_CONFIG isn't set and no configuration is passed in? The command errors out, because it can't locate a configuration file. It's not magic, bestie. π
π§° Extending pecan with Custom Commands π§°
The commands that ship with Pecan are cute and all, but the real utility of its command line toolset lies in its extensibility. It's so convenient to be able to write a Python script that works "in a Pecan environment" β with access to things like your app's parsed configuration file, or a simulated instance of your app itself (like the one pecan shell gives you). Obsessed. π₯
π οΈ Writing a Custom Pecan Command π οΈ
As an example, let's create a command that issues a simulated HTTP GET to your application and prints the result. Invoking it from the command line might look something like this:
$ pecan wget config.py /path/to/some/resource
Say you have a distribution with a package named myapp, and within that package lives a wget.py module:
# myapp/myapp/wget.py
import pecan
from webtest import TestApp
class GetCommand(pecan.commands.BaseCommand):
'''
Issues a (simulated) HTTP GET and returns the request body.
'''
arguments = pecan.commands.BaseCommand.arguments + ({
'name': 'path',
'help': 'the URI path of the resource to request'
},)
def run(self, args):
super(GetCommand, self).run(args)
app = TestApp(self.load_app())
print app.get(args.path).body
Let's analyze this piece-by-piece. π
π« Overriding the run Method π«
First up, we're subclassing BaseCommand and extending the run() method to:
- Load a Pecan application β
load_app() - Wrap it in a fake WSGI environment β
TestApp(https://docs.pylonsproject.org/projects/webtest/en/latest/api.html#webtest.app.TestApp) - Issue an HTTP
GETrequest against it βget()(https://docs.pylonsproject.org/projects/webtest/en/latest/api.html#webtest.app.TestApp.get)
Three steps, one method, zero stress. πΈ
π― Defining Custom Arguments π―
The arguments class attribute is what you use to define command line arguments specific to your custom command. Notice in the example that we're adding to the arguments list provided by BaseCommand (which already gives you an argument for the config_file) rather than overriding it entirely. Teamwork, fr fr. π€
The format of the arguments class attribute is a tuple of dictionaries (https://docs.python.org/3/library/stdtypes.html#tuple) β each dictionary represents an argument definition in the same format accepted by Python's argparse module (https://docs.python.org/3/library/argparse.html#module-argparse), more specifically add_argument() (https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser.add_argument). By providing a list of arguments in this format, the pecan command can include your custom commands in the help and usage output it provides. β¨
$ pecan -h
usage: pecan [-h] command ...
positional arguments:
command
wget Issues a (simulated) HTTP GET and returns the request body
serve Open an interactive shell with the Pecan app loaded
...
$ pecan wget -h
usage: pecan wget [-h] config_file path
$ pecan wget config.py /path/to/some/resource
Also β and this is the cute part β the first line of the docstring from GetCommand, i.e. Issues a (simulated) HTTP GET and returns the request body, gets automatically used to describe the wget command in the output for $ pecan -h. Following this convention lets you easily integrate a summary for your command into the Pecan command line tool. Little details, big slay. π
π Registering a Custom Command π
So now that you've written your custom command, you'll need to tell your distribution's setup.py about its existence and reinstall. Within your distribution's setup.py file, you'll find a call to setup().
# myapp/setup.py
...
setup(
name='myapp',
version='0.1',
author='Joe Somebody',
...
)
Assuming it doesn't exist already, we'll add the entry_points argument to the setup() call, and define a [pecan.command] definition for your custom command:
# myapp/setup.py
...
setup(
name='myapp',
version='0.1',
author='Joe Somebody',
...
entry_points="""
[pecan.command]
wget = myapp.wget:GetCommand
"""
)
Once you've done that, reinstall your project in development to register the new entry point:
$ python setup.py develop
Then give it a try. π
$ pecan wget config.py /path/to/some/resource
π οΈ DEVELOPING PECAN APPLICATIONS LOCALLY π οΈ
okay so like, this whole section is about your local dev setup β the stuff you do while you're building your app, before it goes out into the world. it's giving "glow-up phase" energy β¨
π Reloading Automatically as Files Change π
bestie. stopping and restarting your dev server every single time you touch a file? that is so interruptive, it's honestly a vibe killer π so pecan serve gives you a --reload flag to make life easier. slay.
to make this work, Pecan uses the Python watchdog library (over at <https://pypi.python.org/pypi/watchdog> β well, the link's in the original, iykyk). you gotta install it for dev use before you keep going:
$ pip install watchdog
Downloading/unpacking watchdog
...
Successfully installed watchdog
$ pecan serve --reload config.py
Monitoring for changes...
Starting server in PID 000.
serving on 0.0.0.0:8080, view at http://127.0.0.1:8080
and then while you're out here doing your thing, Pecan literally listens for any file or directory modification event in your project and silently restarts your server process in the background. no cap. it just handles it π
π Debugging Pecan Applications π
Pecan ships with some simple debugging middleware for helping you diagnose problems in your apps, and honestly it's lowkey a lifesaver. to turn it on, just set the debug flag to True in your configuration file:
app = {
...
'debug': True,
...
}
once it's enabled, the middleware will automatically catch exceptions raised by your app and display the Python stack trace and WSGI environment in your browser whenever runtime exceptions get raised. so you actually get to see what went wrong instead of just screaming into the void π
and to improve debugging β including support for an interactive browser-based console π₯ β Pecan uses the Python backlash library (<https://pypi.python.org/pypi/backlash>). you'll need to install it for development use before continuing:
$ pip install backlash
Downloading/unpacking backlash
...
Successfully installed backlash
a whole console in the browser? that's main character energy fr fr π
π Serving Static Files π
Pecan comes with simple file serving middleware for serving CSS, Javascript, images, and other static files. very "everything in its place" of it. you can configure it by making sure the following options are specified in your configuration file:
app = {
...
'debug': True,
'static_root': '%(confdir)/public
}
where static_root is an absolute pathname to the directory where your static files live. and for convenience, the path may include the %(confdir) variable β Pecan will swap that out with the absolute path of your configuration file at runtime π¦ so you don't have to hardcode your whole machine path, iconic behavior.
note: in production,app.debugshould never be set toTrue, so you'll need to serve your static files via your production web server. like, seriously β never π keep the debug vibes strictly local, bestie.
three little bits of middleware, three little quality-of-life upgrades β your dev environment is literally thriving rn π
DEPLOYING PECAN IN PRODUCTION ππ
Okay bestie, so you've built your app and now it needs to live on the actual internet, and there are genuinely a variety of ways to deploy a Pecan project to a production environment. The following examples are meant to provide direction, not explicit instruction β deployment is usually heavily dependent upon the needs and goals of individual applications, so your mileage will probably vary. π
Note: While Pecan comes packaged with a simple server for development use (pecan serve), using a production-ready server similar to the ones described in this document is very highly encouraged. Like, please. π₯Ί
πΈ Installing Pecan
A few popular options are available for installing Pecan in production environments:
- Using setuptools β
https://pypi.python.org/pypi/setuptools. Manage Pecan as a dependency in your project'ssetup.pyfile so that it's installed alongside your project (e.g.,python /path/to/project/setup.py install). The default Pecan project described in Creating Your First Pecan Application (#quick-start) facilitates this by including Pecan as a dependency for your project. - Using pip β
http://www.pip-installer.org/. Usepip freezeandpip installto create and install from arequirements.txtfile for your project. - Via the manual instructions found in Installation (
#installation).
Note: Regardless of the route you choose, it's highly recommended that all deployment installations be done in a Python virtual environment at http://www.virtualenv.org/. fr fr, do not skip this one π«Ά
π¨ Disabling Debug Mode
Warning: One of the most important steps to take before deploying a Pecan app into production is to ensure that you have disabled Debug Mode, which provides a development-oriented debugging environment for tracebacks encountered at runtime. Failure to disable this development tool in your production environment will result in serious security issues. In your production configuration file, ensure thatdebugis set toFalse.
# myapp/production_config.py
app = {
...
'debug': False
}
Not me begging you to actually do this. π It's giving "leaving the front door wide open and also taping the key to it." Do not.
π Pecan and WSGI
WSGI is a Python standard that describes a standard interface between servers and an application. Any Pecan application is also known as a "WSGI application" because it implements the WSGI interface, so any server that is "WSGI compatible" may be used to serve your application. A few popular examples are:
- mod_wsgi β
http://code.google.com/p/modwsgi/ - uWSGI β
http://projects.unbit.it/uwsgi/ - Gunicorn β
http://gunicorn.org/ - waitress β
http://docs.pylonsproject.org/projects/waitress/en/latest/ - CherryPy β
http://cherrypy.org/
Generally speaking, the WSGI entry point to any Pecan application can be generated using deploy() (#pecan.deploy.deploy):
from pecan.deploy import deploy
application = deploy('/path/to/some/app/config.py')
That's literally it. Two lines and you're a WSGI app now. iconic. β¨
π Considerations for Static Files
Pecan comes with static file serving (e.g., CSS, Javascript, images) middleware which is not recommended for use in production.
In production, Pecan doesn't serve media files itself; it leaves that job to whichever web server you choose.
For serving static files in production, it's best to separate your concerns by serving static files separately from your WSGI application (primarily for performance reasons). There are several popular ways to accomplish this. Here are two:
- Set up a proxy server (such as nginx β
http://nginx.org/en, cherokee βhttp://www.cherokee-project.com, CherryPy, or lighttpd βhttp://www.lighttpd.net/) to serve static files and proxy application requests through to your WSGI application:
<HTTP Client> βββ <Production/Proxy Server>, e.g., Apache, nginx, cherokee (0.0.0.0:80) βββ <Static Files>
β
βββ <WSGI Server> Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5000 or /tmp/some.sock)
βββ <WSGI Server> Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5001 or /tmp/some.sock)
βββ <WSGI Server> Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5002 or /tmp/some.sock)
βββ <WSGI Server> Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5003 or /tmp/some.sock)
- Serve static files via a separate service, virtual host, or CDN.
Look at that diagram. The way it just fans out into worker instances. π¦ main character architecture honestly.
π³ Common Recipes
Apache + mod_wsgi
mod_wsgi β http://code.google.com/p/modwsgi/ β is a popular Apache module which can be used to host any WSGI-compatible Python application (including your Pecan application π).
To get started, check out the installation and configuration documentation for mod_wsgi β http://code.google.com/p/modwsgi/wiki/InstallationInstructions.
For the sake of example, let's say that our project, simpleapp, lives at /var/www/simpleapp, and that a virtualenv β http://www.virtualenv.org β has been created at /var/www/venv with any necessary dependencies installed (including Pecan). Additionally, for security purposes, we've created a user, user1, and a group, group1 to execute our application under.
The first step is to create a .wsgi file which mod_wsgi will use as an entry point for your application:
# /var/www/simpleapp/app.wsgi
from pecan.deploy import deploy
application = deploy('/var/www/simpleapp/config.py')
Next, add Apache configuration for your application. Here's a simple example:
<VirtualHost *>
ServerName example.com
WSGIDaemonProcess simpleapp user=user1 group=group1 threads=5 python-path=/var/www/venv/lib/python2.7/site-packages
WSGIScriptAlias / /var/www/simpleapp/app.wsgi
<Directory /var/www/simpleapp/>
WSGIProcessGroup simpleapp
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all
</Directory>
</VirtualHost>
For more instructions and examples of mounting WSGI applications using mod_wsgi, consult the mod_wsgi Documentation β http://code.google.com/p/modwsgi/wiki/QuickConfigurationGuide#Mounting_The_WSGI_Application.
Finally, restart Apache and give it a try. π
uWSGI
uWSGI β http://projects.unbit.it/uwsgi/ β is a fast, self-healing and developer/sysadmin-friendly application container server coded in pure C. It uses the uwsgi protocol β http://projects.unbit.it/uwsgi/wiki/uwsgiProtocol β but can speak other protocols as well (http, fastcgi...).
Running Pecan applications with uWSGI is a snap:
$ pip install uwsgi
$ pecan create simpleapp && cd simpleapp
$ python setup.py develop
$ uwsgi --http-socket :8080 --venv /path/to/virtualenv --pecan config.py
or using a Unix socket (that nginx, for example, could be configured to proxy to β http://projects.unbit.it/uwsgi/wiki/RunOnNginx):
$ uwsgi -s /tmp/uwsgi.sock --venv /path/to/virtualenv --pecan config.py
Four lines and you're live. no cap. π₯
Gunicorn
Gunicorn β http://gunicorn.org/ β or "Green Unicorn", is a WSGI HTTP Server for UNIX. It's a pre-fork worker model ported from Ruby's Unicorn project. It supports both eventlet and greenlet.
Running a Pecan application on Gunicorn is simple. Let's walk through it with Pecan's default project:
$ pip install gunicorn
$ pecan create simpleapp && cd simpleapp
$ python setup.py develop
$ gunicorn_pecan config.py
The green unicorn ate. π¦π
CherryPy
CherryPy β http://cherrypy.org/ β offers a pure Python HTTP/1.1-compliant WSGI thread-pooled web server. It can support Pecan applications easily and even serve static files like a production server would do.
The examples that follow are geared towards using CherryPy as the server in charge of handling a Pecan app along with serving static files.
$ pip install cherrypy
$ pecan create simpleapp && cd simpleapp
$ python setup.py develop
To run with CherryPy, the easiest approach is to create a script in the root of the project (alongside setup.py), so that we can describe how our example application should be served. This is how the script (named run.py) looks:
import os
import cherrypy
from cheroot import wsgi
from cheroot.wsgi import PathInfoDispatcher
from pecan.deploy import deploy
simpleapp_wsgi_app = deploy('/path/to/production_config.py')
public_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'public'))
# A dummy class for our Root object
# necessary for some CherryPy machinery
class Root(object):
pass
def make_static_config(static_dir_name):
"""
All custom static configurations are set here, since most are common, it
makes sense to generate them just once.
"""
static_path = os.path.join('/', static_dir_name)
path = os.path.join(public_path, static_dir_name)
configuration = {
static_path: {
'tools.staticdir.on': True,
'tools.staticdir.dir': path
}
}
return cherrypy.tree.mount(Root(), '/', config=configuration)
# Assuming your app has media on different paths, like 'css', and 'images'
application = PathInfoDispatcher({
'/': simpleapp_wsgi_app,
'/css': make_static_config('css'),
'/images': make_static_config('images')
})
server = wsgi.Server(('0.0.0.0', 8080), application, server_name='simpleapp')
try:
server.start()
except KeyboardInterrupt:
print("Terminating server...")
server.stop()
To start the server, simply call it with the Python executable:
$ python run.py
The try/except KeyboardInterrupt with the cute little print("Terminating server...")? That's called graceful shutdown and it's highkey elegant. π
π LOGGING π
okay so Pecan is literally besties with Pythonβs standard-library logging module (https://docs.python.org/3/library/logging.html#module-logging) β it passes your logging config options straight into the logging.config.dictConfig function (http://docs.python.org/library/logging.config.html#configuration-dictionary-schema). The full docs for the dictConfig() format are the actual best source of truth for logging config, but to get you started this chapter gives you a few simple examples. π«Ά
β¨ Configuring Logging β¨
A sample logging config ships with the quickstart project from Creating Your First Pecan Application (#quick-start):
$ pecan create myapp
The default config defines one handler and two loggers. No cap.
# myapp/config.py
app = { ... }
server = { ... }
logging = {
'root' : {'level': 'INFO', 'handlers': ['console']},
'loggers': {
'myapp': {'level': 'DEBUG', 'handlers': ['console']}
},
'handlers': {
'console': {
'level': 'DEBUG',
'class': 'logging.StreamHandler',
'formatter': 'simple'
}
},
'formatters': {
'simple': {
'format': ('%(asctime)s %(levelname)-5.5s [%(name)s]'
'[%(threadName)s] %(message)s')
}
}
}
consolelogs messages tostderrusing thesimpleformatter.myapplogs messages sent at a level above or equal toDEBUGto theconsolehandler.rootlogs messages at a level above or equal to theINFOlevel to theconsolehandler.
πΈ Writing Log Messages in Your Application πΈ
The logger named myapp is reserved for your usage in your Pecan application. That oneβs yours, bestie. π
Once youβve configured your logging, you can place logging calls right in your code. Using the logging framework is genuinely very simple:
# myapp/myapp/controllers/root.py
from pecan import expose
import logging
logger = logging.getLogger(__name__)
class RootController(object):
@expose()
def index(self):
if bad_stuff():
logger.error('Uh-oh!')
return dict()
π¦ Logging to Files and Other Locations π¦
Pythonβs logging library (https://docs.python.org/3/library/logging.html#module-logging) defines a whole variety of handlers that help you write logs to files. A few interesting ones are:
FileHandler(https://docs.python.org/3/library/logging.handlers.html#logging.FileHandler) β used to log messages to a file on the filesystem.RotatingFileHandler(https://docs.python.org/3/library/logging.handlers.html#logging.handlers.RotatingFileHandler) β similar toFileHandler(https://docs.python.org/3/library/logging.handlers.html#logging.FileHandler), but it also rotates logs periodically.SysLogHandler(https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler) β used to log messages to a UNIX syslog.SMTPHandler(https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SMTPHandler) β used to log messages to an email address via SMTP.
Using any of them is as simple as defining a new handler in your applicationβs logging block and assigning it to one or more loggers. Iconic behavior, honestly. β¨
π Logging Requests with Paste Translogger π
Paste (http://pythonpaste.org/) β which is not included with Pecan β includes the TransLogger middleware for logging requests in Apache Combined Log Format (http://httpd.apache.org/docs/2.2/logs.html#combined). Combined with file-based logging, TransLogger can be used to create an access.log file similar to Apache.
To add this middleware, modify the setup_app method in your projectβs app.py as follows:
# myapp/myapp/app.py
from pecan import make_app
from paste.translogger import TransLogger
def setup_app(config):
# ...
app = make_app(
config.app.root
# ...
)
app = TransLogger(app, setup_console_handler=False)
return app
By default, TransLogger creates a logger named wsgi, so youβll need to specify a new (file-based) handler for this logger in your Pecan configuration file:
# myapp/config.py
app = { ... }
server = { ... }
logging = {
'loggers': {
# ...
'wsgi': {'level': 'INFO', 'handlers': ['logfile'], 'qualname': 'wsgi'}
},
'handlers': {
# ...
'logfile': {
'class': 'logging.FileHandler',
'filename': '/etc/access.log',
'level': 'INFO',
'formatter': 'messageonly'
}
},
'formatters': {
# ...
'messageonly': {'format': '%(message)s'}
}
}
π§ͺ TESTING PECAN APPLICATIONS π§ͺ
Tests can live anywhere in your Pecan project as long as the test runner can discover them. Traditionally, they exist in a package named myapp.tests.
The suggested mechanism for unit and integration testing of a Pecan application is the unittest module (https://docs.python.org/3/library/unittest.html#module-unittest). yep, thatβs the one. π«Ά
β Test Discovery and Other Tools β
Tests for a Pecan project can be invoked as simply as python setup.py test, though itβs possible to run your tests with different discovery and automation tools. In particular, Pecan projects are known to work well with nose (http://pypi.python.org/pypi/nose/1.1.2), pytest (http://pytest.org), and tox (http://pypi.python.org/pypi/tox).
π Writing Functional Tests with WebTest π
A unit test typically relies on βmockβ or βfakeβ objects to give the code under test enough context to run. In this way, only an individual unit of source code is tested.
A healthy suite of tests combines unit tests with functional tests. In the context of a Pecan application, functional tests can be written with the help of the webtest library (https://docs.pylonsproject.org/projects/webtest/en/latest/api.html#module-webtest). In this way, itβs possible to write tests that verify the behavior of an HTTP request life cycle from the controller routing down to the HTTP response. The following is an example that is similar to the one included with Pecanβs quickstart project.
# myapp/myapp/tests/__init__.py
import os
from unittest import TestCase
from pecan import set_config
from pecan.testing import load_test_app
class FunctionalTest(TestCase):
"""
Used for functional tests where you need to test your
literal application and its integration with the framework.
"""
def setUp(self):
self.app = load_test_app(os.path.join(
os.path.dirname(__file__),
'config.py'
))
def tearDown(self):
set_config({}, overwrite=True)
The testing utility included with Pecan, pecan.testing.load_test_app() (#pecan.testing.load_test_app), can be passed a file path representing a Pecan configuration file, and will return an instance of the application, wrapped in a TestApp (https://docs.pylonsproject.org/projects/webtest/en/latest/api.html#webtest.app.TestApp) environment.
From here, itβs possible to extend the FunctionalTest base class and write tests that issue simulated HTTP requests.
class TestIndex(FunctionalTest):
def test_index(self):
resp = self.app.get('/')
assert resp.status_int == 200
assert 'Hello, World' in resp.body
See also: See thewebtestdocumentation (https://docs.pylonsproject.org/projects/webtest/en/latest/api.html#module-webtest) for further information about the methods available to aTestApp(https://docs.pylonsproject.org/projects/webtest/en/latest/api.html#webtest.app.TestApp) instance.
π Special Testing Variables π
Sometimes itβs not enough to make assertions about the response body of certain requests. To aid in inspection, Pecan applications provide a special set of βtesting variablesβ to any TestResponse (https://docs.pylonsproject.org/projects/webtest/en/latest/api.html#webtest.response.TestResponse) object. lowkey genius.
Letβs suppose that your Pecan application had some controller which took a name as an optional argument in the URL.
# myapp/myapp/controllers/root.py
from pecan import expose
class RootController(object):
@expose('index.html')
def index(self, name='Joe'):
"""A request to / will access this controller"""
return dict(name=name)
and rendered that name in its template (and thus, the response body).
# myapp/myapp/templates/index.html
Hello, ${name}!
A functional test for this controller might look something like:
class TestIndex(FunctionalTest):
def test_index(self):
resp = self.app.get('/')
assert resp.status_int == 200
assert 'Hello, Joe!' in resp.body
In addition to webtest.TestResponse.body, Pecan also provides webtest.TestResponse.namespace, which represents the template namespace returned from the controller, and webtest.TestResponse.template_name, which contains the name of the template used.
class TestIndex(FunctionalTest):
def test_index(self):
resp = self.app.get('/')
assert resp.status_int == 200
assert resp.namespace == {'name': 'Joe'}
assert resp.template_name == 'index.html'
In this way, itβs possible to test the return value and rendered template of individual controllers. periodt. π
β¨ GENERATING AND VALIDATING FORMS β¨
okay so Pecan literally provides no opinionated support for working with form generation and validation libraries β no cap β but itβs lowkey easy to import your library of choice with minimal effort. π«Ά
this lil article details best practices for integrating the iconic popular forms library, WTForms (http://wtforms.simplecodes.com/), into your Pecan project. π
π§ Defining a Form Definition
letβs start by building a basic form with a required first_name field and an optional last_name field. π
from wtforms import Form, TextField, validators
class MyForm(Form):
first_name = TextField(u'First Name', validators=[validators.required()])
last_name = TextField(u'Last Name', validators=[validators.optional()])
class SomeController(object):
pass
π Rendering a Form in a Template
next, letβs add a controller, and pass a form instance to the template. π
from pecan import expose
from wtforms import Form, TextField, validators
class MyForm(Form):
first_name = TextField(u'First Name', validators=[validators.required()])
last_name = TextField(u'Last Name', validators=[validators.optional()])
class SomeController(object):
@expose(template='index.html')
def index(self):
return dict(form=MyForm())
hereβs the Mako (http://www.makeotemplates.org/) template file: π¦
<form method="post" action="/">
<div>
${form.first_name.label}:
${form.first_name}
</div>
<div>
${form.last_name.label}:
${form.last_name}
</div>
<input type="submit" value="submit">
</form>
π Validating POST Values
using the same MyForm definition, letβs redirect the user if the form is validated, otherwise, render the form again. πβ¨
from pecan import expose, request
from wtforms import Form, TextField, validators
class MyForm(Form):
first_name = TextField(u'First Name', validators=[validators.required()])
last_name = TextField(u'Last Name', validators=[validators.optional()])
class SomeController(object):
@expose(template='index.html')
def index(self):
my_form = MyForm(request.POST)
if request.method == 'POST' and my_form.validate():
# save_values()
redirect('/success')
else:
return dict(form=my_form)
π WORKING WITH SESSIONS AND USER AUTHENTICATION π
Pecan provides no opinionated support for managing user sessions, but itβs easy to hook into your session framework of choice with minimal effort. π
this article details best practices for integrating the popular session framework, Beaker (http://beaker.groovie.org), into your Pecan project. π¦
π₯ Setting up Session Management
there are several approaches that can be taken to set up session management. One approach is WSGI middleware. Another is Pecan Pecan Hooks (<#hooks>). π«Ά
hereβs an example of wrapping your WSGI application with Beakerβs SessionMiddleware (https://beaker.readthedocs.io/en/latest/modules/middleware.html#beaker.middleware.SessionMiddleware) in your projectβs app.py. π
from pecan import conf, make_app
from beaker.middleware import SessionMiddleware
from test_project import model
app = make_app(
...
)
app = SessionMiddleware(app, conf.beaker)
and a corresponding dictionary in your configuration file. π
beaker = {
'session.key' : 'sessionkey',
'session.type' : 'cookie',
'session.validate_key' : '05d2175d1090e31f42fa36e63b8d2aad',
'__force_dict__' : True
}
π WORKING WITH DATABASES, TRANSACTIONS, AND ORM'S π
Okay bestie, real talk: Pecan has zero opinions about your database. None. It's not gonna hand you an ORM and tell you how to live your life. But hooking up your ORM of choice? Literally so easy. This whole section is the best-practices rundown for wiring the very popular Python ORM, SQLAlchemy (<http://sqlalchemy.org>), into your Pecan project. Let's go β¨
π§ init_model and Preparing Your Model
So Pecan's default quickstart project ships with an empty little stub directory where you can build out your model however you want. Behold:
.
βββ test_project
βββ app.py
βββ __init__.py
βββ controllers
βββ model
β βββ __init__.py
βββ templates
By default this module has one special method in it, init_model():
from pecan import conf
def init_model():
"""
This is a stub method which is called at application startup time.
If you need to bind to a parsed database configuration, set up tables
or ORM classes, or perform any database initialization, this is the
recommended place to do it.
For more information working with databases, and some common recipes,
see https://pecan.readthedocs.io/en/latest/databases.html
"""
pass
What's the point of this method, you ask? Iconic question. Its whole job is to read bindings out of your config file and spin up the engines, pools, etc. that your ORM (or database toolkit of choice) needs.
Bonus: your project's model module is also the spot to define functions for the common binding operations β starting transactions, committing or rolling back work, clearing a session. And it's where your object and relation definitions live too. One module, many hats π
Here's what a sample Pecan config file with database bindings might look like:
# Server Specific Configurations
server = {
...
}
# Pecan Application Configurations
app = {
...
}
# Bindings and options to pass to SQLAlchemy's ``create_engine``
sqlalchemy = {
'url' : 'mysql://root:@localhost/dbname?charset=utf8&use_unicode=0',
'echo' : False,
'echo_pool' : False,
'pool_recycle' : 3600,
'encoding' : 'utf-8'
}
And here's a basic model implementation you can use to configure and bind with SQLAlchemy:
from pecan import conf
from sqlalchemy import create_engine, MetaData
from sqlalchemy.orm import scoped_session, sessionmaker
Session = scoped_session(sessionmaker())
metadata = MetaData()
def _engine_from_config(configuration):
configuration = dict(configuration)
url = configuration.pop('url')
return create_engine(url, **configuration)
def init_model():
conf.sqlalchemy.engine = _engine_from_config(conf.sqlalchemy)
def start():
Session.bind = conf.sqlalchemy.engine
metadata.bind = Session.bind
def commit():
Session.commit()
def rollback():
Session.rollback()
def clear():
Session.remove()
π Binding Within the Application
So how do you wrap your app's requests in the right model function calls? There are a few approaches. One is WSGI middleware. But honestly? We also stan Pecan Hooks. Pecan ships with TransactionHook, a hook that will wrap requests in database transactions for you π«Ά To use it, just drop it into your project's app.py file and hand it the functions related to database binding:
from pecan import conf, make_app
from pecan.hooks import TransactionHook
from test_project import model
app = make_app(
conf.app.root,
static_root = conf.app.static_root,
template_path = conf.app.template_path,
debug = conf.app.debug,
hooks = [
TransactionHook(
model.start,
model.start_read_only,
model.commit,
model.rollback,
model.clear
)
]
)
In the example above, on HTTP POST, PUT, and DELETE requests, TransactionHook handles the transaction automatically by following these rules:
- Before controller routing has been determined,
model.start()is called. This function should bind to the appropriate SQLAlchemy engine and start a transaction. - Controller code is run and returns.
- If your controller or template rendering fails and raises an exception,
model.rollback()is called and the original exception is re-raised. This lets you roll back your database transaction so you never commit work when exceptions happen in your app code. No half-finished writes, periodt. - If the controller returns successfully,
model.commit()andmodel.clear()are called.
Now β on idempotent operations (like HTTP GET and HEAD requests), TransactionHook handles transactions following different rules:
model.start_read_only()is called. This function should bind to your SQLAlchemy engine.- Controller code is run and returns.
- If the controller returns successfully,
model.clear()is called.
Also note there's a super useful after_commit() decorator provided in pecan.decorators β check the Pecan Decorators section, iykyk π
π¦ Splitting Reads and Writes
Okay so if you use the strategy above with TransactionHook, splitting your database reads and writes by HTTP method becomes so simple. Like, GET/HEAD requests are read-only and could potentially get routed to a read-only database slave, while POST/PUT/DELETE requests need writing and would always bind to a master database with read/write privileges. Chef's kiss π You can also extend TransactionHook or write your own hook implementation if you want more refined control over where and when your database bindings get called.
Say you've got a master/standby setup, where the master accepts write requests and the standby can only get read requests. A Pecan configuration for sqlalchemy could look like:
# Server Specific Configurations
server = {
...
}
# Pecan Application Configurations
app = {
...
}
# Master database
sqlalchemy_w = {
'url': 'postgresql+psycopg2://root:@master_host/dbname',
'pool_recycle': 3600,
'encoding': 'utf-8'
}
# Read Only database
sqlalchemy_ro = {
'url': 'postgresql+psycopg2://root:@standby_host/dbname',
'pool_recycle': 3600,
'encoding': 'utf-8'
}
Since each database has its own unique config settings, the bindings need to change from what Pecan's default quickstart gives you (see the init_model and Preparing Your Model section above) so they can accommodate both write and read-only requests:
from pecan import conf
from sqlalchemy import create_engine, MetaData
from sqlalchemy.orm import scoped_session, sessionmaker
Session = scoped_session(sessionmaker())
metadata = MetaData()
def init_model():
conf.sqlalchemy_w.engine = _engine_from_config(conf.sqlalchemy_w)
conf.sqlalchemy_ro.engine = _engine_from_config(conf.sqlalchemy_ro)
def _engine_from_config(configuration):
configuration = dict(configuration)
url = configuration.pop('url')
return create_engine(url, **configuration)
def start():
Session.bind = conf.sqlalchemy_w.engine
metadata.bind = conf.sqlalchemy_w.engine
def start_read_only():
Session.bind = conf.sqlalchemy_ro.engine
metadata.bind = conf.sqlalchemy_ro.engine
def commit():
Session.commit()
def rollback():
Session.rollback()
def clear():
Session.close()
def flush():
Session.flush()
π CUSTOM ERROR DOCUMENTS π
okay so this article is literally about configuring a Pecan app to show a custom error page whenever the server returns a 404 Page Not Found status. bestie, we're making errors cute.
This article assumes that you have already created a test application as described in Creating Your First Pecan Application (the <#quick-start> section). π«Ά
Note: π While this example focuses on the HTTP 404 message, the same technique may be applied to define custom actions for any of the HTTP status response codes in the 400 and 500 range. You are well advised to use this power judiciously.
β¨ Overview β¨
Pecan makes it simple to customize error documents in two simple steps:
- Configure Routing of the HTTP status messages you want to handle in your application's
config.py - Write Custom Controllers to handle the status messages you have configured
π Configure Routing π
Let's configure our application test_project to route HTTP 404 Page Not Found messages to a custom controller.
First, let's update test_project/config.py to specify a new error-handler.
# Pecan Application Configurations
app = {
'root' : 'test_project.controllers.root.RootController',
'modules' : ['test_project'],
'static_root' : '%(confdir)s/public',
'template_path' : '%(confdir)s/test_project/templates',
'reload' : True,
'debug' : True,
# modify the 'errors' key to direct HTTP status codes to a custom
# controller
'errors' : {
#404 : '/error/404',
404 : '/notfound',
'__force_dict__' : True
}
}
Instead of the default error page, Pecan will now route 404 messages to the controller method notfound. slay. β¨
πΈ Write Custom Controllers πΈ
The easiest way to implement the error handler is to add it to test_project.root.RootController class (typically in test_project/controllers/root.py).
from pecan import expose
from webob.exc import status_map
class RootController(object):
@expose(generic=True, template='index.html')
def index(self):
return dict()
@index.when(method='POST')
def index_post(self, q):
redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q)
## custom handling of '404 Page Not Found' messages
@expose('error.html')
def notfound(self):
return dict(status=404, message="test_project does not have this page")
@expose('error.html')
def error(self, status):
try:
status = int(status)
except ValueError:
status = 0
message = getattr(status_map.get(status), 'explanation', '')
return dict(status=status, message=message)
And that's it! π
Notice that the only bit of code we added to our RootController was:
## custom handling of '404 Page Not Found' messages
@expose('error.html')
def notfound(self):
return dict(status=404, message="test_project does not have this page")
We simply expose() the notfound controller with the error.html template (which was conveniently generated for us and placed under test_project/templates/ when we created test_project). As with any Pecan controller, we return a dictionary of variables for interpolation by the template renderer.
Now we can modify the error template, or write a brand new one to make the 404 error status page of test_project as pretty or fancy as we want. main character error page energy, honestly. π
π EXAMPLE APPLICATION: SIMPLE FORMS PROCESSING π
This guide will walk you through building a simple Pecan web application that will do some simple forms processing. we're literally doing forms, bestie.
π¦ Project Setup π¦
First, you'll need to install Pecan:
$ pip install pecan
Use Pecan's basic template support to start a new project:
$ pecan create mywebsite
$ cd mywebsite
Install the new project in development mode:
$ python setup.py develop
With the project ready, go into the templates folder and edit the index.html file. Modify it so that it resembles this:
<%inherit file="layout.html" />
<%def name="title()">
Welcome to Pecan!
</%def>
<header>
<h1><img src="/images/logo.png" /></h1>
</header>
<div id="content">
<form method="POST" action="/">
<fieldset>
<p>Enter a message: <input name="message" /></p>
<p>Enter your first name: <input name="first_name" /></p>
<input type="submit" value="Submit" />
</fieldset>
</form>
% if not form_post_data is UNDEFINED:
<p>${form_post_data['first_name']}, your message is: ${form_post_data['message']}</p>
% endif
</div>
What did we just do? π
- Modified the contents of the
formtag to have twoinputtags. The first is namedmessageand the second is namedfirst_name - Added a check if
form_post_datahas not been defined so we don't show the message or wording - Added code to display the message from the user's POST action
Go into the controllers folder now and edit the root.py file. There will be two functions inside of the RootController class which will display the index.html file when your web browser hits the / endpoint. If the user puts some data into the textbox and hits the submit button then they will see the personalized message displayed back at them.
Modify the root.py to look like this:
from pecan import expose
class RootController(object):
@expose(generic=True, template='index.html')
def index(self):
return dict()
@index.when(method='POST', template='index.html')
def index_post(self, **kwargs):
return dict(form_post_data=kwargs)
What did we just do? π§
- Modified the
indexfunction to render the initialindex.htmlwebpage - Modified the
index_postfunction to return the posted data via keyword arguments
Run the application:
$ pecan serve config.py
Open a web browser: http://127.0.0.1:8080/
π Adding Validation π
Enter a message into the textbox along with a name in the second textbox and press the submit button. You should see a personalized message displayed below the form once the page posts back.
One problem you might have noticed is if you don't enter a message or a first name then you simply see no value entered for that part of the message. Let's add a little validation to make sure a message and a first name was actually entered. For this, we will use WTForms (http://wtforms.simplecodes.com/) but you can substitute anything else for your projects.
Add support for the WTForms (http://wtforms.simplecodes.com/) library:
$ pip install wtforms
Note: π«Ά Keep in mind that Pecan is not opinionated when it comes to a particular library when working with form generation, validation, etc. Choose which libraries you prefer and integrate those with Pecan. This is one way of doing this, there are many more ways so feel free to handle this however you want in your own projects.
Go back to the root.py files and modify it like this:
from pecan import expose, request
from wtforms import Form, TextField, validators
class PersonalizedMessageForm(Form):
message = TextField(u'Enter a message',
validators=[validators.required()])
first_name = TextField(u'Enter your first name',
validators=[validators.required()])
class RootController(object):
@expose(generic=True, template='index.html')
def index(self):
return dict(form=PersonalizedMessageForm())
@index.when(method='POST', template='index.html')
def index_post(self):
form = PersonalizedMessageForm(request.POST)
if form.validate():
return dict(message=form.message.data,
first_name=form.first_name.data)
else:
return dict(form=form)
What did we just do? π
- Added the
PersonalizedMessageFormwith two textfields and a required field validator for each - Modified the
indexfunction to create a new instance of thePersonalizedMessageFormclass and return it - In the
index_postfunction modify it to gather the posted data and validate it. If its valid, then set the returned data to be displayed on the webpage. If not valid, send the form which will contain the data plus the error message(s)
Modify the index.html like this:
<%inherit file="layout.html" />
## provide definitions for blocks we want to redefine
<%def name="title()">
Welcome to Pecan!
</%def>
<header>
<h1><img src="/images/logo.png" /></h1>
</header>
<div id="content">
% if not form:
<p>${first_name}, your message is: ${message}</p>
% else:
<form method="POST" action="/">
<div>
${form.message.label}:
${form.message}
% if form.message.errors:
<strong>${form.message.errors[0]}</strong>
% endif
</div>
<div>
${form.first_name.label}:
${form.first_name}
% if form.first_name.errors:
<strong>${form.first_name.errors[0]}</strong>
% endif
</div>
<input type="submit" value="Submit">
</form>
% endif
</div>
Note: πΈ Keep in mind when using the WTForms (http://wtforms.simplecodes.com/) library you can customize the error messages and more. Also, you have multiple validation rules so make sure to catch all the errors which will mean you need a loop rather than the simple example above which grabs the first error item in the list. See the documentation (http://wtforms.simplecodes.com/) for more information.
Run the application:
$ pecan serve config.py
Open a web browser: http://127.0.0.1:8080/
Try the form with valid data and with no data entered. π β¨
π EXAMPLE APPLICATION: SIMPLE AJAX π
This guide will walk you through building a simple Pecan web application that uses AJAX to fetch JSON data from a server. Itβs giving βtiny app, big main character energyβ and weβre gonna build it step by step. β¨
π οΈ Project Setup π οΈ
First, youβll need to install Pecan:
$ pip install pecan
Use Pecanβs basic template support to start a new project:
$ pecan create myajax
$ cd myajax
Install the new project in development mode:
$ python setup.py develop
π Adding JavaScript AJAX Support π
For this project we will need to add jQuery support (http://jquery.com/). To add jQuery go into the templates folder and edit the layout.html file.
Adding jQuery support is easy β we actually only need one line of code:
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
The JavaScript to make the AJAX call is a little more in depth but shouldnβt be unfamiliar if youβve ever worked with jQuery before.
The layout.html file will look like this:
<html>
<head>
<title>${self.title()}</title>
${self.style()}
${self.javascript()}
</head>
<body>
${self.body()}
</body>
</html>
<%def name="title()">
Default Title
</%def>
<%def name="style()">
<link rel="stylesheet" type="text/css" media="screen" href="/css/style.css" />
</%def>
<%def name="javascript()">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<script language="text/javascript" src="/javascript/shared.js"></script>
<script>
function onSuccess(data, status, jqXHR) {
// Use a template or something here instead
// Just for demo purposes
$("#result").html("<div>" +
"<p></p><strong>Project Name: " + data.name + "</strong></p>" +
"<p>Project License: " + data.licensing + "</p>" +
"<p><a href='" + data.repository + "'>Project Repository: " + data.repository + "</a></p>" +
"<p><a href='" + data.documentation + "'>Project Documentation: " + data.documentation + "</a></p>" +
"</div>");
}
function onError(jqXHR, textStatus, errorThrown) {
alert('HTTP Status Code: ' + jqXHR.status + ', ' + errorThrown);
}
$(document).ready(function () {
$("#submit").click(function () {
$.ajax({
url: "/projects/",
data: "id=" + $("#projects").val(),
contentType: 'application/json',
dataType: 'json',
success: onSuccess,
error: onError
});
return false;
});
});
</script>
</%def>
What did we just do? π
- In the
headsection we added jQuery support via the Google CDN (https://developers.google.com/speed/libraries/devguide) - Added JavaScript to make an AJAX call to the server via an HTTP GET passing in the
idof the project to fetch more information on - Once the
onSuccessevent is triggered by the returning data we take that and display it on the web page below the controls
π§ Adding Additional HTML π§
Letβs edit the index.html file next. We will add HTML to support the AJAX interaction between the web page and Pecan. Modify index.html to look like this:
<%inherit file="layout.html" />
<%def name="title()">
Welcome to Pecan!
</%def>
<header>
<h1><img src="/images/logo.png"/></h1>
</header>
<div id="content">
<p>Select a project to get details:</p>
<select id="projects">
<option value="0">OpenStack</option>
<option value="1">Pecan</option>
<option value="2">Stevedore</option>
</select>
<button id="submit" type="submit">Submit</button>
<div id="result"></div>
</div>
What did we just do? π
- Added a dropdown control and submit button for the user to interact with. Users can pick an open source project and get more details on it
π Building the Model with JSON Support π
The HTML and JavaScript work is now taken care of. At this point we can add a model to our project inside of the model folder. Create a file in there called projects.py and add the following to it:
class Project(object):
def __init__(self, name, licensing, repository, documentation):
self.name = name
self.licensing = licensing
self.repository = repository
self.documentation = documentation
def __json__(self):
return dict(
name=self.name,
licensing=self.licensing,
repository=self.repository,
documentation=self.documentation
)
What did we just do? π
- Created a model called
Projectthat can hold project specific data - Added a
__json__method so an instance of theProjectclass can be easily represented as JSON. The controller we will soon build will make use of that JSON capability
Note: There are other ways to return JSON with Pecan, check out JSON Serialization (#jsonify) for more information.
π Working with the Controllers π
We donβt need to do anything major to the root.py file in the controllers folder except to add support for a new controller we will call ProjectsController. Modify the root.py like this:
from pecan import expose
from myajax.controllers.projects import ProjectsController
class RootController(object):
projects = ProjectsController()
@expose(generic=True, template='index.html')
def index(self):
return dict()
What did we just do? π
- Removed some of the initial boilerplate code since we wonβt be using it
- Add support for the upcoming
ProjectsController
The final piece is to add a file called projects.py to the controllers folder. This new file will host the ProjectsController which will listen for incoming AJAX GET calls (in our case) and return the appropriate JSON response.
Add the following code to the projects.py file:
from pecan import expose, response
from pecan.rest import RestController
from myajax.model.projects import Project
class ProjectsController(RestController):
# Note: You would probably store this information in a database
# This is just for simplicity and demonstration purposes
def __init__(self):
self.projects = [
Project(name='OpenStack',
licensing='Apache 2',
repository='http://github.com/openstack',
documentation='http://docs.openstack.org'),
Project(name='Pecan',
licensing='BSD',
repository='http://github.com/pecan/pecan',
documentation='https://pecan.readthedocs.io'),
Project(name='stevedore',
licensing='Apache 2',
repository='http://github.com/dreamhost/pecan',
documentation='http://docs.openstack.org/developer/stevedore/')
]
@expose('json', content_type='application/json')
def get(self, id):
response.status = 200
return self.projects[int(id)]
What did we just do? π
- Created a local class variable called
projectsthat holds three open source projects and their details. Typically this kind of information would probably reside in a database - Added code for the new controller that will listen on the
projectsendpoint and serve back JSON based on theidpassed in from the web page
Run the application:
$ pecan serve config.py
Open a web browser: http://127.0.0.1:8080/
There is something else we could add. What if an id is passed that is not found? A proper HTTP 404 should be sent back. For this we will modify the ProjectsController.
Change the get function to look like this:
@expose('json', content_type='application/json')
def get(self, id):
try:
response.status = 200
return self.projects[int(id)]
except (IndexError, ValueError) as ex:
abort(404)
To test this out we need to pass an invalid id to the ProjectsController. This can be done by going into the index.html and adding an additional option tag with an id value that is outside of 0-2.
<p>Select a project to get details:</p>
<select id="projects">
<option value="0">OpenStack</option>
<option value="1">Pecan</option>
<option value="2">Stevedore</option>
<option value="3">WSME</option>
</select>
You can see that we added WSME to the list and the value is 3.
Run the application:
$ pecan serve config.py
Open a web browser: http://127.0.0.1:8080/
Select WSME from the list. You should see the error dialog box triggered.
Pecanβs source code is well documented using Python docstrings and comments. In addition, we have generated API documentation from the docstrings here:
π PECAN.CORE β PECAN CORE π
Okay bestie, this is the main character module β pecan.core is literally the base module for creating and extending Pecan, and all the core logic for processing HTTP requests and responses lives right here. Like, everything else is kinda just vibing around it. π
π¦ class pecan.core.Pecan(*args, **kw)
Bases: PecanBase
So this is the Pecan application object, fr. Heads up though: you generally create it using pecan.make_app rather than making it by hand β that's the ~intended~ way, iykyk.
It creates a Pecan application instance, which is a WSGI application. Slay.
β¨ Parameters β¨
rootβ a string representing a root controller object (e.g.,"myapp.controller.root.RootController")default_rendererβ the default template rendering engine to use. Defaults tomako.template_pathβ a relative file system path (from the project root) where template files live. Defaults to'templates'.hooksβ a callable which returns a list ofpecan.hooks.PecanHook <#pecan.hooks.PecanHook>custom_renderersβ custom renderer objects, as a dictionary keyed by engine name.extra_template_varsβ any variables to inject into the template namespace automatically. πforce_canonicalβ a boolean indicating if this project should require canonical URLs.guess_content_type_from_extβ a boolean indicating if this project should use the extension in the URL for guessing the content type to return.use_context_localsβ whenTrue,pecan.requestandpecan.responsewill be available as thread-local references.request_clsβ can be used to specify a custompecan.requestobject. Defaults topecan.Request.response_clsβ can be used to specify a custompecan.responseobject. Defaults topecan.Response.
π find_controller(_state)
The main request handler for Pecan applications. That's it, that's the whole thing β it's giving lead role energy. π
πͺ handle_hooks(hooks, *args, **kw)
Processes hooks of the specified type. Cute, organized, on top of things.
Parameters
hook_typeβ the type of hook, includingbefore,after,on_error, andon_route.*argsβ arguments to pass to the hooks.
πΈ class pecan.core.Request(environ, charset=None, unicode_errors=None, decode_param_names=None, **kw)
Bases: Request
(That's literally the whole listing, bestie β no extra lore. π«Ά)
π class pecan.core.Response(body=None, status=None, headerlist=None, app_iter=None, content_type=None, conditional_response=None, charset=<object object>, **kw)
Bases: Response
Same situation β charset defaults to a literal <object object> sentinel, which is iconic in a slightly cursed way. π
π₯ pecan.core.abort(status_code, detail='', headers=None, comment=None, **kw)
Raise an HTTP status code, as specified. Super useful for returning status codes like 401 Unauthorized or 403 Forbidden β like when someone tries to walk into the VIP section without a wristband. π
Parameters
status_codeβ the HTTP status code as an integer.detailβ the message to send along, as a string.headersβ a dictionary of headers to send along with the response.commentβ a comment to include in the response.
π pecan.core.load_app(config, **kwargs)
Used to load a Pecan application and its environment based on passed configuration. Basically the app's whole morning routine. βοΈ
Parameters
configβ can be a dictionary containing configuration, a string which represents a (relative) configuration filename
Returns a pecan.Pecan object. π
π¨ pecan.core.override_template(template, content_type=None)
Call this within a controller to override the template that is used in your response. Like changing your outfit mid-day β the fit is now different, periodt. π
Parameters
templateβ a valid path to a template file, just as you would specify in an@expose.content_typeβ a valid MIME type to use for theresponse.func_closure
π« Not me pointing out that response.func_closure is literally how it's written in the docs. I'm keeping it character for character, no cap β do not come for me.
πͺ pecan.core.redirect(location=None, internal=False, code=None, headers=None, add_slash=False, request=None)
Perform a redirect, either internal or external. An internal redirect performs the redirect server-side, while the external redirect utilizes an HTTP 302 status code. Two very different vibes, same destination. βοΈ
Parameters
locationβ the HTTP location to redirect to.internalβ a boolean indicating whether the redirect should be internal.codeβ the HTTP status code to use for the redirect. Defaults to302.headersβ any HTTP headers to send with the response, as a dictionary.requestβ thepecan.Requestinstance to use.
πΌοΈ pecan.core.render(template, namespace, app=None)
Render the specified template using the Pecan rendering framework with the specified template namespace as a dictionary. Lowkey the lifesaver when you're in a controller and you have no template specified in the @expose. πΈ
Parameters
templateβ the path to your template, as you would specify in@expose.namespaceβ the namespace to use for rendering the template, as a dictionary.appβ the instance ofpecan.Pecanto use
π PECAN.COMMANDS -- PECAN COMMANDS π
Okay bestie, so the pecan.commands module is the one that implements the pecan console script β like, the actual thing that gives you pecan serve and pecan shell command line utilities. It's giving "the behind-the-scenes bestie that makes the CLI happen." π«Ά
class pecan.commands.base.BaseCommand
Bases: BaseCommandParent
A base interface for Pecan commands. You can extend it to support pecan command extensions in individual Pecan projects, e.g.:
$ pecan my-custom-command config.py
# myapp/myapp/custom_command.py
class CustomCommand(pecan.commands.base.BaseCommand):
'''
(First) line of the docstring is used to summarize the command.
'''
arguments = ({
'name': '--extra_arg',
'help': 'an extra command line argument',
'optional': True
})
def run(self, args):
super(SomeCommand, self).run(args)
if args.extra_arg:
pass
class pecan.commands.base.BaseCommandParent
Bases: object (https://docs.python.org/3/library/functions.html#object)
A base interface for Pecan commands. It can be extended to support pecan command extensions in individual Pecan projects, e.g.:
$ pecan my-custom-command config.py
# myapp/myapp/custom_command.py
class CustomCommand(pecan.commands.base.BaseCommand):
'''
(First) line of the docstring is used to summarize the command.
'''
arguments = ({
'name': '--extra_arg',
'help': 'an extra command line argument',
'optional': True
})
def run(self, args):
super(SomeCommand, self).run(args)
if args.extra_arg:
pass
run(args) β to be implemented by subclasses. Like, that's your homework, bestie. π
class pecan.commands.base.CommandManager
Bases: object (https://docs.python.org/3/library/functions.html#object)
Used to discover pecan.command entry points. It's the little scout that finds your commands. π
class pecan.commands.base.CommandRunner
Bases: object (https://docs.python.org/3/library/functions.html#object)
Dispatches pecan command execution requests. So it's the one actually sending the command off to do its thing. π
class pecan.commands.base.HelpfulArgumentParser(prog=None, usage=None, description=None, epilog=None, parents=[], formatter_class=<class 'argparse.HelpFormatter'>, prefix_chars='-', fromfile_prefix_chars=None, argument_default=None, conflict_handler='error', add_help=True, allow_abbrev=True, exit_on_error=True, *, suggest_on_error=False, color=True)
Bases: ArgumentParser (https://docs.python.org/3/library/argparse.html#argparse.ArgumentParser)
error(message: string) β prints a usage message incorporating the message to stderr and exits.
If you override this in a subclass, it should not return β it should either exit or raise an exception. No cap, it literally has to leave or throw. π
π₯οΈ pecan.commands.server -- Pecan Development Server π₯οΈ
Serve command for Pecan. This is the server-side bestie. π»β¨
class pecan.commands.serve.PecanWSGIRequestHandler(*args, **kwargs)
Bases: WSGIRequestHandler (https://docs.python.org/3/library/wsgiref.html#wsgiref.simple_server.WSGIRequestHandler), object (https://docs.python.org/3/library/functions.html#object)
A wsgiref request handler class that allows actual log output depending on the application configuration. It's giving "logs that actually show up." πͺ΅
log_message(format, *args) β overrides the log_message method from the wsgiref server so that normal logging works with whatever configuration the application has been set to.
Levels are inferred from the HTTP status code:4XXcodes are treated as warnings,5XXas errors, and everything else as INFO level. So your logs know the vibe of every response. π₯
class pecan.commands.serve.ServeCommand
Bases: BaseCommand
Serves a Pecan web application. This command serves a Pecan web application using the provided configuration file for the server and application. Very "here's my config, now go live." π
run(args) β to be implemented by subclasses. Again, subclass homework. π
serve(app, conf) β a very simple approach for a WSGI server. Simple, cute, does the job. π
pecan.commands.serve.gunicorn_run()
The gunicorn_pecan command for launching pecan applications. Gunicorn bestie has entered the chat. π¦
π pecan.commands.shell -- Pecan Interactive Shell π
Shell command for Pecan. Interactive shell time, bestie. Let's poke at the app live. π«§
class pecan.commands.shell.BPythonShell
Bases: object (https://docs.python.org/3/library/functions.html#object)
Open an interactive bpython shell with the Pecan app loaded. Fancy shell energy. β¨
classmethod invoke(ns, banner)
Parameters:
nsβ local namespacebannerβ interactive shell startup banner
Embed an interactive bpython shell. She's embedded and ready. π
class pecan.commands.shell.IPythonShell
Bases: object (https://docs.python.org/3/library/functions.html#object)
Open an interactive ipython shell with the Pecan app loaded. IPython girlies rise. π
classmethod invoke(ns, banner)
Parameters:
nsβ local namespacebannerβ interactive shell startup banner
Embed an interactive ipython shell. It tries the InteractiveShellEmbed API first, and falls back on IPShellEmbed for older IPython versions. Like, it's got a backup plan and everything. π«Ά
class pecan.commands.shell.NativePythonShell
Bases: object (https://docs.python.org/3/library/functions.html#object)
Open an interactive python shell with the Pecan app loaded. The classic. No extra flavor needed. ππ©΅
classmethod invoke(ns, banner)
Parameters:
nsβ local namespacebannerβ interactive shell startup banner
Embed an interactive native python shell. Simple. Iconic. Periodt. π
class pecan.commands.shell.ShellCommand
Bases: BaseCommand
Open an interactive shell with the Pecan app loaded. It attempts to invoke the specified python shell flavor (ipython, bpython, etc.). It falls back on the native python shell if the requested flavor variance is not installed.
It's giving "we tried to be fancy, but we'll still be your bestie if you don't have the extras." ππ«Ά
invoke_shell(locs, banner) β invokes the appropriate flavor of the python shell. It falls back on the native python shell if the requested flavor (ipython, bpython, etc.) is not installed.
load_model(config) β loads the model extension module. Gotta load that model, bestie. π§
run(args) β loads the pecan app, prepares the locals, sets the banner, and invokes the python shell.
β¨ PECAN.CONFIGURATION -- PECAN CONFIGURATION ENGINE β¨
The pecan.configuration module provides an implementation of a Python-based configuration engine for Pecan applications. This is where your config lives its best life. ποΈπ
class pecan.configuration.Config(conf_dict=None, filename='')
Bases: object (https://docs.python.org/3/library/functions.html#object)
Base class for Pecan configurations.
Create a Pecan configuration object from a dictionary or a filename. You can hand it a dict or point it at a file β flexible queen. π
Parameters:
conf_dictβ a python dictionary to use for the configuration.filenameβ a filename to use for the configuration.
to_dict(prefix=None) β converts recursively the Config object into a valid dictionary.
Parameters:
prefixβ a string to optionally prefix all key elements in the returned dictionary.
update(conf_dict) β updates this configuration with a dictionary.
Parameters:
conf_dictβ a python dictionary to update this configuration with.
class pecan.configuration.ConfigDict
Bases: dict (https://docs.python.org/3/library/stdtypes.html#dict)
Yep, it's a dict subclass. That's the whole vibe. ποΈ
pecan.configuration.conf_from_dict(conf_dict)
Creates a configuration dictionary from a dictionary.
Parameters:
conf_dictβ the configuration dictionary.
pecan.configuration.conf_from_file(filepath)
Creates a configuration dictionary from a file.
Parameters:
filepathβ the path to the file.
pecan.configuration.get_conf_path_from_env()
If the PECAN_CONFIG environment variable exists and it points to a valid path, it will return that. Otherwise, it will raise a RuntimeError.
So like, it's either giving you the path or giving you an error. No in-between. πβ οΈ
pecan.configuration.initconf()
Initializes the default configuration and exposes it at pecan.configuration.conf, which is also exposed at pecan.conf. Defaults are now available in two cute little spots. π
pecan.configuration.set_config(config, overwrite=False)
Updates the global configuration.
Parameters:
configβ can be a dictionary containing configuration, or a string which represents a (relative) configuration filename.
π PECAN.DECORATORS -- PECAN DECORATORS π
The pecan.decorators module is literally full of useful decorators for building Pecan apps, bestie. Like, itβs giving βlittle helpers that make your controllers behave right.β β¨
π pecan.decorators.accept_noncanonical(func)
Flags a controller method as accepting non-canoncial URLs. Periodt.
π pecan.decorators.after_commit(action)
If youβre using the pecan.hooks TransactionHook, this lets you flag a controller method to perform a callable action after the commit is successfully issued. Obsessed with post-commit clean-up? This is your girl. π
Parameters
actionβ The callable to call after the commit is successfully issued.
π pecan.decorators.after_rollback(action)
Same energy as after_commit, but for rollback: if youβre using the pecan.hooks TransactionHook, this lets you flag a controller method to perform a callable action after the rollback is successfully issued. π
Parameters
actionβ The callable to call after the rollback is successfully issued.
π pecan.decorators.expose(template=None, generic=False, route=None, **kw)
This is the decorator that flags controller methods as βexposedβ for access via HTTP, and it configures that access. Main character decorator behavior, honestly. π
Parameters
templateβ The path to a template, relative to the base template directory. Can also be passed a string representing a special or custom renderer, such as'json'for The JSON Renderer.content_typeβ The content-type to use for this template.genericβ A boolean which flags this as a βgenericβ controller, which uses generic functions based uponfunctools.singledispatchgeneric functions. Allows you to split a single controller into multiple paths based upon HTTP method.routeβ The name of the path segment to match (excluding separator characters, like/). Defaults to the name of the function itself, but this can be used to resolve paths which are not valid Python function names, e.g., if you wanted to route a function tosome-special-path.
π pecan.decorators.transactional(ignore_redirects=True)
If youβre using the pecan.hooks TransactionHook, this lets you flag a controller method or class as being wrapped in a transaction, regardless of HTTP method. Transactional queen behavior. π
Parameters
ignore_redirectsβ Indicates if the hook should ignore redirects for this controller or not.
π¦ PECAN.DEPLOY -- PECAN DEPLOY π¦
The pecan.deploy module includes fixtures to help deploy Pecan applications. Deployment era, but make it cute. π
β¨ pecan.deploy.deploy(config)
Given a config (dictionary of relative filename), returns a configured WSGI app. Thatβs it. Thatβs the whole vibe.
πΈ PECAN.HOOKS -- PECAN HOOKS πΈ
The pecan.hooks module includes support for creating Pecan hooks, which are a simple way to hook into the request processing of requests coming into your application to perform cross-cutting tasks. Think of them as little side quests that run during request time. π§
π class pecan.hooks.HookController
Bases: object
A base class for controllers that would like to specify hooks on their controller methods. Simply create a list of hook objects called __hooks__ as a class attribute of your controller. No cap, thatβs the whole trick. β¨
π class pecan.hooks.PecanHook
Bases: object
A base class for Pecan hooks. Inherit from this class to create your own hooks. Set a priority on a hook by setting the priority attribute for the hook, which defaults to 100. Prioritize your faves accordingly. π
πΊ after(state)
Override this method to create a hook that gets called after the request has been handled by the controller.
Parameters
stateβ The Pecan state object for the current request.
πΊ before(state)
Override this method to create a hook that gets called after routing, but before the request gets passed to your controller.
Parameters
stateβ The Pecan state object for the current request.
πΊ on_error(state, e)
Override this method to create a hook that gets called upon an exception being raised in your controller.
Parameters
stateβ The Pecan state object for the current request.eβ The Exception object that was raised.
πΊ on_route(state)
Override this method to create a hook that gets called upon the start of routing.
Parameters
stateβ The Pecan state object for the current request.
π class pecan.hooks.RequestViewerHook(config=None, writer=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>, terminal=True, headers=True)
Bases: PecanHook
class pecan.hooks.RequestViewerHook(config=None, writer=<_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>, terminal=True, headers=True)
Parameters
configβ A (optional) dictionary that can hold items and/or blacklist keys.writerβ The stream writer to use. Can redirect output to other streams as long as the passed in stream has awritecallable method.terminalβ Outputs to the chosen stream writer (usually the terminal)headersβ Sets values to the X-HTTP headers
Returns some information about what is going on in a single request. It accepts specific items to report on but uses a default list of items when none are passed in. Based on the requested url, items can also be blacklisted. Configuration is flexible, can be passed in (or not) and can contain some or all the keys supported. Very βchoose your own adventure,β honestly. π¦
π items
This key holds the items that this hook will display. When this key is passed only the items in the list will be used. Valid items are any item that the request object holds, by default it uses the following:
pathstatusmethodcontrollerparamshooks
Note: This key should always use a list of items to use.
π blacklist
This key holds items that will be blacklisted based on url. If there is a need to omit urls that start with /javascript, then this key would look like:
'blacklist': ['/javascript']
As many blacklisting items as needed can be contained in the list. The hook will verify that the url is not starting with items in this list to display results, otherwise it will get omitted.
Note: This key should always use a list of items to use.
For more detailed documentation about this hook, please see RequestViewerHook.
πΊ after(state)
Override this method to create a hook that gets called after the request has been handled by the controller.
Parameters
stateβ The Pecan state object for the current request.
πΊ format_hooks(hooks)
Tries to format the hook objects to be more readable. Specific to Pecan (not available in the request object). π
πΊ get_controller(state)
Retrieves the actual controller name from the application. Specific to Pecan (not available in the request object). π
π class pecan.hooks.TransactionHook(start, start_ro, commit, rollback, clear)
Bases: PecanHook
Parameters
startβ A callable that will bind to a writable database and start a transaction.start_roβ A callable that will bind to a readable database.commitβ A callable that will commit the active transaction.rollbackβ A callable that will roll back the active transaction.clearβ A callable that will clear your current context.
A basic framework hook for supporting wrapping requests in transactions. By default, it will wrap all but GET and HEAD requests in a transaction. Override the is_transactional method to define your own rules for what requests should be transactional. Transactional gatekeeping, but make it customizable. π₯
πΊ after(state)
Override this method to create a hook that gets called after the request has been handled by the controller.
Parameters
stateβ The Pecan state object for the current request.
πΊ before(state)
Override this method to create a hook that gets called after routing, but before the request gets passed to your controller.
Parameters
stateβ The Pecan state object for the current request.
πΊ is_transactional(state)
Decide if a request should be wrapped in a transaction, based upon the state of the request. By default, wraps all but GET and HEAD requests in a transaction, along with respecting the transactional decorator from :mod:pecan.decorators.
Parameters
stateβ The Pecan state object for the current request.
πΊ on_error(state, e)
Override this method to create a hook that gets called upon an exception being raised in your controller.
Parameters
stateβ The Pecan state object for the current request.eβ The Exception object that was raised.
πΊ on_route(state)
Override this method to create a hook that gets called upon the start of routing.
Parameters
stateβ The Pecan state object for the current request.
π PECAN.MIDDLEWARE.DEBUG -- PECAN DEBUGGING MIDDLEWARE π
π¦ PECAN.JSONIFY β PECAN JSON SUPPORT π¦
okay so the pecan.jsonify module is literally the reason your JSON rules can exist β it gives you support for creating JSON rules using generic functions. iconic behavior honestly.
β¨ class pecan.jsonify.GenericFunctionJSON(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None) β¨
Bases: GenericJSON
π default(obj)
Converts an object and returns a JSON-friendly structure. it's giving "translator for things JSON doesn't understand."
Parameters
objβ object or structure to be converted into a JSON-ifiable structure
Considers the following special cases in order (order matters bestie, pay attention):
- object has a callable
__json__()attribute defined β returns the result of the call to__json__() dateanddatetimeobjects β returns the object cast tostrDecimalobjects β returns the object cast tofloat- SQLAlchemy objects β returns a copy of the
object.__dict__with internal SQLAlchemy parameters removed - SQLAlchemy
ResultProxyobjects β Casts the iterableResultProxyinto a list of tuples containing the entire resultset data, returns the list in a dictionary along with the resultset"row"count.
Note: {'count': 5, 'rows': [('Ed Jones',), ('Pete Jones',), ('Wendy Williams',), ('Mary Contrary',), ('Fred Smith',)]}
- SQLAlchemy
RowProxyobjects β Casts theRowProxycursor object into a dictionary, probably losing its ordered dictionary behavior in the process but making it JSON-friendly. (rip ordering, but we move π ) webob.multidict.MultiDictobjects β returnsMultiDict.mixed()dictionary, which is guaranteed to be JSON-friendly.
πΈ class pecan.jsonify.GenericJSON(*, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False, indent=None, separators=None, default=None) πΈ
Bases: JSONEncoder <https://docs.python.org/3/library/json.html#json.JSONEncoder>
Generic JSON encoder. Makes several attempts to correctly JSONify requested response objects. it tries, okay, and honestly it slays.
π default(obj)
Converts an object and returns a JSON-friendly structure.
Parameters
objβ object or structure to be converted into a JSON-ifiable structure
Considers the following special cases in order:
- object has a callable
__json__()attribute defined β returns the result of the call to__json__() dateanddatetimeobjects β returns the object cast tostrDecimalobjects β returns the object cast tofloat- SQLAlchemy objects β returns a copy of the
object.__dict__with internal SQLAlchemy parameters removed - SQLAlchemy
ResultProxyobjects β Casts the iterableResultProxyinto a list of tuples containing the entire resultset data, returns the list in a dictionary along with the resultset"row"count.
Note: {'count': 5, 'rows': [('Ed Jones',), ('Pete Jones',), ('Wendy Williams',), ('Mary Contrary',), ('Fred Smith',)]}
- SQLAlchemy
RowProxyobjects β Casts theRowProxycursor object into a dictionary, probably losing its ordered dictionary behavior in the process but making it JSON-friendly. webob.multidict.MultiDictobjects β returnsMultiDict.mixed()dictionary, which is guaranteed to be JSON-friendly.
π PECAN.REST β PECAN REST CONTROLLER π
The pecan.rest module includes support for writing fully RESTful controllers in your Pecan application. yes bestie, fully RESTful.
π class pecan.rest.RestController(*args, **kwargs) π
Bases: object <https://docs.python.org/3/library/functions.html#object>
A base class for REST based controllers. Inherit from this class to implement a REST controller.
RestController implements a set of routing functions which override the default pecan routing with behavior consistent with RESTful routing. This functionality covers navigation to the requested resource controllers, and the appropriate handling of both the common (GET, POST, PUT, DELETE) as well as custom-defined REST action methods.
For more on developing RESTful web applications with Pecan, see Writing RESTful Web Services with Generic Controllers <#rest>. go read it, i'll wait π«Ά
β PECAN.ROUTING β PECAN ROUTING β
The pecan.routing module is the basis for all object-dispatch routing in Pecan. like, the whole vibe check of how your URL finds its home.
π« pecan.routing.find_object(obj, remainder, notfound_handlers, request)
'Walks' the url path in search of an action for which a controller is implemented and returns that controller object along with what's left of the remainder. yes it literally goes on a lil stroll through your url. so brave.
π« pecan.routing.lookup_controller(obj, remainder, request=None)
Traverses the requested url path and returns the appropriate controller object, including default routes.
Handles common errors gracefully. (gracefully!! we love emotional maturity)
π« pecan.routing.route(*args)
This function is used to define an explicit route for a path segment.
You generally only want to use this in situations where your desired path segment is not a valid Python variable/function name. iykyk β Python's parser said no π
For example, if you wanted to be able to route to:
/path/with-dashes/
...the following is invalid Python syntax:
class Controller(object):
with-dashes = SubController()
...so you would instead define the route explicitly:
class Controller(object):
pass
pecan.route(Controller, 'with-dashes', SubController())
slay. problem solved. no cap.
π₯ PECAN.SECURE β PECAN SECURE CONTROLLERS π₯
The pecan.secure module includes a basic framework for building security into your applications. lock the door bestie π
π pecan.secure.secure(func_or_obj, check_permissions_for_obj=None)
This method secures a method or class depending on invocation.
To decorate a method use one argument:
@secure(<check_permissions_method>)
To secure a class, invoke with two arguments:
secure(<obj instance>, <check_permissions_method>)
π pecan.secure.unlocked(func_or_obj)
This method unlocks method or class attribute on a SecureController. Can be used to decorate or wrap an attribute. it's the VIP pass basically.
π class pecan.secure.SecureControllerBase
Bases: object <https://docs.python.org/3/library/functions.html#object>
π§ classmethod check_permissions()
Returns True or False to grant access. Implemented in subclasses of SecureController. so like, you gotta actually write it yourself, this is just the blueprint.
π class pecan.secure.SecureController
Bases: SecureControllerBase
Used to apply security to a controller. Implementations of SecureController should extend the check_permissions method to return a True or False value (depending on whether or not the user has permissions to the controller). gatekeeping, but like, the responsible kind π
π§΅ PECAN.TEMPLATING -- PECAN TEMPLATING π
okay so the pecan.templating module? It's giving support for a whole variety of templating engines, plus the ability to create your own template engines. Obsessed.
class pecan.templating.ExtraNamespace(extras=None)β Bases:object. Extra variables for the template namespace to pass to the renderer as named parameters.- Parameters:
extrasβ dictionary of extra parameters. Defaults to an empty dict. make_ns(ns)β returns the lazily created template namespace.update(d)β updates the extra variable dictionary for the namespace.
- Parameters:
class pecan.templating.GenshiRenderer(path, extra_vars)β Bases:object. Defines the builtin Genshi renderer.render(template_path, namespace)β implements Genshi rendering.
class pecan.templating.JinjaRenderer(path, extra_vars)β Bases:object. Defines the builtin Jinja renderer.render(template_path, namespace)β implements Jinja rendering.
class pecan.templating.JsonRenderer(path, extra_vars)β Bases:object. Defines the builtin JSON renderer.render(template_path, namespace)β implements JSON rendering.
class pecan.templating.KajikiRenderer(path, extra_vars)β Bases:object. Defines the builtin Kajiki renderer.render(template_path, namespace)β implements Kajiki rendering.
class pecan.templating.MakoRenderer(path, extra_vars)β Bases:object. Defines the builtin Mako renderer.render(template_path, namespace)β implements Mako rendering.
class pecan.templating.RendererFactory(custom_renderers=None, extra_vars=None)β Bases:object. Manufactures knownRendererobjects.- Parameters:
custom_renderersβ custom-defined renderers to manufacture.extra_varsβ extra vars for the template namespace.
add_renderers(custom_dict)β adds a custom renderer.- Parameters:
custom_dictβ a dictionary of custom renderers to add.
- Parameters:
available(name)β returns true if queried renderer class is available.- Parameters:
nameβ renderer name.
- Parameters:
get(name, template_path)β returns the renderer object.- Parameters:
nameβ name of the requested renderer.template_pathβ path to the template.
- Parameters:
- Parameters:
pecan.templating.format_genshi_error(exc_value)β implements Genshi renderer error formatting.pecan.templating.format_jinja_error(exc_value)β implements Jinja renderer error formatting.pecan.templating.format_line_context(filename, lineno, context=10)β formats the line context for error rendering.- Parameters:
filenameβ the location of the file, within which the error occurred.linenoβ the offending line number.contextβ number of lines of code to display before and after the offending line.
- Parameters:
pecan.templating.format_mako_error(exc_value)β implements Mako renderer error formatting.
π§ͺ PECAN.TESTING -- PECAN TESTING π«Ά
The pecan.testing module includes fixtures to help test Pecan applications. Bestie, fixtures are the vibe.
pecan.testing.load_test_app(config=None, **kwargs)β used for functional tests where you need to test your literal application and its integration with the framework.- Parameter:
configβ Can be a dictionary containing configuration, a string which represents a (relative) configuration filename orNonewhich will fallback to get thePECAN_CONFIGenv variable. - returns a
pecan.PecanWSGI application wrapped in awebtest.TestAppinstance.
- Parameter:
app = load_test_app('path/to/some/config.py')
resp = app.get('/path/to/some/resource').status_int
assert resp.status_int == 200
resp = app.post('/path/to/some/resource', params={'param': 'value'})
assert resp.status_int == 302
Alternatively you could call load_test_app with no parameters if the environment variable is set:
app = load_test_app()
resp = app.get('/path/to/some/resource').status_int
assert resp.status_int == 200
pecan.testing.reset_global_config()β when tests alter application configurations they can get sticky and pollute other tests that might rely on a pristine configuration. This helper will reset the config by overwriting it withpecan.configuration.DEFAULT. clean-slate era.
π οΈ PECAN.UTIL -- PECAN UTILS π
The pecan.util module includes utility functions for Pecan.
pecan.util.getargspec(method)β drill through layers of decorators attempting to locate the actual argspec for a method.
π 1.8.0
- replaced deprecated
pkg_resourceswithimportlib.metadataandimportlib.resources(https://github.com/pecan/pecan/pull/171) - minimum Python version is now
3.10; removed support for Python3.8and3.9(https://github.com/pecan/pecan/pull/171)
π¦ 1.7.0
- added official support for Python
3.8through3.13(https://github.com/pecan/pecan/pull/165) - extensions pecan imports via
pecan.exthas been removed (https://github.com/pecan/pecan/issues/157)
πΈ 1.6.0
- remove pecan's dependency on
logutils(https://pypi.org/project/logutils/), which is no longer actively maintained (https://github.com/pecan/pecan/issues/159) β as a result, pecan's CLI will no longer print colorized log levels - remove support for Python
3.7and prior
π 1.5.1
- addressed an installation bug caused by a duplicate entry script (
https://github.com/pecan/pecan/pull/142)
π 1.5.0
- pecan no longer has a dependency on
six(https://github.com/pecan/pecan/issues/144) - pecan now supports SQLAlchemy
2.0(https://github.com/pecan/pecan/issues/143) - pecan no longer supports SQLAlchemy
1.3
π 1.4.2
- pecan no longer depends on
webtest(https://github.com/pecan/pecan/issues/139)
π 1.4.1
- add support for Python
3.10 - added trove classifiers for Python
3.6-3.9 - fixed a bug related to
setuptoolsas a dependency (https://github.com/pecan/pecan/pull/122) - fixed a bug that broke pecan when used with certain versions of SQLAlchemy (
https://github.com/pecan/pecan/pulls)
π§ 1.4.0
- pecan now requires
webob >= 1.8 - fixed a bug when parsing certain Accept headers (
https://github.com/Pylons/webob/issues/403) - removed official support for Python
3.5
π₯ 1.3.3
- fixed a bug in
RestControllerthat incorrectly routed certain@securerequests (https://github.com/pecan/pecan/pull/105) - removed official support for Python
3.3
π 1.3.2
- pecan now works with
webob< and >1.8(https://github.com/pecan/pecan/pull/99)
β 1.3.1
- pinned
webobto<1.8due to breaking changes in Accept header parsing (https://github.com/pecan/pecan/pull/97) (https://github.com/Pylons/webob/pull/338)
π 1.3.0
- pecan is now officially supported for Python
3.6 - pecan is no longer supported for Python
2.6
π«Ά 1.2.1
- Reverts a stable API change/regression (in the
1.2release) (https://github.com/pecan/pecan/issues/72). This change will be re-released in a future major version upgrade.
π 1.2
- Added a better error message when an invalid template renderer is specified in
pecan.expose()(https://github.com/pecan/pecan/issues/81). - Pecan controllers that return
Noneare now treated as an HTTP 204 No Content (https://github.com/pecan/pecan/issues/72). - The
methodargument topecan.expose()for generic controllers is no longer optional (https://github.com/pecan/pecan/pull/77).
π 1.1.2
- Fixed a bug where JSON-formatted HTTP response bodies were not making use of pecan's JSON type registration functionality (
http://pecan.readthedocs.io/en/latest/jsonify.html) (https://github.com/pecan/pecan/issues/68). - Updated code and documentation examples to support readthedoc's move from
readthedocs.orgtoreadthedocs.io.
π 1.1.1
- Pecan now officially supports Python
3.5. - Pecan now uses
inspect.signatureinstead ofinspect.getargspecin Python3.5and higher (becauseinspect.getargspecis deprecated in these versions of Python 3). - Fixed a bug that caused
afterhooks to run multiple times whenpecan.redirect(..., internal=True)was used (https://github.com/pecan/pecan/issues/58).
π¦ 1.1.0
pecan.middleware.debug.DebugMiddlewarenow logs exceptions at theERRORlevel (https://github.com/pecan/pecan/pull/56).- Fix a Javascript bug in the default project scaffold (
https://github.com/pecan/pecan/pull/55).
πΈ 1.0.5
- Fix a bug in controller argspec detection when class-based decorators are used (
https://github.com/pecan/pecan/issues/47).
π 1.0.4
- Removed an open file handle leak when pecan renders errors for Jinja2 and Genshi templates (
https://github.com/pecan/pecan/issues/30). - Resolved a bug which caused log output to be duplicated in projects created with
pecan create(https://github.com/pecan/pecan/issues/39).
π 1.0.3
- Fixed a bug in
pecan.hooks.HookControllerfor newer versions ofPython3.4(https://github.com/pecan/pecan/issues/19).
π§΅ 1.0.2
- Fixed an edge case in
pecan.util.getargspecthat caused the incorrect argspec to be returned in certain situations when using Python2.6. - Added a
threading.lockto the file system monitoring inpecan serve --reloadto avoid extraneous server reloads.
β¨ 1.0.1
- Fixed a bug wherein the file extension for URLs with a trailing slash (
file.htmlvsfile.html/) were not correctly guessed, thus resulting in incorrectContent-Typeheaders. - Fixed a subtle bug in
pecan.config.Configurationattribute/item assignment that caused some types of configuration changes to silently fail.
π 1.0.0 π
- π
Replaced pecanβs debugger middleware with an (optional) dependency on the
backlashpackage. If youβre a dev who wants to debug application-level tracebacks interactively,pip install backlashin your development environment. Periodt. - π Fixed a
Content-Type-related bug: when an explicitcontent_typeis specified as an argument topecan.expose(), it is now given precedence over the application-level default renderer. - β¨ Fixed a bug that prevented the usage of certain RFC3986-specified characters in path segments.
- π₯ Fixed a bug in
pecan.abortwhich suppressed the original traceback (and prevented monitoring tools like NewRelic from working as effectively).
πΈ 0.9.0 πΈ
- π Support for Python 3.2 has been dropped.
- π¦ Added a new feature which allows users to specify custom path segments for controllers. This is especially useful for path segments that are not valid Python identifiers (such as path segments that include certain punctuation characters, like
/some/~path~/). - π Added a new configuration option,
app.debugger, which allows developers to specify an alternative debugger topdb(e.g.,ipdb) when performing interactive debugging with pecanβsDebugMiddleware. - π Changed new quickstart pecan projects to default the pecan log level to
DEBUGfor development. - π§ Fixed a bug that prevented
staticmethodsfrom being used as controllers. - π Fixed a decoding bug in the way pecan handles certain quoted URL path segments and query strings.
- β¨ Fixed several bugs in the way pecan handles Unicode path segments (for example, now you can define pecan routes that contain emoji characters).
- π Fixed several bugs in
RestControllerthat caused it to return HTTP 404 Not Found rather than HTTP 405 Method Not Allowed. Additionally,RestControllernow returns validAllowheaders when HTTP 405 Method Not Allowed is returned. - π Fixed a bug which allowed special pecan methods (
_route,_lookup,_default) to be marked as generic REST methods. - π«Ά Added more emphasis in pecanβs documentation to the need for
debug=Falsein production deployments.
π 0.8.3 π
- π Changed pecan to more gracefully handle a few odd request encoding edge cases. Now pecan applications respond with an HTTP 400 (rather than an uncaught
UnicodeDecodeError, resulting in an HTTP 500) when:- HTTP POST requests are composed of non-Unicode data
- Request paths contain invalid percent-encoded characters, e.g.,
/some/path/%aa/
- π Improved verbosity for import-related errors in pecan configuration files, especially those involving relative imports.
π¦ 0.8.2 π¦
- π Fixes a bug that breaks support for multi-value query string variables (e.g.,
?check=a&check=b).
π 0.8.1 π
- β¨ Improved detection of infinite recursion for
PecanHookandpypy. This fixes a bug discovered in pecan + pypy that could result in infinite recursion when using thePecanHookmetaclass. - πΈ Fixed a bug that prevented
@exposedcontrollers from using@staticmethod. - π Fixed a minor bug in the controller argument calculation.
π 0.8.0 π
- π For HTTP POSTs, map JSON request bodies to controller keyword arguments.
- β¨ Improved argspec detection and leniency for wrapped controllers.
- π― When path arguments are incorrect for
RestController, return HTTP 404, not 400. - π§ When detecting non-content for HTTP 204, properly catch
UnicodeDecodeError. - π Fixed a routing bug for generic subcontrollers.
- π₯ Fixed a bug in generic function handling when context locals are disabled.
- π Fixed a bug that mixes up argument order for generic functions.
- π Removed
assertfor flow control; it can be optimized away withpython -O.
β 0.7.0 β
- π Fixed an edge case in
RestControllerrouting which should have returned an HTTP 400 but was instead raising an exception (and thus, HTTP 500). - π Fixed an incorrect root logger configuration for quickstarted pecan projects.
- β¨ Added
pecan.state.arguments, a new feature for inspecting controller call arguments. - π
Fixed an infinite recursion error in
PecanHookapplication. Subclassing bothrest.RestControllerandhooks.HookControllerresulted in an infinite recursion error in hook application (which prevented applications from starting). - πΈ Pecanβs tests are now included in its source distribution.
π«Ά 0.6.1 π«Ά
- π Fixed a bug which causes pecan to mistakenly return HTTP 204 for non-empty response bodies.
π 0.6.0 π
- β¨ Added support for disabling the
pecan.requestandpecan.responsethreadlocals at the WSGI application level in favor of explicit reference passing. For more information, seeContext/Thread-Locals vs. Explicit Argument Passing <#contextlocals>. - π Added better support for hook composition via subclassing and mixins. For more information, see
Attaching Hooks <#attaching-hooks>. - π¦ Added support for specifying custom request and response implementations at the WSGI application level for people who want to extend the functionality provided by the base classes in
webob. - π Pecan controllers may now return an explicit
webob.Responseinstance to short-circuit Pecanβs template rendering and serialization. - π
For generic methods that return HTTP 405, pecan now generates an
Allowheader to communicate acceptable methods to the client. - π Fixed a bug in adherence to RFC2616: if an exposed method returns no response body (or namespace), pecan will now enforce an HTTP 204 response (instead of HTTP 200).
- π₯ Fixed a bug in adherence to RFC2616: when pecan responds with HTTP 204 or HTTP 304, the
Content-Typeheader is automatically stripped (because these types of HTTP responses do not contain body content). - β¨ Fixed a bug: now when clients request JSON via an
Acceptheader, webob HTTP exceptions are serialized as JSON, not their native HTML representation. - π Fixed a bug that broke applications which specified
default_renderer = json.
π 0.5.0 π
- π This release adds formal support for pypy.
- π Added colored request logging to the
pecan servecommand. - π Added a scaffold for easily generating a basic REST API.
- β¨ Added the ability to pass arbitrary keyword arguments to
pecan.testing.load_test_app. - π Fixed a recursion-related bug in the error document middleware.
- π
Fixed a bug in the
gunicorn_pecancommand that causedthreading.localdata to leak between eventlet/gevent green threads. - πΈ Improved documentation through fixes and narrative tutorials for sample pecan applications.
π¦ 0.4.5 π¦
- π― Fixed a trailing slash bug for
RestControllers that have a_lookupmethod. - β¨ Cleaned up the WSGI app reference from the threadlocal state on every request (to avoid potential memory leaks, especially when testing).
- π Improved pecan documentation and corrected intersphinx references.
- π pecan supports Python 3.4.
π 0.4.4 π
- π Removed memoization of certain controller attributes, which can lead to a memory leak in dynamic controller lookups.
π 0.4.3 π
- π Fixed several bugs for
RestController. - π₯ Fixed a bug in security handling for generic controllers.
- β¨ Resolved a bug in
_defaulthandlers used inRestController. - π¦ Persist
pecan.request.contextacross internal redirects.
π 0.4.2 π
- π Remove a routing optimization that breaks the WSME pecan plugin.
πΈ 0.4.1 πΈ
- π Moved the project to StackForge infrastructure
http://docs.openstack.org/infra/system-config/stackforge.html, including Gerrit code review, Jenkins continuous integration, and GitHub mirroring. - π Added a pecan plugin for the popular uwsgi server
https://uwsgi-docs.readthedocs.io. - β¨ Replaced the
simplegenericdependency with the newfunctools.singledispatchfunction in preparation for Python 3.4 support. - π₯ Optimized pecanβs core dispatch routing for notably faster response times.
π§ 0.3.2 π§
- π
Made some changes to simplify how
pecan.conf.appis passed to new apps. - π Fixed a routing bug for certain
_lookupcontroller configurations. - πΈ Improved documentation for handling file uploads.
- π Deprecated the
pecan.conf.requestviewerconfiguration option.
β¨ 0.3.1 β¨
- π
on_errorhooks can now return a Pecan Response objects. - π¦ Minor documentation and release tooling updates.
π 0.3.0 π
- π Pecan now supports Python 2.6, 2.7, 3.2, and 3.3.
π 0.2.4 π
- β¨ Add support for
_lookupmethods as a fallback inRestController. - π A variety of improvements to project documentation.
π― 0.2.3 π―
- π₯ Add a variety of optimizations to
pecan.corethat improve request handling time by approximately 30% for simple object dispatch routing. - π Store exceptions raised by
abortin the WSGI environ so they can be accessed later in the request handling (e.g., by other middleware or pecan hooks). - π Make
TransactionHookmore robust so that it isnβt as susceptible to failure when exceptions occur in other pecan hooks within a request. - π Rearrange quickstart verbiage so users donβt miss a necessary step.
π¦ 0.2.2 π¦
- β¨ Unobfuscate syntax highlighting JavaScript for debian packaging.
- πΈ Extract the scaffold-building tests into
tox. - π
Add support for specifying a pecan configuration file via the
PECAN_CONFIGenvironment variable. - π Fix a bug in
DELETEmethods in two (or more) nestedRestControllers. - π― Add documentation for returning specific HTTP status codes.
π 0.2.1 π
- π Include a license, readme, and
requirements.txtin distributions. - β¨ Improve inspection with
dir()forpecan.requestandpecan.response. - π Fix a bug which prevented pecan applications from being mounted at WSGI virtual paths.
π 0.2.0 π
- π Update base project scaffolding tests to be more repeatable.
- π Add an application-level configuration option to disable content-type guessing by URL.
- π Fix the wrong test dependency on Jinja, itβs Jinja2.
- π₯ Fix a routing-related bug in
RestController. Fixes #156. - β¨ Add an explicit
CONTRIBUTING.rstdocument. - π Improve visibility of deployment-related docs.
- π Add support for a
gunicorn_pecanconsole script. - π Remove and annotate a few unused (and py26 alternative) imports.
- π Bug fix: donβt strip a dotted extension from the path unless it has a matching mimetype.
- π§ Add a test to the scaffold project buildout that ensures
pep8passes. - π Fix misleading output for
$ pecan --version.
π 0.2.0B π
okay bestie, tiny lil patch release but she ATE, here's the receipts:
- π Fixed a bug in
SecureController. resolves #131 β periodt, that issue is CLOSED. - π§ Extracted the debug middleware static file dependencies into physical files. yes ma'am, real files now, no cap.
- π©· Improved a test that could fail because of a race condition β lowkey the most relatable bug category tbh π
- π Improved the documentation about the configuration format and about
app.py. read it, it's giving helpful π - β¨ Added support for content type detection via HTTP
Acceptheaders β she's multilingual now fr fr - π οΈ Corrected the source installation instructions in the
READMEβ no more following a broken tutorial, we love to see it - πΈ Fixed an incorrect code example in the Hooks documentation. iykyk, sneaky lil code examples.
- π©Ή docs: fixed a minor typo in the
*argsRouting example. one (1) typo. annihilated. π
π LICENSE π
The Pecan framework and the documentation is BSD Licensed β here's the whole thing verbatim, bestie, because licenses are not a vibe you get to paraphrase π«Ά:
Copyright (c) <2010>, Pecan Framework
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the <organization> nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
π that ALL CAPS disclaimer part? that's the "we are not responsible if your prod goes down at 3am" paragraph. it's iconic, it's load-bearing, do not delete it.
π Author π
Jonathan LaCour β the main character who wrote this. respectfully obsessed. π
π Copyright π
2010, Jonathan LaCour
okay that's the whole page, bestie, you made it to the end, highkey proud of you π₯Ήπ this was PECAN(1) β source Pecan version 1.8.0, dated Mar 20, 2026 β go forth, run your commands, and slay β¨π¦π
π¦ where this page comes from
- arch rolling: python-pecan 1.8.0-1 Β· BSD-3-Clause
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash-1017 on : a language model's rewrite of the original, under its terms