pax_global_header 0000666 0000000 0000000 00000000064 14454530445 0014522 g ustar 00root root 0000000 0000000 52 comment=fbd720c7cc7f035bb4436ffea869d7d7d8a09792
pecan-1.5.1/ 0000775 0000000 0000000 00000000000 14454530445 0012614 5 ustar 00root root 0000000 0000000 pecan-1.5.1/.coveragerc 0000664 0000000 0000000 00000000023 14454530445 0014730 0 ustar 00root root 0000000 0000000 [run]
source=pecan
pecan-1.5.1/.github/ 0000775 0000000 0000000 00000000000 14454530445 0014154 5 ustar 00root root 0000000 0000000 pecan-1.5.1/.github/workflows/ 0000775 0000000 0000000 00000000000 14454530445 0016211 5 ustar 00root root 0000000 0000000 pecan-1.5.1/.github/workflows/run-tests.yml 0000664 0000000 0000000 00000001475 14454530445 0020707 0 ustar 00root root 0000000 0000000 name: run pull request CI
on:
schedule:
- cron: '0 12 * * *' # daily ~8am est
workflow_dispatch:
pull_request:
jobs:
unit-tests:
runs-on: "ubuntu-20.04" # see: https://github.com/actions/setup-python/issues/544
timeout-minutes: 10
strategy:
matrix:
python: [3.6, 3.7, 3.8, 3.9, "3.10", "3.11"]
command: ["py", "scaffolds", "sqlalchemy-1.4", "sqlalchemy-2"]
exclude:
# SQLAlchemy2 requires >= py3.7
- python: 3.6
command: "sqlalchemy-2"
steps:
- uses: actions/checkout@v2
- name: setup Python
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python }}
- name: install tox
run: pip install tox
- name: run tox unit tests
run: tox -e ${{ matrix.command }}
pecan-1.5.1/.gitignore 0000664 0000000 0000000 00000000324 14454530445 0014603 0 ustar 00root root 0000000 0000000 # From GitHub's .gitignore template collection
*.py[co]
# Packages
*.egg
*.egg-info
dist
build
eggs
develop-eggs
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
htmlcov
.DS_Store
pecan-1.5.1/.readthedocs.yaml 0000664 0000000 0000000 00000000647 14454530445 0016052 0 ustar 00root root 0000000 0000000 # .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/source/conf.py
python:
install:
- method: pip
path: .
pecan-1.5.1/AUTHORS 0000664 0000000 0000000 00000000406 14454530445 0013664 0 ustar 00root root 0000000 0000000 Pecan is written by various contributors (by date of contribution):
Jonathan LaCour
Alfredo Deza
Mark McClain
Ryan Petrello
Yoann Roman
John Anderson
Jeremy Jones
Benjamin W. Smith
Pete Chudykowski
Mike Perez
Justin Barber
Wesley Spikes
Steven Berler
Chad Lung
pecan-1.5.1/CONTRIBUTING.rst 0000664 0000000 0000000 00000001142 14454530445 0015253 0 ustar 00root root 0000000 0000000 Contributing to Pecan
---------------------
Pecan uses GitHub pull requests for bug fixes and feature additions.
All contributions must:
* Include accompanying tests.
* Include narrative and API documentation if new features are added.
* Be (generally) compliant with `PEP8
`_.
* Not break the test or build. Before submitting a review, ``$ pip
install tox && tox`` from your source to ensure that all tests still pass
across multiple versions of Python.
Bugs should be filed on GitHub:
https://github.com/pecan/pecan/issues/new
pecan-1.5.1/LICENSE 0000664 0000000 0000000 00000002741 14454530445 0013625 0 ustar 00root root 0000000 0000000 Copyright (c) <2011>, Jonathan LaCour
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 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 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.
pecan-1.5.1/MANIFEST.in 0000664 0000000 0000000 00000000437 14454530445 0014356 0 ustar 00root root 0000000 0000000 recursive-include pecan/scaffolds/base *
include pecan/scaffolds/base/*
recursive-include pecan/scaffolds/rest-api *
include pecan/scaffolds/rest-api/*
include pecan/middleware/resources/*
include LICENSE README.rst requirements.txt test-requirements.txt
recursive-include pecan/tests *
pecan-1.5.1/README.rst 0000664 0000000 0000000 00000002327 14454530445 0014307 0 ustar 00root root 0000000 0000000 Pecan
=====
A WSGI object-dispatching web framework, designed to be lean and fast with few
dependencies.
.. image:: https://badge.fury.io/py/pecan.png
:target: https://pypi.python.org/pypi/pecan/
:alt: Latest PyPI version
Installing
----------
::
$ pip install pecan
...or, for the latest (unstable) tip::
$ git clone https://github.com/pecan/pecan.git
$ cd pecan && python setup.py install
Running Tests
-------------
::
$ python setup.py test
...or, to run all tests across all supported environments::
$ pip install tox && tox
Viewing Documentation
---------------------
`Available online `_, or to build manually::
$ cd docs && make html
$ open docs/build/html/index.html
...or::
$ cd docs && make man
$ man docs/build/man/pecan.1
Contributing
------------
For information on contributing to Pecan, please read our `Contributing
Guidelines `_.
Bugs should be reported at:
https://github.com/pecan/pecan/issues/new
Additional Help/Support
-----------------------
Most Pecan interaction is done via the `pecan-dev Mailing List
`_.
pecan-1.5.1/docs/ 0000775 0000000 0000000 00000000000 14454530445 0013544 5 ustar 00root root 0000000 0000000 pecan-1.5.1/docs/Makefile 0000664 0000000 0000000 00000010756 14454530445 0015215 0 ustar 00root root 0000000 0000000 # Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
clean:
-rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Pecan.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Pecan.qhc"
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Pecan"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Pecan"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
make -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
pecan-1.5.1/docs/source/ 0000775 0000000 0000000 00000000000 14454530445 0015044 5 ustar 00root root 0000000 0000000 pecan-1.5.1/docs/source/changes.rst 0000664 0000000 0000000 00000035247 14454530445 0017221 0 ustar 00root root 0000000 0000000 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 setuptools as 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 RestController that incorrectly routed certain @secure
requests (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 webob to <1.8 due 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.2 release)
(https://github.com/pecan/pecan/issues/72). This change will 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 `None` are now treated as an `HTTP 204 No
Content` (https://github.com/pecan/pecan/issues/72).
* The `method` argument to `pecan.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.org` to `readthedocs.io`.
1.1.1
=====
* Pecan now officially supports Python 3.5.
* Pecan now uses `inspect.signature` instead of `inspect.getargspec` in
Python 3.5 and higher (because `inspect.getargspec` is deprecated in these
versions of Python 3).
* Fixed a bug that caused "after" hooks to run multiple times when
`pecan.redirect(..., internal=True)` was used
(https://github.com/pecan/pecan/issues/58).
1.1.0
=====
* `pecan.middleware.debug.DebugMiddleware` now logs exceptions at the ERROR
level (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.HookController` for newer versions of Python3.4
(https://github.com/pecan/pecan/issues/19).
1.0.2
=====
* Fixed an edge case in `pecan.util.getargspec` that caused the incorrect
argspec to be returned in certain situations when using Python 2.6.
* Added a `threading.lock` to the file system monitoring in `pecan serve
--reload` to avoid extraneous server reloads.
1.0.1
=====
* Fixed a bug wherein the file extension for URLs with a trailing slash
(`file.html` vs `file.html/`) were not correctly guessed, thus resulting in
incorrect Content-Type headers.
* Fixed a subtle bug in `pecan.config.Configuration` attribute/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
`backlash` package. Developers who want to debug application-level
tracebacks interactively should `pip install backlash` in their development
environment.
* Fixed a Content-Type related bug: when an explicit content_type is specified
as an argument to `pecan.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.abort` which 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 to `pdb` (e.g., `ipdb`) when performing
interactive debugging with pecan's `DebugMiddleware`.
* Changed new quickstart pecan projects to default the `pecan` log level to
`DEBUG` for development.
* Fixed a bug that prevented `staticmethods` from 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 RestController that caused it to return `HTTP 404 Not
Found` rather than `HTTP 405 Method Not Allowed`. Additionally,
RestController now returns valid `Allow` headers 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=False` in
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 PecanHook and pypy. This fixes
a bug discovered in pecan + pypy that could result in infinite recursion when
using the PecanHook metaclass.
* Fixed a bug that prevented @exposed controllers 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 `assert` for flow control; it can be optimized away with `python -O`.
0.7.0
=====
* Fixed an edge case in RestController routing 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 PecanHook application. Subclassing both
`rest.RestController` and `hooks.HookController` resulted 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.request` and `pecan.response`
threadlocals at the WSGI application level in favor of explicit reference
passing. For more information, see :ref:`contextlocals`.
* Added better support for hook composition via subclassing and mixins. For
more information, see :ref:`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.Response` instance to
short-circuit Pecan's template rendering and serialization.
* For generic methods that return HTTP 405, pecan now generates an `Allow`
header 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-Type` header is automatically stripped (because these
types of HTTP responses do not contain body content).
* Fixed a bug: now when clients request JSON via an `Accept` header, `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 serve` command.
* 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_pecan` command that caused `threading.local`
data 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 `RestController`s that have a `_lookup` method.
* 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 `_default` handlers used in `RestController`.
* Persist `pecan.request.context` across internal redirects.
0.4.2
=====
* Remove a routing optimization that breaks the WSME pecan plugin.
0.4.1
=====
* Moved the project to `StackForge infrastructure
`_, including Gerrit code review,
Jenkins continuous integration, and GitHub mirroring.
* Added a pecan plugin for the popular `uwsgi server
`_.
* Replaced the ``simplegeneric`` dependency with the new
``functools.singledispatch`` function 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.app`` is passed to new apps.
* Fixed a routing bug for certain ``_lookup`` controller configurations.
* Improved documentation for handling file uploads.
* Deprecated the ``pecan.conf.requestviewer`` configuration option.
0.3.1
=====
* ``on_error`` hooks 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 ``_lookup`` methods as a fallback in RestController.
* A variety of improvements to project documentation.
0.2.3
=====
* Add a variety of optimizations to ``pecan.core`` that improve request
handling time by approximately 30% for simple object dispatch routing.
* Store exceptions raised by ``abort`` in the WSGI environ so they can be
accessed later in the request handling (e.g., by other middleware or pecan
hooks).
* Make TransactionHook more 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_CONFIG``
environment variable.
* Fix a bug in ``DELETE`` methods in two (or more) nested ``RestControllers``.
* Add documentation for returning specific HTTP status codes.
0.2.1
=====
* Include a license, readme, and ``requirements.txt`` in distributions.
* Improve inspection with ``dir()`` for ``pecan.request`` and ``pecan.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.rst`` document.
* Improve visibility of deployment-related docs.
* Add support for a ``gunicorn_pecan`` console 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 pep8 passes.
* Fix misleading output for ``$ pecan --version``.
0.2.0b
======
* Fix a bug in ``SecureController``. Resolves #131.
* Extract debug middleware static file dependencies into physical files.
* Improve a test that can fail due to a race condition.
* Improve documentation about configation format and ``app.py``.
* Add support for content type detection via HTTP Accept headers.
* Correct source installation instructions in ``README``.
* Fix an incorrect code example in the Hooks documentation.
* docs: Fix minor typo in ``*args`` Routing example.
pecan-1.5.1/docs/source/commands.rst 0000664 0000000 0000000 00000020134 14454530445 0017377 0 ustar 00root root 0000000 0000000 .. _commands:
Command Line Pecan
==================
Any Pecan application can be controlled and inspected from the command
line using the built-in :command:`pecan` command. The usage examples
of :command:`pecan` in this document are intended to be invoked from
your project's root directory.
Serving a Pecan App For Development
-----------------------------------
Pecan comes bundled with a lightweight WSGI development server based on
Python's :py:mod:`wsgiref.simple_server` module.
Serving your Pecan app is as simple as invoking the ``pecan serve`` command::
$ 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 visiting it in your browser.
The server ``host`` and ``port`` in your configuration file can be changed as
described in :ref:`server_configuration`.
.. include:: reload.rst
:start-after: #reload
The Interactive Shell
---------------------
Pecan applications also come with an interactive Python shell which can be used
to execute expressions in an environment very similar to the one your
application runs in. To invoke an interactive shell, 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
>>> app.get('/')
<200 OK text/html body='\n ...\n\n'/936>
Press ``Ctrl-D`` to exit the interactive shell (or ``Ctrl-Z`` on Windows).
Using an Alternative Shell
++++++++++++++++++++++++++
``pecan shell`` has optional support for the `IPython `_
and `bpython `_ alternative shells, each of
which can be specified with the ``--shell`` flag (or its abbreviated alias,
``-s``), e.g.,
::
$ pecan shell --shell=ipython config.py
$ pecan shell -s bpython config.py
.. _env_config:
Configuration from an environment variable
------------------------------------------
In all the examples shown, you will see that the :command:`pecan` commands
accepted a file path to the configuration file. An alternative to this is to
specify the configuration file in an environment variable (:envvar:`PECAN_CONFIG`).
This is completely optional; if a file path is passed in explicitly, Pecan will
honor that before looking for an environment variable.
For example, to serve a Pecan application, a variable could be exported and
subsequently be 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
Note that the path needs to reference a valid pecan configuration file,
otherwise the command will error out with a message indicating that
the path is invalid (for example, if a directory is passed in).
If :envvar:`PECAN_CONFIG` is not set and no configuration is passed in, the command
will error out because it will not be able to locate a configuration file.
Extending ``pecan`` with Custom Commands
----------------------------------------
While the commands packaged with Pecan are useful, the real utility of its
command line toolset lies in its extensibility. It's convenient to be able to
write a Python script that can work "in a Pecan environment" with access to
things like your application's parsed configuration file or a simulated
instance of your application itself (like the one provided in the ``pecan
shell`` command).
Writing a Custom Pecan Command
++++++++++++++++++++++++++++++
As an example, let's create a command that can be used to issue a simulated
HTTP GET to your application and print the result. Its invocation from the
command line might look something like this::
$ pecan wget config.py /path/to/some/resource
Let's say you have a distribution with a package in it named ``myapp``, and
that within this package is 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, we're subclassing :class:`~pecan.commands.base.BaseCommand` and extending
the :func:`~pecan.commands.base.BaseCommandParent.run` method to:
* Load a Pecan application - :func:`~pecan.core.load_app`
* Wrap it in a fake WGSI environment - :class:`~webtest.app.TestApp`
* Issue an HTTP GET request against it - :meth:`~webtest.app.TestApp.get`
Defining Custom Arguments
,,,,,,,,,,,,,,,,,,,,,,,,,
The :attr:`arguments` class attribute is used to define command line arguments
specific to your custom command. You'll notice in this example that we're
*adding* to the arguments list provided by :class:`~pecan.commands.base.BaseCommand`
(which already provides an argument for the ``config_file``), rather
than overriding it entirely.
The format of the :attr:`arguments` class attribute is a :class:`tuple` of
dictionaries, with each dictionary representing an argument definition in the
same format accepted by Python's :py:mod:`argparse` module (more specifically,
:meth:`~argparse.ArgumentParser.add_argument`). By providing a list of
arguments in this format, the :command:`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
Additionally, you'll notice that the first line of the docstring from
:class:`GetCommand` -- ``Issues a (simulated) HTTP GET and returns the
request body`` -- is automatically used to describe the :command:`wget`
command in the output for ``$ pecan -h``. Following this convention
allows you to easily integrate a summary for your command into the
Pecan command line tool.
Registering a Custom Command
++++++++++++++++++++++++++++
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 :func:`~setuptools.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 :func:`~setuptools.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 this, 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
pecan-1.5.1/docs/source/conf.py 0000664 0000000 0000000 00000016360 14454530445 0016351 0 ustar 00root root 0000000 0000000 # -*- coding: utf-8 -*-
#
# Pecan documentation build configuration file, created by
# sphinx-quickstart on Sat Oct 9 14:41:27 2010.
#
# This file is execfile()d w/ the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import pkg_resources
import sys
import os
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.append(os.path.abspath('.'))
# -- General configuration ----------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
intersphinx_mapping = {
'python': ('http://docs.python.org', None),
'webob': ('http://docs.webob.org/en/latest', None),
'webtest': ('https://webtest.readthedocs.io/en/latest/', None),
'beaker': ('https://beaker.readthedocs.io/en/latest/', None),
'paste': ('http://pythonpaste.org', None),
}
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Pecan'
copyright = u'2010, Jonathan LaCour'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
dist = pkg_resources.get_distribution('pecan')
version = release = dist.version
# The full version, including alpha/beta/rc tags.
#release = '0.3.0'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# The reST default role (used for this markup: `text`) to use for documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# -- Options for HTML output --------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'nature'
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['static']
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'Pecandoc'
# -- Options for LaTeX output -------------------------------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual])
latex_documents = [
('index', 'Pecan.tex', u'Pecan Documentation',
u'Jonathan LaCour', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output -------------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'pecan', u'Pecan Documentation',
[u'Jonathan LaCour'], 1)
]
pecan-1.5.1/docs/source/configuration.rst 0000664 0000000 0000000 00000013553 14454530445 0020454 0 ustar 00root root 0000000 0000000 .. _configuration:
Configuring Pecan Applications
==============================
Pecan is very easy to configure. As long as you follow certain conventions,
using, setting and dealing with configuration should be very intuitive.
Pecan configuration files are pure Python. Each "section" of the
configuration is a dictionary assigned to a variable name in the
configuration module.
Default Values
---------------
Below is the complete list of default values the framework uses::
server = {
'port' : '8080',
'host' : '0.0.0.0'
}
app = {
'root' : None,
'modules' : [],
'static_root' : 'public',
'template_path' : ''
}
.. _application_configuration:
Application Configuration
-------------------------
The ``app`` configuration values are used by Pecan to wrap your
application into a valid `WSGI app
`_. The ``app`` configuration
is specific to your application, and includes values like the root
controller class location.
A typical application configuration might look like this::
app = {
'root' : 'project.controllers.root.RootController',
'modules' : ['project'],
'static_root' : '%(confdir)s/public',
'template_path' : '%(confdir)s/project/templates',
'debug' : True
}
Let's look at each value and what it means:
**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 an ``app.setup_app`` function which is called to create the
WSGI app. In other words, this package should be where your
``app.py`` file is located, and this file should contain a
``setup_app`` function.
**root**
The root controller of your application. Remember to provide 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.
**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.
.. warning::
Make sure **debug** is *always* set to ``False`` in production environments.
.. seealso::
* :ref:`app_template`
.. _server_configuration:
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). Put these settings in
their own blocks in your configuration file.
::
twitter = {
'api_key' : 'FOO',
'api_secret' : 'SECRET'
}
.. _accessibility:
Accessing Configuration at Runtime
----------------------------------
You can access any configuration value at runtime via :py:mod:`pecan.conf`.
This includes custom, application, and server-specific values.
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 :py:mod:`pecan.conf` as::
>>> from pecan import conf
>>> conf.administrator
'foo_bar_user'
Dictionary Conversion
---------------------
In certain situations you might want to deal with keys and values, but in strict
dictionary form. The :class:`~pecan.configuration.Config` object has a helper
method for this purpose that will return a dictionary representation of the
configuration, including nested values.
Below is a representation of how you can access the
:meth:`~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
-------------------------
:func:`~pecan.configuration.Config.to_dict` allows you to pass an optional
string argument if you need to prefix the keys in the returned dictionary.
::
>>> 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 ``()``. 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.
::
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)
pecan-1.5.1/docs/source/contextlocals.rst 0000664 0000000 0000000 00000004333 14454530445 0020463 0 ustar 00root root 0000000 0000000 .. _contextlocals:
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.
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
:func:`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
:func:`threading.local` to behave in a greenlet-local manner.
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.
pecan-1.5.1/docs/source/databases.rst 0000664 0000000 0000000 00000017650 14454530445 0017536 0 ustar 00root root 0000000 0000000 .. _databases:
Working with Databases, Transactions, and ORM's
===============================================
Pecan provides no opinionated support for working with databases, but
it's easy to hook into your ORM of choice. This article details best
practices for integrating the popular Python ORM, SQLAlchemy_, into
your Pecan project.
.. _SQLAlchemy: http://sqlalchemy.org
.. _init_model:
``init_model`` and Preparing Your Model
---------------------------------------
Pecan's default quickstart project includes an empty stub directory
for implementing your model as you see fit.
::
.
└── test_project
├── app.py
├── __init__.py
├── controllers
├── model
│ ├── __init__.py
└── templates
By default, this module contains a special method, :func:`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
The purpose of this method is to determine bindings from your
configuration file and create necessary engines, pools,
etc. according to your ORM or database toolkit of choice.
Additionally, your project's :py:mod:`model` module can be used to define
functions for common binding operations, such as starting
transactions, committing or rolling back work, and clearing a session.
This is also the location in your project where object and relation
definitions should be defined. Here's what a sample Pecan
configuration 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 a basic model implementation that can be used to configure and
bind using 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
------------------------------
There are several approaches to wrapping your application's requests
with calls to appropriate model function calls. One approach is WSGI
middleware. We also recommend Pecan :ref:`hooks`. Pecan comes with
:class:`~pecan.hooks.TransactionHook`, a hook which can be used to wrap
requests in database transactions for you. To use it, simply include it in
your project's ``app.py`` file and pass it a set of 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 above example, on HTTP ``POST``, ``PUT``, and ``DELETE``
requests, :class:`~pecan.hooks.TransactionHook` takes care of the transaction
automatically by following these rules:
#. Before controller routing has been determined, :func:`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, :func:`model.rollback` is called and the original
exception is re-raised. This allows you to rollback your database
transaction to avoid committing work when exceptions occur in your
application code.
#. If the controller returns successfully, :func:`model.commit` and
:func:`model.clear` are called.
On idempotent operations (like HTTP ``GET`` and ``HEAD`` requests),
:class:`~pecan.hooks.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 that there is a useful :func:`~pecan.decorators.after_commit`
decorator provided in :ref:`pecan_decorators`.
Splitting Reads and Writes
--------------------------
Employing the strategy above with :class:`~pecan.hooks.TransactionHook` makes
it very simple to split database reads and writes based upon HTTP methods
(i.e., GET/HEAD requests are read-only and would potentially be routed
to a read-only database slave, while POST/PUT/DELETE requests require
writing, and would always bind to a master database with read/write
privileges). It's also possible to extend
:class:`~pecan.hooks.TransactionHook` or write your own hook implementation for
more refined control over where and when database bindings are called.
Assuming a master/standby setup, where the master accepts write requests and
the standby can only get read requests, a Pecan configuration for sqlalchemy
could be::
# 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'
}
Given the unique configuration settings for each database, the bindings would
need to change from what Pecan's default quickstart provides (see
:ref:`init_model` section) to accommodate for 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()
pecan-1.5.1/docs/source/deployment.rst 0000664 0000000 0000000 00000023130 14454530445 0017755 0 ustar 00root root 0000000 0000000 .. _deployment:
Deploying Pecan in Production
=============================
There are 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*
(:command:`pecan serve`), using a *production-ready* server similar to the ones
described in this document is **very highly encouraged**.
Installing Pecan
----------------
A few popular options are available for installing Pecan in production
environments:
* Using `setuptools `_. Manage
Pecan as a dependency in your project's ``setup.py`` file so that it's
installed alongside your project (e.g., ``python
/path/to/project/setup.py install``). The default Pecan project
described in :ref:`quick_start` facilitates this by including Pecan as
a dependency for your project.
* Using `pip `_.
Use ``pip freeze`` and ``pip install`` to create and install from
a ``requirements.txt`` file for your project.
* Via the manual instructions found in :ref:`Installation`.
.. note::
Regardless of the route you choose, it's highly recommended that all
deployment installations be done in a Python `virtual environment
`_.
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 that ``debug`` is set to ``False``.
::
# myapp/production_config.py
app = {
...
'debug': False
}
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 `__
* `uWSGI `__
* `Gunicorn `__
* `waitress `__
* `CherryPy `__
Generally speaking, the WSGI entry point to any Pecan application can be
generated using :func:`~pecan.deploy.deploy`::
from pecan.deploy import deploy
application = deploy('/path/to/some/app/config.py')
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:
1. Set up a proxy server (such as `nginx `__, `cherokee
`__, :ref:`cherrypy`, or `lighttpd
`__) to serve static files and proxy application
requests through to your WSGI application:
::
─── , e.g., Apache, nginx, cherokee (0.0.0.0:80) ───
│
├── Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5000 or /tmp/some.sock)
├── Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5001 or /tmp/some.sock)
├── Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5002 or /tmp/some.sock)
└── Instance e.g., mod_wsgi, Gunicorn, uWSGI (127.0.0.1:5003 or /tmp/some.sock)
2. Serve static files via a separate service, virtual host, or CDN.
Common Recipes
--------------
Apache + mod_wsgi
+++++++++++++++++
`mod_wsgi `_ 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.
For the sake of example, let's say that our project, ``simpleapp``, lives at
``/var/www/simpleapp``, and that a `virtualenv `_
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::
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
WSGIProcessGroup simpleapp
WSGIApplicationGroup %{GLOBAL}
Order deny,allow
Allow from all
For more instructions and examples of mounting WSGI applications using
mod_wsgi, consult the `mod_wsgi Documentation`_.
.. _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 `_ is a fast, self-healing and
developer/sysadmin-friendly application container server coded in pure C. It
uses the `uwsgi `__
protocol, 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 `_)::
$ uwsgi -s /tmp/uwsgi.sock --venv /path/to/virtualenv --pecan config.py
Gunicorn
++++++++
`Gunicorn `__, 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
.. _cherrypy:
CherryPy
++++++++
`CherryPy `__ 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
pecan-1.5.1/docs/source/development.rst 0000664 0000000 0000000 00000003406 14454530445 0020123 0 ustar 00root root 0000000 0000000 .. _development:
Developing Pecan Applications Locally
=====================================
.. include:: reload.rst
:start-after: #reload
Debugging Pecan Applications
----------------------------
Pecan comes with simple debugging middleware for helping diagnose problems
in your applications. To enable the debugging middleware, simply set the
``debug`` flag to ``True`` in your configuration file::
app = {
...
'debug': True,
...
}
Once enabled, the middleware will automatically catch exceptions raised by your
application and display the Python stack trace and WSGI environment in your
browser when runtime exceptions are raised.
To improve debugging, including support for an interactive browser-based
console, Pecan makes use of the Python `backlash
` library. You’ll need to install it
for development use before continuing::
$ pip install backlash
Downloading/unpacking backlash
...
Successfully installed backlash
Serving Static Files
--------------------
Pecan comes with simple file serving middleware for serving CSS, Javascript,
images, and other static files. You can configure it by ensuring that 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 in which your
static files live. For convenience, the path may include the ``%(confdir)``
variable, which Pecan will substitute with the absolute path of your
configuration file at runtime.
.. note::
In production, ``app.debug`` should *never* be set to ``True``, so you'll
need to serve your static files via your production web server.
pecan-1.5.1/docs/source/errors.rst 0000664 0000000 0000000 00000007267 14454530445 0017126 0 ustar 00root root 0000000 0000000 .. _errors:
Custom Error Documents
======================
In this article we will configure a Pecan application to display a custom
error page whenever the server returns a ``404 Page Not Found`` status.
This article assumes that you have already created a test application as
described in :ref:`quick_start`.
.. 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:
Overview
--------
Pecan makes it simple to customize error documents in two simple steps:
* :ref:`configure` of the HTTP status messages you want to handle
in your application's ``config.py``
* :ref:`controllers` to handle the status messages you have configured
.. _configure:
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``.
.. _controllers:
Write Custom Controllers
------------------------
The easiest way to implement the error handler is to
add it to :class:`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 :class:`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 :func:`~pecan.decorators.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.
pecan-1.5.1/docs/source/forms.rst 0000664 0000000 0000000 00000005051 14454530445 0016725 0 ustar 00root root 0000000 0000000 .. _forms:
Generating and Validating Forms
===============================
Pecan provides no opinionated support for working with
form generation and validation libraries, but it’s easy to import your
library of choice with minimal effort.
This article details best practices for integrating the popular forms library,
`WTForms `_, 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_ template file:
.. _Mako: http://www.makeotemplates.org/
.. code-block:: html
Validating POST Values
----------------------
Using the same :class:`MyForm` definition, let's redirect the user if the form is
validated, otherwise, render the form again.
.. code-block:: python
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)
pecan-1.5.1/docs/source/hooks.rst 0000664 0000000 0000000 00000032514 14454530445 0016726 0 ustar 00root root 0000000 0000000 .. _hooks:
Pecan Hooks
===========
Although it is easy to use WSGI middleware with Pecan, it can be hard
(sometimes impossible) to have access to Pecan's internals from within
middleware. Pecan Hooks are a way to interact with the framework,
without having to write separate middleware.
Hooks allow you to execute code at key points throughout the life cycle of your request:
* :func:`~pecan.hooks.PecanHook.on_route`: called before Pecan attempts to
route a request to a controller
* :func:`~pecan.hooks.PecanHook.before`: called after routing, but before
controller code is run
* :func:`~pecan.hooks.PecanHook.after`: called after controller code has been
run
* :func:`~pecan.hooks.PecanHook.on_error`: called when a request generates an
exception
Implementating a Pecan Hook
---------------------------
In the below example, a simple hook will gather some information about
the request and print it to ``stdout``.
Your hook implementation needs to import :class:`~pecan.hooks.PecanHook` so it
can be used as a base class. From there, you'll want to override the
:func:`~pecan.hooks.PecanHook.on_route`, :func:`~pecan.hooks.PecanHook.before`,
:func:`~pecan.hooks.PecanHook.after`, or
:func:`~pecan.hooks.PecanHook.on_error` methods to
define 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
:func:`~pecan.hooks.PecanHook.on_route`, :func:`~pecan.hooks.PecanHook.before`,
and :func:`~pecan.hooks.PecanHook.after` are each passed a shared
state object which includes useful information, such as the request and
response objects, and which controller was selected by Pecan's routing::
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)
:func:`~pecan.hooks.PecanHook.on_error` is passed a shared state object **and**
the original exception. If an :func:`~pecan.hooks.PecanHook.on_error` handler
returns a Response object, this response will be returned to the end user and
no furthur :func:`~pecan.hooks.PecanHook.on_error` hooks will be executed::
class CustomErrorHook(PecanHook):
def on_error(self, state, exc):
if isinstance(exc, SomeExceptionType):
return webob.Response('Custom Error!', status=500)
.. _attaching_hooks:
Attaching Hooks
---------------
Hooks can be attached in a project-wide manner by specifying a list of hooks
in your project's configuration file.
::
app = {
'root' : '...'
# ...
'hooks': lambda: [SimpleHook()]
}
Hooks can also be applied selectively to controllers and their sub-controllers
using the :attr:`__hooks__` attribute on one or more controllers and
subclassing :class:`~pecan.hooks.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 :class:`SimpleHook` is included, let's see what happens
when we run the app and browse the application 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
Hooks can be inherited from parent class or mixins. Just make sure to
subclass from :class:`~pecan.hooks.HookController`.
::
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()
Let's see what happens when we run the app.
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.
Hooks That Come with Pecan
--------------------------
Pecan includes some hooks in its core. This section will describe
their different uses, how to configure them, and examples of common
scenarios.
.. _requestviewerhook:
RequestViewerHook
'''''''''''''''''
This hook is useful for debugging purposes. It has access to every
attribute the ``response`` object has plus a few others that are specific to
the framework.
There are two main ways that this hook can provide information 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.
.. seealso::
* :ref:`pecan_hooks`
Configuring RequestViewerHook
.............................
There are a few ways to get this hook properly configured and running. However,
it is useful to know that no actual configuration is needed to have it up and
running.
By default it will output information about these items:
* path : Displays the url that was used to generate this response
* status : 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 response
* params : A list of tuples for the params passed in at request time
* hooks : 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) 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 specify the information that the hook will return.
Sometimes you will need a specific piece of information or a certain
bunch of them according to the development need so the defaults will
need to be changed and a list of items specified.
.. 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.
The hook has access to every single attribute the request object has
and not only to the default ones that are displayed, so you can fine tune the
information displayed.
These is a list containing all the possible attributes the hook has access to
(directly from `webob`):
====================== ==========================
====================== ==========================
accept make_tempfile
accept_charset max_forwards
accept_encoding method
accept_language params
application_url path
as_string path_info
authorization path_info_peek
blank path_info_pop
body path_qs
body_file path_url
body_file_raw postvars
body_file_seekable pragma
cache_control query_string
call_application queryvars
charset range
content_length referer
content_type referrer
cookies relative_url
copy remote_addr
copy_body remote_user
copy_get remove_conditional_headers
date request_body_tempfile_limit
decode_param_names scheme
environ script_name
from_file server_name
from_string server_port
get_response str_GET
headers str_POST
host str_cookies
host_url str_params
http_version str_postvars
if_match str_queryvars
if_modified_since unicode_errors
if_none_match upath_info
if_range url
if_unmodified_since urlargs
is_body_readable urlvars
is_body_seekable uscript_name
is_xhr user_agent
make_body_seekable
====================== ==========================
And these are the specific ones from Pecan and the hook:
* controller
* hooks
* params (params is actually available from `webob` but it is parsed
by the hook for redability)
Blacklisting Certain Paths
..........................
Sometimes it's annoying to get information about *every* single
request. To limit the output, pass the list of URL paths for which
you do not want data as the ``blacklist``.
The matching is done at the start of the URL path, so be careful when using
this feature. 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).
For any number of blocking you may need, just add as many items as wanted::
{ 'blacklist' : ['/favicon.ico', '/javascript', '/images'] }
Again, the ``blacklist`` key can be used along with the ``items`` key
or not (it is not required).
pecan-1.5.1/docs/source/index.rst 0000664 0000000 0000000 00000007417 14454530445 0016716 0 ustar 00root root 0000000 0000000 Introduction and History
========================
Welcome to Pecan, a lean Python web framework inspired by CherryPy,
TurboGears, and Pylons. Pecan was originally created by the developers
of `ShootQ `_ while working at `Pictage
`_.
Pecan was created to fill a void in the Python web-framework world – a
very lightweight framework that provides object-dispatch style
routing. 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). Pecan instead focuses on HTTP
itself.
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
Narrative Documentation
=======================
.. toctree::
:maxdepth: 2
installation.rst
quick_start.rst
routing.rst
templates.rst
rest.rst
configuration.rst
secure_controller.rst
hooks.rst
jsonify.rst
contextlocals.rst
commands.rst
development.rst
deployment.rst
logging.rst
testing.rst
Cookbook and Common Patterns
============================
.. toctree::
:maxdepth: 2
forms.rst
sessions.rst
databases.rst
errors.rst
simple_forms_processing.rst
simple_ajax.rst
API Documentation
=================
Pecan's source code is well documented using Python docstrings and
comments. In addition, we have generated API documentation from the
docstrings here:
.. toctree::
:maxdepth: 2
pecan_core.rst
pecan_commands.rst
pecan_configuration.rst
pecan_decorators.rst
pecan_deploy.rst
pecan_hooks.rst
pecan_middleware_debug.rst
pecan_jsonify.rst
pecan_rest.rst
pecan_routing.rst
pecan_secure.rst
pecan_templating.rst
pecan_testing.rst
pecan_util.rst
Change History
=================
.. toctree::
:maxdepth: 1
changes.rst
License
-------
The Pecan framework and the documentation is BSD Licensed::
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 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 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.
pecan-1.5.1/docs/source/installation.rst 0000664 0000000 0000000 00000002656 14454530445 0020310 0 ustar 00root root 0000000 0000000 .. _installation:
Installation
============
Stable Version
--------------
We recommend installing Pecan with `pip
`_, but you
can also try with :command:`easy_install`. Creating a spot in your environment
where Pecan can be isolated from other packages is best practice.
To get started with an environment for Pecan, we recommend creating a new
`virtual environment `_ using `virtualenv
`_::
$ 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.
Next, let's install Pecan::
$ pip install pecan
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.
Alternatively, you can also install from GitHub directly with ``pip``.::
$ pip install -e git://github.com/pecan/pecan.git#egg=pecan
pecan-1.5.1/docs/source/jsonify.rst 0000664 0000000 0000000 00000004563 14454530445 0017267 0 ustar 00root root 0000000 0000000 .. _jsonify:
JSON Serialization
==================
Pecan includes a simple, easy-to-use system for generating and serving
JSON. To get started, create a file in your project called
``json.py`` and import it in your project's ``app.py``.
Your ``json`` module will contain a series of rules for generating
JSON from objects you return in your controller.
Let's say that we have a controller in our Pecan application which
we want to use to return JSON output for a :class:`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()
In order for this controller to function, Pecan will need to know how to
convert the :class:`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 :class:`model.User` class is
returned from a controller which is configured to return JSON, the
:func:`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.
Alternatively, the rule can be specified on the object itself, by
specifying a :func:`__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.
pecan-1.5.1/docs/source/logging.rst 0000664 0000000 0000000 00000011015 14454530445 0017222 0 ustar 00root root 0000000 0000000 .. _logging:
Logging
=======
Pecan uses the Python standard library's :py:mod:`logging` module by passing
logging configuration options into the `logging.config.dictConfig`_
function. The full documentation for the :func:`dictConfig` format is
the best source of information for logging configuration, but to get
you started, this chapter will provide you with a few simple examples.
.. _logging.config.dictConfig: http://docs.python.org/library/logging.config.html#configuration-dictionary-schema
Configuring Logging
-------------------
Sample logging configuration is provided with the quickstart project
introduced in :ref:`quick_start`:
::
$ pecan create myapp
The default configuration defines one handler and two loggers.
::
# 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')
}
}
}
* ``console`` logs messages to ``stderr`` using the ``simple`` formatter.
* ``myapp`` logs messages sent at a level above or equal to ``DEBUG`` to
the ``console`` handler
* ``root`` logs messages at a level above or equal to the ``INFO`` level to
the ``console`` handler
Writing Log Messages in Your Application
----------------------------------------
The logger named ``myapp`` is reserved for your usage in your Pecan
application.
Once you have configured your logging, you can place logging calls in your
code. Using the logging framework is 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 :py:mod:`logging` library defines a variety of handlers that assist in
writing logs to file. A few interesting ones are:
* :class:`~logging.FileHandler` - used to log messages to a file on the filesystem
* :class:`~logging.handlers.RotatingFileHandler` - similar to
:class:`~logging.FileHandler`, but also rotates logs
periodically
* :class:`~logging.handlers.SysLogHandler` - used to log messages to a UNIX syslog
* :class:`~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 of more loggers.
Logging Requests with Paste Translogger
---------------------------------------
`Paste `_ (which is not included with Pecan) includes
the :class:`~paste.translogger.TransLogger` middleware
for logging requests in `Apache Combined Log Format
`_. Combined with
file-based logging, TransLogger can be used to create an ``access.log`` file
similar to ``Apache``.
To add this middleware, modify your 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, :class:`~paste.translogger.TransLogger` creates a logger named
``wsgi``, so you'll need to specify a new (file-based) handler for this logger
in our 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'}
}
}
pecan-1.5.1/docs/source/pecan_commands.rst 0000664 0000000 0000000 00000001277 14454530445 0020554 0 ustar 00root root 0000000 0000000 .. _pecan_commands:
:mod:`pecan.commands` -- Pecan Commands
=======================================
The :mod:`pecan.commands` module implements the ``pecan`` console script
used to provide (for example) ``pecan serve`` and ``pecan shell`` command line
utilities.
.. automodule:: pecan.commands.base
:members:
:show-inheritance:
:mod:`pecan.commands.server` -- Pecan Development Server
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: pecan.commands.serve
:members:
:show-inheritance:
:mod:`pecan.commands.shell` -- Pecan Interactive Shell
++++++++++++++++++++++++++++++++++++++++++++++++++++++
.. automodule:: pecan.commands.shell
:members:
:show-inheritance:
pecan-1.5.1/docs/source/pecan_configuration.rst 0000664 0000000 0000000 00000000522 14454530445 0021612 0 ustar 00root root 0000000 0000000 .. _pecan_configuration:
:mod:`pecan.configuration` -- Pecan Configuration Engine
========================================================
The :mod:`pecan.configuration` module provides an implementation of a
Python-based configuration engine for Pecan applications.
.. automodule:: pecan.configuration
:members:
:show-inheritance: pecan-1.5.1/docs/source/pecan_core.rst 0000664 0000000 0000000 00000000446 14454530445 0017700 0 ustar 00root root 0000000 0000000 .. _pecan_core:
:mod:`pecan.core` -- Pecan Core
===============================
The :mod:`pecan.core` module is the base module for creating and extending
Pecan. The core logic for processing HTTP requests and responses lives
here.
.. automodule:: pecan.core
:members:
:show-inheritance: pecan-1.5.1/docs/source/pecan_decorators.rst 0000664 0000000 0000000 00000000421 14454530445 0021106 0 ustar 00root root 0000000 0000000 .. _pecan_decorators:
:mod:`pecan.decorators` -- Pecan Decorators
===========================================
The :mod:`pecan.decorators` module includes useful decorators for
creating Pecan applications.
.. automodule:: pecan.decorators
:members:
:show-inheritance: pecan-1.5.1/docs/source/pecan_deploy.rst 0000664 0000000 0000000 00000000367 14454530445 0020246 0 ustar 00root root 0000000 0000000 .. _pecan_deploy:
:mod:`pecan.deploy` -- Pecan Deploy
===========================================
The :mod:`pecan.deploy` module includes fixtures to help deploy Pecan
applications.
.. automodule:: pecan.deploy
:members:
:show-inheritance:
pecan-1.5.1/docs/source/pecan_hooks.rst 0000664 0000000 0000000 00000000544 14454530445 0020072 0 ustar 00root root 0000000 0000000 .. _pecan_hooks:
:mod:`pecan.hooks` -- Pecan Hooks
=================================
The :mod:`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.
.. automodule:: pecan.hooks
:members:
:show-inheritance: pecan-1.5.1/docs/source/pecan_jsonify.rst 0000664 0000000 0000000 00000000430 14454530445 0020422 0 ustar 00root root 0000000 0000000 .. _pecan_jsonify:
:mod:`pecan.jsonify` -- Pecan ``JSON`` Support
==============================================
The :mod:`pecan.jsonify` module includes support for ``JSON`` rule
creation using generic functions.
.. automodule:: pecan.jsonify
:members:
:show-inheritance: pecan-1.5.1/docs/source/pecan_middleware_debug.rst 0000664 0000000 0000000 00000000340 14454530445 0022224 0 ustar 00root root 0000000 0000000 .. _pecan_middleware_debug:
:mod:`pecan.middleware.debug` -- Pecan Debugging Middleware
===========================================================
.. automodule:: pecan.middleware.debug
:members:
:show-inheritance:
pecan-1.5.1/docs/source/pecan_rest.rst 0000664 0000000 0000000 00000000437 14454530445 0017725 0 ustar 00root root 0000000 0000000 .. _pecan_rest:
:mod:`pecan.rest` -- Pecan ``REST`` Controller
==============================================
The :mod:`pecan.rest` module includes support for writing fully
``RESTful`` controllers in your Pecan application.
.. automodule:: pecan.rest
:members:
:show-inheritance: pecan-1.5.1/docs/source/pecan_routing.rst 0000664 0000000 0000000 00000000367 14454530445 0020441 0 ustar 00root root 0000000 0000000 .. _pecan_routing:
:mod:`pecan.routing` -- Pecan Routing
=====================================
The :mod:`pecan.routing` module is the basis for all object-dispatch
routing in Pecan.
.. automodule:: pecan.routing
:members:
:show-inheritance: pecan-1.5.1/docs/source/pecan_secure.rst 0000664 0000000 0000000 00000000675 14454530445 0020242 0 ustar 00root root 0000000 0000000 .. _pecan_secure:
:mod:`pecan.secure` -- Pecan Secure Controllers
===============================================
The :mod:`pecan.secure` module includes a basic framework for building
security into your applications.
.. automodule:: pecan.secure
:members:
:show-inheritance:
.. autoclass:: pecan.secure.SecureControllerBase
:members:
:show-inheritance:
.. autoclass:: pecan.secure.SecureController
:members:
:show-inheritance:
pecan-1.5.1/docs/source/pecan_templating.rst 0000664 0000000 0000000 00000000501 14454530445 0021104 0 ustar 00root root 0000000 0000000 .. _pecan_templating:
:mod:`pecan.templating` -- Pecan Templating
===========================================
The :mod:`pecan.templating` module includes support for a variety of
templating engines, plus the ability to create your own template
engines.
.. automodule:: pecan.templating
:members:
:show-inheritance: pecan-1.5.1/docs/source/pecan_testing.rst 0000664 0000000 0000000 00000000372 14454530445 0020423 0 ustar 00root root 0000000 0000000 .. _pecan_testing:
:mod:`pecan.testing` -- Pecan Testing
===========================================
The :mod:`pecan.testing` module includes fixtures to help test Pecan
applications.
.. automodule:: pecan.testing
:members:
:show-inheritance:
pecan-1.5.1/docs/source/pecan_util.rst 0000664 0000000 0000000 00000000323 14454530445 0017717 0 ustar 00root root 0000000 0000000 .. _pecan_util:
:mod:`pecan.util` -- Pecan Utils
================================
The :mod:`pecan.util` module includes utility functions for Pecan.
.. automodule:: pecan.util
:members:
:show-inheritance: pecan-1.5.1/docs/source/quick_start.rst 0000664 0000000 0000000 00000022514 14454530445 0020133 0 ustar 00root root 0000000 0000000 .. _quick_start:
Creating Your First Pecan Application
=====================================
Let's create a small sample project with Pecan.
.. note::
This guide does not cover the installation of Pecan. If you need
instructions for installing Pecan, refer to :ref:`installation`.
.. _app_template:
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.
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 :mod:`sys.path`, yet can still be edited directly from its
source distribution::
$ python setup.py develop
Your new project contain 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.
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.
Pecan application structure generally follows the MVC_ pattern. The
directories under ``test_project`` encompass your models, controllers
and templates.
.. _MVC: http://en.wikipedia.org/wiki/Model–view–controller
**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 :func:`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
:ref:`python_based_config` below.
To avoid unneeded dependencies and to remain as flexible as possible,
Pecan doesn't impose any database or ORM (`Object Relational
Mapper`_). 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.
.. _Object Relational Mapper: http://en.wikipedia.org/wiki/Object-relational_mapping
.. _running_application:
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 :command:`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.
.. _python_based_config:
Python-Based Configuration
--------------------------
For ease of use, Pecan configuration files are pure Python--they're even saved
as ``.py`` files.
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.
There's a lot to cover here, so we'll come back to configuration files in
a later chapter (:ref:`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 :func:`index` method is marked as *publicly available* via the
:func:`~pecan.decorators.expose` decorator (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 :func:`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.
::
@index.when(method='POST')
def index_post(self, q):
redirect('https://pecan.readthedocs.io/en/latest/search.html?q=%s' % q)
The :func:`index_post` method receives one HTTP ``POST`` argument (``q``). Because
the argument ``method`` to :func:`@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.
::
@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)
Finally, we have the :func:`error` method, which allows the application to display
custom pages for certain HTTP errors (``404``, etc...).
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::
$ python setup.py test -q
running test
running egg_info
writing requirements to sam.egg-info/requires.txt
writing sam.egg-info/PKG-INFO
writing top-level names to sam.egg-info/top_level.txt
writing dependency_links to sam.egg-info/dependency_links.txt
reading manifest file 'sam.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
writing manifest file 'sam.egg-info/SOURCES.txt'
running build_ext
....
----------------------------------------------------------------------
Ran 4 tests in 0.009s
OK
The tests themselves can be found in the ``tests`` module in your project.
Deploying to a Web Server
-------------------------
Ready to deploy your new Pecan app? Take a look at :ref:`deployment`.
pecan-1.5.1/docs/source/reload.rst 0000664 0000000 0000000 00000001541 14454530445 0017045 0 ustar 00root root 0000000 0000000 :orphan:
#reload
Reloading Automatically as Files Change
---------------------------------------
Pausing to restart your development server as you work can be interruptive, so
:command:`pecan serve` provides a ``--reload`` flag to make life easier.
To provide this functionality, Pecan makes use of the Python
`watchdog `_ library. 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.
pecan-1.5.1/docs/source/rest.rst 0000664 0000000 0000000 00000021366 14454530445 0016563 0 ustar 00root root 0000000 0000000 .. _rest:
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 is 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 //
@expose(generic=True, template='json')
def index(self):
return self.book
# HTTP PUT //
@index.when(method='PUT', template='json')
def index_PUT(self, **kw):
BOOKS[self.id_] = kw['name']
return self.book
# HTTP DELETE //
@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'])
Writing RESTful Web Services with RestController
================================================
.. _TurboGears2: http://turbogears.org
For compatability with the TurboGears2_ library, Pecan also provides
a class-based solution to RESTful routing, :class:`~pecan.rest.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, :class:`~pecan.rest.RestController` routes as follows:
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| Method | Description | Example Method(s) / URL(s) |
+=================+==============================================================+============================================+
| get_one | Display one record. | GET /books/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get_all | Display all records in a resource. | GET /books/ |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get | A combo of get_one and get_all. | GET /books/ |
| | +--------------------------------------------+
| | | GET /books/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| new | Display a page to create a new resource. | GET /books/new |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| edit | Display a page to edit an existing resource. | GET /books/1/edit |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| post | Create a new record. | POST /books/ |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| put | Update an existing record. | POST /books/1?_method=put |
| | +--------------------------------------------+
| | | PUT /books/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| get_delete | Display a delete confirmation page. | GET /books/1/delete |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
| delete | Delete an existing record. | POST /books/1?_method=delete |
| | +--------------------------------------------+
| | | DELETE /books/1 |
+-----------------+--------------------------------------------------------------+--------------------------------------------+
Pecan's :class:`~pecan.rest.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 :class:`~pecan.rest.RestController` also
supports the :meth:`index`, :meth:`_default`, and :meth:`_lookup`
routing overrides.
.. warning::
If you need to override :meth:`_route`, make sure to call
:func:`RestController._route` at the end of your custom method so
that the REST routing described above still occurs.
Nesting ``RestController``
---------------------------
:class:`~pecan.rest.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 :func:`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 :func:`get_one` then :func:`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 :class:`~pecan.rest.RestController` by defining a special
:attr:`_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()
:attr:`_custom_actions` maps method names to the list of valid HTTP
verbs for those custom actions. In this case :func:`checkout` supports
``POST``.
pecan-1.5.1/docs/source/routing.rst 0000664 0000000 0000000 00000045544 14454530445 0017301 0 ustar 00root root 0000000 0000000 .. _routing:
Controllers and Routing
=======================
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).
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.
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
:func:`~pecan.decorators.expose`. If a method is *not* decorated with
:func:`~pecan.decorators.expose`, Pecan will never route a request to it.
:func:`~pecan.decorators.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 :ref:`specify a template and a namespace
`::
from pecan import expose
class RootController(object):
@expose('html_template.mako')
def hello(self):
return {'msg': 'Hello!'}
::
${msg}
Pecan also has built-in support for a special :ref:`JSON renderer
`, which translates template namespaces into rendered JSON text::
from pecan import expose
class RootController(object):
@expose('json')
def hello(self):
return {'msg': 'Hello!'}
:func:`~pecan.decorators.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 :func:`~pecan.decorators.expose` three times, with
different arguments.
::
@expose('json')
The first tells Pecan to serialize the response namespace using JSON
serialization when the client requests ``/hello.json`` or if an
``Accept: application/json`` header is present.
::
@expose('text_template.mako', content_type='text/plain')
The second tells Pecan to use the ``text_template.mako`` template file when the
client requests ``/hello.txt`` or asks for text/plain via an ``Accept`` header.
::
@expose('html_template.mako')
The third tells Pecan to use the ``html_template.mako`` template file when the
client requests ``/hello.html``. If the client requests ``/hello``, Pecan will
use the ``text/html`` content type by default; in the absense of an explicit
content type, Pecan assumes the client wants HTML.
.. seealso::
* :ref:`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 :func:`~pecan.decorators.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``.
:func:`~pecan.routing.route` can also be used explicitly as an alternative to
the ``route`` argument in :func:`~pecan.decorators.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 simliarly by utilizing
:func:`~pecan.routing.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/``.
Routing Based on Request Method
-------------------------------
The ``generic`` argument to :func:`~pecan.decorators.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 :func:`_lookup`, :func:`_default`, and :func:`_route` methods. Defining
these methods on your controller objects provides additional flexibility for
processing all or part of a URL.
Routing to Subcontrollers with ``_lookup``
------------------------------------------
The :func:`_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 :func:`_lookup` method may accept one or more arguments, segments
of the URL path to be processed (split on
``/``). :func:`_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,
:func:`_lookup` is called as a last resort, when no other controller
method matches the URL and there is no :func:`_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``.
Falling Back with ``_default``
------------------------------
The :func:`_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
:func:`RootController._default`.
Defining Customized Routing with ``_route``
-------------------------------------------
The :func:`_route` method allows a controller to completely override the routing
mechanism of Pecan. Pecan itself uses the :func:`_route` method to implement its
:class:`~pecan.rest.RestController`. If you want to design an alternative
routing system on top of Pecan, defining a base controller class that defines
a :func:`_route` method will enable you to have total control.
Interacting with the Request and Response Object
================================================
For every HTTP request, Pecan maintains a :ref:`thread-local reference
` to the request and response object, ``pecan.request`` and
``pecan.response``. These are instances of :class:`pecan.Request`
and :class:`pecan.Response`, respectively, and can be interacted with
from within 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!'
While Pecan abstracts away much of the need to interact with these objects
directly, there may be situations where you want to access them, such as:
* 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.
::
from pecan import expose, response
class RootController(object):
@expose('json')
def hello(self):
response.status = 203
return {'foo': 'bar'}
Use the utility function :func:`~pecan.core.abort` to raise HTTP errors.
::
from pecan import expose, abort
class RootController(object):
@expose('json')
def hello(self):
abort(404)
:func:`~pecan.core.abort` raises an instance of
:class:`~webob.exc.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 :ref:`errors`).
If you'd like to return an explicit response, you can do so using
:class:`~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
at times, it may be 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.
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
:class:`~webob.request.Request`, serving a static file download is as simple as
setting the WSGI ``app_iter`` and specifying the content type::
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 :func:`~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:
.. code-block:: html
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::
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``.
Helper Functions
----------------
Pecan also provides several useful helper functions for moving between
different routes. The :func:`~pecan.core.redirect` function allows you to issue
internal or ``HTTP 302`` redirects.
.. seealso::
The :func:`redirect` utility, along with several other useful
helpers, are documented in :ref:`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.
For example, in the following code that makes use of :func:`_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.
pecan-1.5.1/docs/source/secure_controller.rst 0000664 0000000 0000000 00000016550 14454530445 0021336 0 ustar 00root root 0000000 0000000 .. _secure_controller:
Security and Authentication
===========================
Pecan provides no out-of-the-box support for authentication, but it
does give you the necessary tools to handle authentication and
authorization as you see fit.
``secure`` Decorator Basics
---------------------------
You can wrap entire controller subtrees *or* individual method calls
with access controls using the :func:`~pecan.secure.secure` decorator.
To decorate a method, use one argument::
secure('')
To secure a class, invoke with two arguments::
secure(object_instance, '')
::
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, the same functionality can also be accomplished by
subclassing Pecan's :class:`~pecan.secure.SecureController`. Implementations of
:class:`~pecan.secure.SecureController` should extend the
:meth:`~pecan.secure.SecureControllerBase.check_permissions` class method to
return ``True`` if the user has permissions to the controller branch and
``False`` if they 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 note the use of the :func:`~pecan.secure.unlocked` decorator in the above
example, which can be used similarly to explicitly unlock a controller for
public access without any security checks.
Writing Authentication/Authorization Methods
--------------------------------------------
The :meth:`~pecan.secure.SecureControllerBase.check_permissions` method should
be used to determine user authentication and authorization. The code you
implement here could range from simple session assertions (the existing user is
authenticated as an administrator) to connecting to an LDAP service.
More on ``secure``
------------------
The :func:`~pecan.secure.secure` method has several advanced uses that allow
you to create robust security policies for your application.
First, you can pass via a string the name of either a class method or an
instance method of the controller to use as the
:meth:`~pecan.secure.SecureControllerBase.check_permissions` method. Instance
methods are particularly useful if you wish to authorize access to attributes
of a model instance. Consider the following example of a basic virtual
filesystem.
::
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
The :func:`~pecan.secure.secure` method also accepts a function argument. When
passing a function, make sure that the function is imported from another
file or defined in the same file before the class definition, otherwise
you will likely get error during module import.
::
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'
You can also use the :func:`~pecan.secure.secure` method to change the behavior
of a :class:`~pecan.secure.SecureController`. Decorating a method or wrapping
a subcontroller tells Pecan to use another security function other than the
default controller method. This is useful for situations where you want
a different level or type of security.
::
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')
In the example above, pecan will *only* call :func:`admin_user` when a request is
made for ``/api/``.
Multiple Secure Controllers
---------------------------
Secure controllers can be nested to provide increasing levels of
security on subcontrollers. In the example below, when a request is
made for ``/admin/index/``, Pecan first calls
:func:`~pecan.secure.SecureControllerBase.check_permissions` on the
:class:`RootController` and then
calls :func:`~pecan.secure.SecureControllerBase.check_permissions` on the
:class:`AdminController`.
::
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-1.5.1/docs/source/sessions.rst 0000664 0000000 0000000 00000002342 14454530445 0017445 0 ustar 00root root 0000000 0000000 .. _session:
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 `_, 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 :ref:`hooks`.
Here's an example of wrapping your WSGI application with Beaker's
:class:`~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
}
pecan-1.5.1/docs/source/simple_ajax.rst 0000664 0000000 0000000 00000022651 14454530445 0020100 0 ustar 00root root 0000000 0000000 .. _simple_ajax:
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.
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. 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:
.. code-block:: html
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:
.. code-block:: html
${self.title()}
${self.style()}
${self.javascript()}
${self.body()}
<%def name="title()">
Default Title
%def>
<%def name="style()">
%def>
<%def name="javascript()">
%def>
**What did we just do?**
#. In the ``head`` section we added jQuery support via the `Google CDN `_
#. Added JavaScript to make an AJAX call to the server via an HTTP ``GET`` passing in the ``id`` of the project to fetch more information on
#. Once the ``onSuccess`` event 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:
.. code-block:: html
<%inherit file="layout.html" />
<%def name="title()">
Welcome to Pecan!
%def>
Select a project to get details:
**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:
.. code-block:: python
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 ``Project`` that can hold project specific data
#. Added a ``__json__`` method so an instance of the ``Project class`` 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 :ref:`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:
.. code-block:: python
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:
.. code-block:: python
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 ``projects`` that 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 ``projects`` endpoint and serve back JSON based on the ``id`` passed 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:
.. code-block:: python
@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.
.. code-block:: html
Select a project to get details:
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-1.5.1/docs/source/simple_forms_processing.rst 0000664 0000000 0000000 00000015674 14454530445 0022546 0 ustar 00root root 0000000 0000000 .. _simple_forms_processing:
Example Application: Simple Forms Processing
============================================
This guide will walk you through building a simple Pecan web application that will do some simple forms processing.
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:
.. code-block:: html
<%inherit file="layout.html" />
<%def name="title()">
Welcome to Pecan!
%def>
% if not form_post_data is UNDEFINED:
${form_post_data['first_name']}, your message is: ${form_post_data['message']}
% endif
**What did we just do?**
#. Modified the contents of the ``form`` tag to have two ``input`` tags. The first is named ``message`` and the second is named ``first_name``
#. Added a check if ``form_post_data`` has 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:
.. code-block:: python
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 ``index`` function to render the initial ``index.html`` webpage
#. Modified the ``index_post`` function 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 `_ but you can substitute anything else for your projects.
Add support for the `WTForms `_ 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:
.. code-block:: python
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 ``PersonalizedMessageForm`` with two textfields and a required field validator for each
#. Modified the ``index`` function to create a new instance of the ``PersonalizedMessageForm`` class and return it
#. In the ``index_post`` function 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:
.. code-block:: html
<%inherit file="layout.html" />
## provide definitions for blocks we want to redefine
<%def name="title()">
Welcome to Pecan!
%def>
% if not form:
${first_name}, your message is: ${message}
% else:
% endif
.. note::
Keep in mind when using the `WTForms `_ 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 `_ 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.
pecan-1.5.1/docs/source/static 0000664 0000000 0000000 00000000000 14454530445 0016244 0 ustar 00root root 0000000 0000000 pecan-1.5.1/docs/source/templates.rst 0000664 0000000 0000000 00000010051 14454530445 0017571 0 ustar 00root root 0000000 0000000 .. _templates:
Templating in Pecan
===================
Pecan includes support for a variety of templating engines and also
makes it easy to add support for new template engines. Currently,
Pecan supports:
=============== =============
Template System Renderer Name
=============== =============
Mako_ mako
Genshi_ genshi
Kajiki_ kajiki
Jinja2_ jinja
JSON json
=============== =============
.. _Mako: http://www.makotemplates.org/
.. _Genshi: http://genshi.edgewall.org/
.. _Kajiki: http://kajiki.pythonisito.com/
.. _Jinja2: http://jinja.pocoo.org/
The default template system is ``mako``, but that can be changed by
passing the ``default_renderer`` key in your application's
configuration::
app = {
'default_renderer' : 'kajiki',
# ...
}
Using Template Renderers
------------------------
:py:mod:`pecan.decorators` defines a decorator called
:func:`~pecan.decorators.expose`, which is used to flag a method as a public
controller. The :func:`~pecan.decorators.expose` decorator takes a ``template``
argument, which can be used to specify the path to the template file to use for
the controller method being exposed.
::
class MyController(object):
@expose('path/to/mako/template.html')
def index(self):
return dict(message='I am a mako template')
:func:`~pecan.decorators.expose` will use the default template engine unless
the path is prefixed by another renderer name.
::
@expose('kajiki:path/to/kajiki/template.html')
def my_controller(self):
return dict(message='I am a kajiki template')
.. seealso::
* :ref:`pecan_decorators`
* :ref:`pecan_core`
* :ref:`routing`
Overriding Templates
--------------------
:func:`~pecan.core.override_template` allows you to override the template set
for a controller method when it is exposed. When
:func:`~pecan.core.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.
::
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
----------------
:func:`~pecan.core.render` allows you to manually render output using the Pecan
templating framework. Pass the template path and values to go into the
template, and :func:`~pecan.core.render` returns the rendered output as text.
::
@expose()
def controller(self):
return render('my_template.html', dict(message='I am the namespace'))
.. _expose_json:
The JSON Renderer
-----------------
Pecan also provides a ``JSON`` renderer, which you can use by exposing
a controller method with ``@expose('json')``.
.. seealso::
* :ref:`jsonify`
* :ref:`pecan_jsonify`
Defining Custom Renderers
-------------------------
To define a custom renderer, you can 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 specify the renderer in the :func:`~pecan.decorators.expose` method::
class RootController(object):
@expose('my_renderer:template.html')
def index(self):
return dict(name='Bob')
pecan-1.5.1/docs/source/testing.rst 0000664 0000000 0000000 00000010616 14454530445 0017257 0 ustar 00root root 0000000 0000000 .. _testing:
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 :mod:`unittest` module.
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 `_, `pytest
`_,
and `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 :mod:`webtest` library. In this way, it is 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, :func:`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 :class:`~webtest.app.TestApp`
environment.
From here, it's possible to extend the :class:`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
.. seealso::
See the :mod:`webtest` documentation
for further information about the methods available to a
:class:`~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 :class:`~webtest.response.TestResponse` object.
Let's suppose that your Pecan applicaton 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 it's 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 :attr:`webtest.TestResponse.body`, Pecan also provides
:attr:`webtest.TestResponse.namespace`, which represents the template namespace
returned from the controller, and :attr:`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.
pecan-1.5.1/pecan/ 0000775 0000000 0000000 00000000000 14454530445 0013702 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/__init__.py 0000664 0000000 0000000 00000011644 14454530445 0016021 0 ustar 00root root 0000000 0000000 from .core import (
abort, override_template, Pecan, Request, Response, load_app,
redirect, render, request, response
)
from .decorators import expose
from .hooks import RequestViewerHook
from .middleware.debug import DebugMiddleware
from .middleware.errordocument import ErrorDocumentMiddleware
from .middleware.recursive import RecursiveMiddleware
from .middleware.static import StaticFileMiddleware
from .routing import route
from .configuration import set_config, Config
from .configuration import _runtime_conf as conf
from . import middleware
try:
from logging.config import dictConfig as load_logging_config
except ImportError:
from logutils.dictconfig import dictConfig as load_logging_config # noqa
import warnings
__all__ = [
'make_app', 'load_app', 'Pecan', 'Request', 'Response', 'request',
'response', 'override_template', 'expose', 'conf', 'set_config', 'render',
'abort', 'redirect', 'route'
]
def make_app(root, **kw):
'''
Utility for creating the Pecan application object. This function should
generally be called from the ``setup_app`` function in your project's
``app.py`` file.
:param root: A string representing a root controller object (e.g.,
"myapp.controller.root.RootController")
:param static_root: The relative path to a directory containing static
files. Serving static files is only enabled when
debug mode is set.
:param debug: A flag to enable debug mode. This enables the debug
middleware and serving static files.
:param wrap_app: A function or middleware class to wrap the Pecan app.
This must either be a wsgi middleware class or a
function that returns a wsgi application. This wrapper
is applied first before wrapping the application in
other middlewares such as Pecan's debug middleware.
This should be used if you want to use middleware to
perform authentication or intercept all requests before
they are routed to the root controller.
:param logging: A dictionary used to configure logging. This uses
``logging.config.dictConfig``.
All other keyword arguments are passed in to the Pecan app constructor.
:returns: a ``Pecan`` object.
'''
# Pass logging configuration (if it exists) on to the Python logging module
logging = kw.get('logging', {})
debug = kw.get('debug', False)
if logging:
if debug:
try:
#
# By default, Python 2.7+ silences DeprecationWarnings.
# However, if conf.app.debug is True, we should probably ensure
# that users see these types of warnings.
#
from logging import captureWarnings
captureWarnings(True)
warnings.simplefilter("default", DeprecationWarning)
except ImportError:
# No captureWarnings on Python 2.6, DeprecationWarnings are on
pass
if isinstance(logging, Config):
logging = logging.to_dict()
if 'version' not in logging:
logging['version'] = 1
load_logging_config(logging)
# Instantiate the WSGI app by passing **kw onward
app = Pecan(root, **kw)
# Optionally wrap the app in another WSGI app
wrap_app = kw.get('wrap_app', None)
if wrap_app:
app = wrap_app(app)
# Configuration for serving custom error messages
errors = kw.get('errors', getattr(conf.app, 'errors', {}))
if errors:
app = middleware.errordocument.ErrorDocumentMiddleware(app, errors)
# Included for internal redirect support
app = middleware.recursive.RecursiveMiddleware(app)
# When in debug mode, load exception debugging middleware
static_root = kw.get('static_root', None)
if debug:
debug_kwargs = getattr(conf, 'debug', {})
debug_kwargs.setdefault('context_injectors', []).append(
lambda environ: {
'request': environ.get('pecan.locals', {}).get('request')
}
)
app = DebugMiddleware(
app,
**debug_kwargs
)
# Support for serving static files (for development convenience)
if static_root:
app = middleware.static.StaticFileMiddleware(app, static_root)
elif static_root:
warnings.warn(
"`static_root` is only used when `debug` is True, ignoring",
RuntimeWarning
)
if hasattr(conf, 'requestviewer'):
warnings.warn(''.join([
"`pecan.conf.requestviewer` is deprecated. To apply the ",
"`RequestViewerHook` to your application, add it to ",
"`pecan.conf.app.hooks` or manually in your project's `app.py` ",
"file."]),
DeprecationWarning
)
return app
pecan-1.5.1/pecan/commands/ 0000775 0000000 0000000 00000000000 14454530445 0015503 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/commands/__init__.py 0000664 0000000 0000000 00000000257 14454530445 0017620 0 ustar 00root root 0000000 0000000 from .base import CommandRunner, BaseCommand # noqa
from .serve import ServeCommand # noqa
from .shell import ShellCommand # noqa
from .create import CreateCommand # noqa
pecan-1.5.1/pecan/commands/base.py 0000664 0000000 0000000 00000011257 14454530445 0016775 0 ustar 00root root 0000000 0000000 import pkg_resources
import argparse
import logging
import sys
from warnings import warn
log = logging.getLogger(__name__)
class HelpfulArgumentParser(argparse.ArgumentParser):
def error(self, message): # pragma: nocover
"""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.
"""
self.print_help(sys.stderr)
self._print_message('\n')
self.exit(2, '%s: %s\n' % (self.prog, message))
class CommandManager(object):
""" Used to discover `pecan.command` entry points. """
def __init__(self):
self.commands = {}
self.load_commands()
def load_commands(self):
for ep in pkg_resources.iter_entry_points('pecan.command'):
log.debug('%s loading plugin %s', self.__class__.__name__, ep)
if ep.name in self.commands:
warn(
"Duplicate entry points found on `%s` - ignoring %s" % (
ep.name,
ep
),
RuntimeWarning
)
continue
try:
cmd = ep.load()
cmd.run # ensure existance; catch AttributeError otherwise
except Exception as e: # pragma: nocover
warn("Unable to load plugin %s: %s" % (ep, e), RuntimeWarning)
continue
self.add({ep.name: cmd})
def add(self, cmd):
self.commands.update(cmd)
class CommandRunner(object):
""" Dispatches `pecan` command execution requests. """
def __init__(self):
self.manager = CommandManager()
self.parser = HelpfulArgumentParser(add_help=True)
self.parser.add_argument(
'--version',
action='version',
version='Pecan %s' % self.version
)
self.parse_sub_commands()
def parse_sub_commands(self):
subparsers = self.parser.add_subparsers(
dest='command_name',
metavar='command'
)
for name, cmd in self.commands.items():
sub = subparsers.add_parser(
name,
help=cmd.summary
)
for arg in getattr(cmd, 'arguments', tuple()):
arg = arg.copy()
if isinstance(arg.get('name'), str):
sub.add_argument(arg.pop('name'), **arg)
elif isinstance(arg.get('name'), list):
sub.add_argument(*arg.pop('name'), **arg)
def run(self, args):
ns = self.parser.parse_args(args)
if ns.command_name is None:
self.run(['--help'])
return
self.commands[ns.command_name]().run(ns)
@classmethod
def handle_command_line(cls): # pragma: nocover
runner = CommandRunner()
runner.run(sys.argv[1:])
@property
def version(self):
return pkg_resources.get_distribution('pecan').version
@property
def commands(self):
return self.manager.commands
class BaseCommandMeta(type):
@property
def summary(cls):
"""
This is used to populate the --help argument on the command line.
This provides a default behavior which takes the first sentence of the
command's docstring and uses it.
"""
return cls.__doc__.strip().splitlines()[0].rstrip('.')
class BaseCommandParent(object):
"""
A base interface for Pecan commands.
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
"""
arguments = ({
'name': 'config_file',
'help': 'a Pecan configuration file',
'nargs': '?',
'default': None,
},)
def run(self, args):
"""To be implemented by subclasses."""
self.args = args
def load_app(self):
from pecan import load_app
return load_app(self.args.config_file)
BaseCommand = BaseCommandMeta('BaseCommand', (BaseCommandParent,), {
'__doc__': BaseCommandParent.__doc__
})
pecan-1.5.1/pecan/commands/create.py 0000664 0000000 0000000 00000003170 14454530445 0017321 0 ustar 00root root 0000000 0000000 """
Create command for Pecan
"""
import pkg_resources
import logging
from warnings import warn
from pecan.commands import BaseCommand
from pecan.scaffolds import DEFAULT_SCAFFOLD
log = logging.getLogger(__name__)
class ScaffoldManager(object):
""" Used to discover `pecan.scaffold` entry points. """
def __init__(self):
self.scaffolds = {}
self.load_scaffolds()
def load_scaffolds(self):
for ep in pkg_resources.iter_entry_points('pecan.scaffold'):
log.debug('%s loading scaffold %s', self.__class__.__name__, ep)
try:
cmd = ep.load()
cmd.copy_to # ensure existance; catch AttributeError otherwise
except Exception as e: # pragma: nocover
warn(
"Unable to load scaffold %s: %s" % (ep, e), RuntimeWarning
)
continue
self.add({ep.name: cmd})
def add(self, cmd):
self.scaffolds.update(cmd)
class CreateCommand(BaseCommand):
"""
Creates the file layout for a new Pecan scaffolded project.
"""
manager = ScaffoldManager()
arguments = ({
'name': 'project_name',
'help': 'the (package) name of the new project'
}, {
'name': 'template_name',
'metavar': 'template_name',
'help': 'a registered Pecan template',
'nargs': '?',
'default': DEFAULT_SCAFFOLD,
'choices': manager.scaffolds.keys()
})
def run(self, args):
super(CreateCommand, self).run(args)
self.manager.scaffolds[args.template_name]().copy_to(
args.project_name
)
pecan-1.5.1/pecan/commands/serve.py 0000664 0000000 0000000 00000015253 14454530445 0017207 0 ustar 00root root 0000000 0000000 """
Serve command for Pecan.
"""
from __future__ import print_function
import logging
import os
import sys
import threading
import time
import subprocess
from wsgiref.simple_server import WSGIRequestHandler
from pecan.commands import BaseCommand
from pecan import util
logger = logging.getLogger(__name__)
class ServeCommand(BaseCommand):
"""
Serves a Pecan web application.
This command serves a Pecan web application using the provided
configuration file for the server and application.
"""
arguments = BaseCommand.arguments + ({
'name': '--reload',
'help': 'Watch for changes and automatically reload.',
'default': False,
'action': 'store_true'
},)
def run(self, args):
super(ServeCommand, self).run(args)
app = self.load_app()
self.serve(app, app.config)
def create_subprocess(self):
self.server_process = subprocess.Popen(
[arg for arg in sys.argv if arg != '--reload'],
stdout=sys.stdout, stderr=sys.stderr
)
def watch_and_spawn(self, conf):
from watchdog.observers import Observer
from watchdog.events import (
FileSystemEventHandler, FileSystemMovedEvent, FileModifiedEvent,
DirModifiedEvent
)
print('Monitoring for changes...')
self.create_subprocess()
parent = self
class AggressiveEventHandler(FileSystemEventHandler):
lock = threading.Lock()
def should_reload(self, event):
for t in (
FileSystemMovedEvent, FileModifiedEvent, DirModifiedEvent
):
if isinstance(event, t):
return True
return False
def on_modified(self, event):
if self.should_reload(event) and self.lock.acquire(False):
parent.server_process.kill()
parent.create_subprocess()
time.sleep(1)
self.lock.release()
# Determine a list of file paths to monitor
paths = self.paths_to_monitor(conf)
event_handler = AggressiveEventHandler()
for path, recurse in paths:
observer = Observer()
observer.schedule(
event_handler,
path=path,
recursive=recurse
)
observer.start()
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
pass
def paths_to_monitor(self, conf):
paths = []
for package_name in getattr(conf.app, 'modules', []):
module = __import__(package_name, fromlist=['app'])
if hasattr(module, 'app') and hasattr(module.app, 'setup_app'):
paths.append((
os.path.dirname(module.__file__),
True
))
break
paths.append((os.path.dirname(conf.__file__), False))
return paths
def _serve(self, app, conf):
from wsgiref.simple_server import make_server
host, port = conf.server.host, int(conf.server.port)
srv = make_server(
host,
port,
app,
handler_class=PecanWSGIRequestHandler,
)
print('Starting server in PID %s' % os.getpid())
if host == '0.0.0.0':
print(
'serving on 0.0.0.0:%s, view at http://127.0.0.1:%s' %
(port, port)
)
else:
print("serving on http://%s:%s" % (host, port))
try:
srv.serve_forever()
except KeyboardInterrupt:
# allow CTRL+C to shutdown
pass
def serve(self, app, conf):
"""
A very simple approach for a WSGI server.
"""
if self.args.reload:
try:
self.watch_and_spawn(conf)
except ImportError:
print('The `--reload` option requires `watchdog` to be '
'installed.')
print(' $ pip install watchdog')
else:
self._serve(app, conf)
def gunicorn_run():
"""
The ``gunicorn_pecan`` command for launching ``pecan`` applications
"""
try:
from gunicorn.app.wsgiapp import WSGIApplication
except ImportError as exc:
args = exc.args
arg0 = args[0] if args else ''
arg0 += ' (are you sure `gunicorn` is installed?)'
exc.args = (arg0,) + args[1:]
raise
class PecanApplication(WSGIApplication):
def init(self, parser, opts, args):
if len(args) != 1:
parser.error("No configuration file was specified.")
self.cfgfname = os.path.normpath(
os.path.join(os.getcwd(), args[0])
)
self.cfgfname = os.path.abspath(self.cfgfname)
if not os.path.exists(self.cfgfname):
parser.error("Config file not found: %s" % self.cfgfname)
from pecan.configuration import _runtime_conf, set_config
set_config(self.cfgfname, overwrite=True)
# If available, use the host and port from the pecan config file
cfg = {}
if _runtime_conf.get('server'):
server = _runtime_conf['server']
if hasattr(server, 'host') and hasattr(server, 'port'):
cfg['bind'] = '%s:%s' % (
server.host, server.port
)
return cfg
def load(self):
from pecan.deploy import deploy
return deploy(self.cfgfname)
PecanApplication("%(prog)s [OPTIONS] config.py").run()
class PecanWSGIRequestHandler(WSGIRequestHandler, object):
"""
A wsgiref request handler class that allows actual log output depending on
the application configuration.
"""
def __init__(self, *args, **kwargs):
# We set self.path to avoid crashes in log_message() on unsupported
# requests (like "OPTIONS").
self.path = ''
super(PecanWSGIRequestHandler, self).__init__(*args, **kwargs)
def log_message(self, 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, 4XX codes are treated as
warnings, 5XX as errors and everything else as INFO level.
"""
code = args[1][0]
levels = {
'4': 'warning',
'5': 'error'
}
log_handler = getattr(logger, levels.get(code, 'info'))
log_handler(format % args)
pecan-1.5.1/pecan/commands/shell.py 0000664 0000000 0000000 00000012625 14454530445 0017172 0 ustar 00root root 0000000 0000000 """
Shell command for Pecan.
"""
from pecan.commands import BaseCommand
try:
from webtest import TestApp
except ImportError:
TestApp = None
from warnings import warn
import sys
class NativePythonShell(object):
"""
Open an interactive python shell with the Pecan app loaded.
"""
@classmethod
def invoke(cls, ns, banner): # pragma: nocover
"""
:param ns: local namespace
:param banner: interactive shell startup banner
Embed an interactive native python shell.
"""
import code
py_prefix = sys.platform.startswith('java') and 'J' or 'P'
shell_banner = 'Pecan Interactive Shell\n%sython %s\n\n' % \
(py_prefix, sys.version)
shell = code.InteractiveConsole(locals=ns)
try:
import readline # noqa
except ImportError:
pass
shell.interact(shell_banner + banner)
class IPythonShell(object):
"""
Open an interactive ipython shell with the Pecan app loaded.
"""
@classmethod
def invoke(cls, ns, banner): # pragma: nocover
"""
:param ns: local namespace
:param banner: interactive shell startup banner
Embed an interactive ipython shell.
Try the InteractiveShellEmbed API first, fall back on
IPShellEmbed for older IPython versions.
"""
try:
from IPython.frontend.terminal.embed import (
InteractiveShellEmbed
)
# try and load their default profile
from IPython.frontend.terminal.ipapp import (
load_default_config
)
config = load_default_config()
shell = InteractiveShellEmbed(config=config, banner2=banner)
shell(local_ns=ns)
except ImportError:
# Support for the IPython <= 0.10 shell API
from IPython.Shell import IPShellEmbed
shell = IPShellEmbed(argv=[])
shell.set_banner(shell.IP.BANNER + '\n\n' + banner)
shell(local_ns=ns, global_ns={})
class BPythonShell(object):
"""
Open an interactive bpython shell with the Pecan app loaded.
"""
@classmethod
def invoke(cls, ns, banner): # pragma: nocover
"""
:param ns: local namespace
:param banner: interactive shell startup banner
Embed an interactive bpython shell.
"""
from bpython import embed
embed(ns, ['-i'], banner)
class ShellCommand(BaseCommand):
"""
Open an interactive shell with the Pecan app loaded.
Attempt to invoke the specified python shell flavor
(ipython, bpython, etc.). Fall back on the native
python shell if the requested flavor variance is not
installed.
"""
SHELLS = {
'python': NativePythonShell,
'ipython': IPythonShell,
'bpython': BPythonShell,
}
arguments = BaseCommand.arguments + ({
'name': ['--shell', '-s'],
'help': 'which Python shell to use',
'choices': SHELLS.keys(),
'default': 'python'
},)
def run(self, args):
"""
Load the pecan app, prepare the locals, sets the
banner, and invokes the python shell.
"""
super(ShellCommand, self).run(args)
# load the application
app = self.load_app()
# prepare the locals
locs = dict(__name__='pecan-admin')
locs['wsgiapp'] = app
if TestApp:
locs['app'] = TestApp(app)
model = self.load_model(app.config)
if model:
locs['model'] = model
# insert the pecan locals
from pecan import abort, conf, redirect, request, response
locs['abort'] = abort
locs['conf'] = conf
locs['redirect'] = redirect
locs['request'] = request
locs['response'] = response
# prepare the banner
banner = ' The following objects are available:\n'
banner += ' %-10s - This project\'s WSGI App instance\n' % 'wsgiapp'
banner += ' %-10s - The current configuration\n' % 'conf'
if TestApp:
banner += ' %-10s - webtest.TestApp wrapped around wsgiapp\n' % 'app' # noqa
if model:
model_name = getattr(
model,
'__module__',
getattr(model, '__name__', 'model')
)
banner += ' %-10s - Models from %s\n' % ('model', model_name)
self.invoke_shell(locs, banner)
def invoke_shell(self, locs, banner):
"""
Invokes the appropriate flavor of the python shell.
Falls back on the native python shell if the requested
flavor (ipython, bpython,etc) is not installed.
"""
shell = self.SHELLS[self.args.shell]
try:
shell().invoke(locs, banner)
except ImportError as e:
warn((
"%s is not installed, `%s`, "
"falling back to native shell") % (self.args.shell, e),
RuntimeWarning
)
if shell == NativePythonShell:
raise
NativePythonShell().invoke(locs, banner)
def load_model(self, config):
"""
Load the model extension module
"""
for package_name in getattr(config.app, 'modules', []):
module = __import__(package_name, fromlist=['model'])
if hasattr(module, 'model'):
return module.model
return None
pecan-1.5.1/pecan/compat/ 0000775 0000000 0000000 00000000000 14454530445 0015165 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/compat/__init__.py 0000664 0000000 0000000 00000001264 14454530445 0017301 0 ustar 00root root 0000000 0000000 import inspect
import urllib.parse as urlparse # noqa
from urllib.parse import quote, unquote_plus # noqa
from urllib.request import urlopen, URLError # noqa
from html import escape # noqa
izip = zip
def is_bound_method(ob):
return inspect.ismethod(ob) and ob.__self__ is not None
def getargspec(func):
import sys
if sys.version_info < (3, 5):
return inspect.getargspec(func)
from collections import namedtuple
ArgSpec = namedtuple('ArgSpec', 'args varargs keywords defaults')
args, varargs, keywords, defaults = inspect.getfullargspec(func)[:4]
return ArgSpec(args=args, varargs=varargs, keywords=keywords,
defaults=defaults)
pecan-1.5.1/pecan/configuration.py 0000664 0000000 0000000 00000016034 14454530445 0017127 0 ustar 00root root 0000000 0000000 import re
import inspect
import os
import sys
from importlib.machinery import SourceFileLoader
IDENTIFIER = re.compile(r'[a-z_](\w)*$', re.IGNORECASE)
DEFAULT = {
# Server Specific Configurations
'server': {
'port': '8080',
'host': '0.0.0.0'
},
# Pecan Application Configurations
'app': {
'root': None,
'modules': [],
'static_root': 'public',
'template_path': '',
'force_canonical': True
}
}
class ConfigDict(dict):
pass
class Config(object):
'''
Base class for Pecan configurations.
Create a Pecan configuration object from a dictionary or a
filename.
:param conf_dict: A python dictionary to use for the configuration.
:param filename: A filename to use for the configuration.
'''
def __init__(self, conf_dict={}, filename=''):
self.__values__ = {}
self.__file__ = filename
self.update(conf_dict)
def empty(self):
self.__values__ = {}
def update(self, conf_dict):
'''
Updates this configuration with a dictionary.
:param conf_dict: A python dictionary to update this configuration
with.
'''
if isinstance(conf_dict, dict):
iterator = iter(conf_dict.items())
else:
iterator = iter(conf_dict)
for k, v in iterator:
if not IDENTIFIER.match(k):
msg = ("'%s' is not a valid Python identifier,"
"consider using the '__force_dict__' key if requiring "
"a native dictionary")
raise ValueError(msg % k)
cur_val = self.__values__.get(k)
if isinstance(cur_val, Config):
cur_val.update(conf_dict[k])
else:
self[k] = conf_dict[k]
def get(self, attribute, default=None):
try:
return self[attribute]
except KeyError:
return default
def __dictify__(self, obj, prefix):
'''
Private helper method for to_dict.
'''
for k, v in obj.copy().items():
if prefix:
del obj[k]
k = "%s%s" % (prefix, k)
if isinstance(v, Config):
v = self.__dictify__(dict(v), prefix)
obj[k] = v
return obj
def to_dict(self, prefix=None):
'''
Converts recursively the Config object into a valid dictionary.
:param prefix: A string to optionally prefix all key elements in the
returned dictonary.
'''
conf_obj = dict(self)
return self.__dictify__(conf_obj, prefix)
def __getattr__(self, name):
try:
return self.__values__[name]
except KeyError:
msg = "'pecan.conf' object has no attribute '%s'" % name
raise AttributeError(msg)
def __setattr__(self, key, value):
if key not in ('__values__', '__file__'):
self.__setitem__(key, value)
return
super(Config, self).__setattr__(key, value)
def __getitem__(self, key):
return self.__values__[key]
def __setitem__(self, key, value):
if isinstance(value, dict) and not isinstance(value, ConfigDict):
if value.get('__force_dict__'):
del value['__force_dict__']
self.__values__[key] = ConfigDict(value)
else:
self.__values__[key] = Config(value, filename=self.__file__)
elif isinstance(value, str) and '%(confdir)s' in value:
confdir = os.path.dirname(self.__file__) or os.getcwd()
self.__values__[key] = value.replace('%(confdir)s', confdir)
else:
self.__values__[key] = value
def __iter__(self):
return iter(self.__values__.items())
def __dir__(self):
"""
When using dir() returns a list of the values in the config. Note:
This function only works in Python2.6 or later.
"""
return list(self.__values__.keys())
def __repr__(self):
return 'Config(%s)' % str(self.__values__)
def conf_from_file(filepath):
'''
Creates a configuration dictionary from a file.
:param filepath: The path to the file.
'''
abspath = os.path.abspath(os.path.expanduser(filepath))
conf_dict = {}
if not os.path.isfile(abspath):
raise RuntimeError('`%s` is not a file.' % abspath)
# First, make sure the code will actually compile (and has no SyntaxErrors)
with open(abspath, 'rb') as f:
compiled = compile(f.read(), abspath, 'exec')
# Next, attempt to actually import the file as a module.
# This provides more verbose import-related error reporting than exec()
absname, _ = os.path.splitext(abspath)
basepath, module_name = absname.rsplit(os.sep, 1)
SourceFileLoader(module_name, abspath).load_module(module_name)
# If we were able to import as a module, actually exec the compiled code
exec(compiled, globals(), conf_dict)
conf_dict['__file__'] = abspath
return conf_from_dict(conf_dict)
def 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``.
'''
config_path = os.environ.get('PECAN_CONFIG')
if not config_path:
error = "PECAN_CONFIG is not set and " \
"no config file was passed as an argument."
elif not os.path.isfile(config_path):
error = "PECAN_CONFIG was set to an invalid path: %s" % config_path
else:
return config_path
raise RuntimeError(error)
def conf_from_dict(conf_dict):
'''
Creates a configuration dictionary from a dictionary.
:param conf_dict: The configuration dictionary.
'''
conf = Config(filename=conf_dict.get('__file__', ''))
for k, v in iter(conf_dict.items()):
if k.startswith('__'):
continue
elif inspect.ismodule(v):
continue
conf[k] = v
return conf
def initconf():
'''
Initializes the default configuration and exposes it at
``pecan.configuration.conf``, which is also exposed at ``pecan.conf``.
'''
return conf_from_dict(DEFAULT)
def set_config(config, overwrite=False):
'''
Updates the global configuration.
:param config: Can be a dictionary containing configuration, or a string
which represents a (relative) configuration filename.
'''
if config is None:
config = get_conf_path_from_env()
# must be after the fallback other a bad fallback will incorrectly clear
if overwrite is True:
_runtime_conf.empty()
if isinstance(config, str):
config = conf_from_file(config)
_runtime_conf.update(config)
if config.__file__:
_runtime_conf.__file__ = config.__file__
elif isinstance(config, dict):
_runtime_conf.update(conf_from_dict(config))
else:
raise TypeError('%s is neither a dictionary or a string.' % config)
_runtime_conf = initconf()
pecan-1.5.1/pecan/core.py 0000664 0000000 0000000 00000076535 14454530445 0015224 0 ustar 00root root 0000000 0000000 from inspect import Arguments
from itertools import chain, tee
from mimetypes import guess_type, add_type
from os.path import splitext
import logging
import operator
import sys
import types
from webob import (Request as WebObRequest, Response as WebObResponse, exc,
acceptparse)
from webob.multidict import NestedMultiDict
from .compat import urlparse, izip, is_bound_method as ismethod
from .jsonify import encode as dumps
from .secure import handle_security
from .templating import RendererFactory
from .routing import lookup_controller, NonCanonicalPath
from .util import _cfg, getargspec
from .middleware.recursive import ForwardRequestException
# make sure that json is defined in mimetypes
add_type('application/json', '.json', True)
state = None
logger = logging.getLogger(__name__)
class RoutingState(object):
def __init__(self, request, response, app, hooks=[], controller=None,
arguments=None):
self.request = request
self.response = response
self.app = app
self.hooks = hooks
self.controller = controller
self.arguments = arguments
class Request(WebObRequest):
def __getattribute__(self, name):
try:
return WebObRequest.__getattribute__(self, name)
except UnicodeDecodeError as e:
logger.exception(e)
abort(400)
class Response(WebObResponse):
pass
def proxy(key):
class ObjectProxy(object):
explanation_ = AttributeError(
"`pecan.state` is not bound to a context-local context.\n"
"Ensure that you're accessing `pecan.request` or `pecan.response` "
"from within the context of a WSGI `__call__` and that "
"`use_context_locals` = True."
)
def __getattr__(self, attr):
try:
obj = getattr(state, key)
except AttributeError:
raise self.explanation_
return getattr(obj, attr)
def __setattr__(self, attr, value):
obj = getattr(state, key)
return setattr(obj, attr, value)
def __delattr__(self, attr):
obj = getattr(state, key)
return delattr(obj, attr)
def __dir__(self):
obj = getattr(state, key)
return dir(obj)
return ObjectProxy()
request = proxy('request')
response = proxy('response')
def override_template(template, content_type=None):
'''
Call within a controller to override the template that is used in
your response.
:param template: a valid path to a template file, just as you would specify
in an ``@expose``.
:param content_type: a valid MIME type to use for the response.func_closure
'''
request.pecan['override_template'] = template
if content_type:
request.pecan['override_content_type'] = content_type
def abort(status_code, detail='', headers=None, comment=None, **kw):
'''
Raise an HTTP status code, as specified. Useful for returning status
codes like 401 Unauthorized or 403 Forbidden.
:param status_code: The HTTP status code as an integer.
:param detail: The message to send along, as a string.
:param headers: A dictionary of headers to send along with the response.
:param comment: A comment to include in the response.
'''
# If there is a traceback, we need to catch it for a re-raise
try:
_, _, traceback = sys.exc_info()
webob_exception = exc.status_map[status_code](
detail=detail,
headers=headers,
comment=comment,
**kw
)
raise webob_exception.with_traceback(traceback)
finally:
# Per the suggestion of the Python docs, delete the traceback object
del traceback
def redirect(location=None, internal=False, code=None, headers={},
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.
:param location: The HTTP location to redirect to.
:param internal: A boolean indicating whether the redirect should be
internal.
:param code: The HTTP status code to use for the redirect. Defaults to 302.
:param headers: Any HTTP headers to send with the response, as a
dictionary.
:param request: The :class:`pecan.Request` instance to use.
'''
request = request or state.request
if add_slash:
if location is None:
split_url = list(urlparse.urlsplit(request.url))
new_proto = request.environ.get(
'HTTP_X_FORWARDED_PROTO', split_url[0]
)
split_url[0] = new_proto
else:
split_url = urlparse.urlsplit(location)
split_url[2] = split_url[2].rstrip('/') + '/'
location = urlparse.urlunsplit(split_url)
if not headers:
headers = {}
if internal:
if code is not None:
raise ValueError('Cannot specify a code for internal redirects')
request.environ['pecan.recursive.context'] = request.context
raise ForwardRequestException(location)
if code is None:
code = 302
raise exc.status_map[code](location=location, headers=headers)
def render(template, namespace, app=None):
'''
Render the specified template using the Pecan rendering framework
with the specified template namespace as a dictionary. Useful in a
controller where you have no template specified in the ``@expose``.
:param template: The path to your template, as you would specify in
``@expose``.
:param namespace: The namespace to use for rendering the template, as a
dictionary.
:param app: The instance of :class:`pecan.Pecan` to use
'''
app = app or state.app
return app.render(template, namespace)
def load_app(config, **kwargs):
'''
Used to load a ``Pecan`` application and its environment based on passed
configuration.
:param config: Can be a dictionary containing configuration, a string which
represents a (relative) configuration filename
returns a pecan.Pecan object
'''
from .configuration import _runtime_conf, set_config
set_config(config, overwrite=True)
for package_name in getattr(_runtime_conf.app, 'modules', []):
module = __import__(package_name, fromlist=['app'])
if hasattr(module, 'app') and hasattr(module.app, 'setup_app'):
app = module.app.setup_app(_runtime_conf, **kwargs)
app.config = _runtime_conf
return app
raise RuntimeError(
'No app.setup_app found in any of the configured app.modules'
)
class PecanBase(object):
SIMPLEST_CONTENT_TYPES = (
['text/html'],
['text/plain']
)
def __init__(self, root, default_renderer='mako',
template_path='templates', hooks=lambda: [],
custom_renderers={}, extra_template_vars={},
force_canonical=True, guess_content_type_from_ext=True,
context_local_factory=None, request_cls=Request,
response_cls=Response, **kw):
if isinstance(root, str):
root = self.__translate_root__(root)
self.root = root
self.request_cls = request_cls
self.response_cls = response_cls
self.renderers = RendererFactory(custom_renderers, extra_template_vars)
self.default_renderer = default_renderer
# pre-sort these so we don't have to do it per-request
if callable(hooks):
hooks = hooks()
self.hooks = list(sorted(
hooks,
key=operator.attrgetter('priority')
))
self.template_path = template_path
self.force_canonical = force_canonical
self.guess_content_type_from_ext = guess_content_type_from_ext
def __translate_root__(self, item):
'''
Creates a root controller instance from a string root, e.g.,
> __translate_root__("myproject.controllers.RootController")
myproject.controllers.RootController()
:param item: The string to the item
'''
if '.' in item:
parts = item.split('.')
name = '.'.join(parts[:-1])
fromlist = parts[-1:]
module = __import__(name, fromlist=fromlist)
kallable = getattr(module, parts[-1])
msg = "%s does not represent a callable class or function."
if not callable(kallable):
raise TypeError(msg % item)
return kallable()
raise ImportError('No item named %s' % item)
def route(self, req, node, path):
'''
Looks up a controller from a node based upon the specified path.
:param node: The node, such as a root controller object.
:param path: The path to look up on this node.
'''
path = path.split('/')[1:]
try:
node, remainder = lookup_controller(node, path, req)
return node, remainder
except NonCanonicalPath as e:
if self.force_canonical and \
not _cfg(e.controller).get('accept_noncanonical', False):
if req.method == 'POST':
raise RuntimeError(
"You have POSTed to a URL '%s' which "
"requires a slash. Most browsers will not maintain "
"POST data when redirected. Please update your code "
"to POST to '%s/' or set force_canonical to False" %
(req.pecan['routing_path'],
req.pecan['routing_path'])
)
redirect(code=302, add_slash=True, request=req)
return e.controller, e.remainder
def determine_hooks(self, controller=None):
'''
Determines the hooks to be run, in which order.
:param controller: If specified, includes hooks for a specific
controller.
'''
controller_hooks = []
if controller:
controller_hooks = _cfg(controller).get('hooks', [])
if controller_hooks:
return list(
sorted(
chain(controller_hooks, self.hooks),
key=operator.attrgetter('priority')
)
)
return self.hooks
def handle_hooks(self, hooks, hook_type, *args):
'''
Processes hooks of the specified type.
:param hook_type: The type of hook, including ``before``, ``after``,
``on_error``, and ``on_route``.
:param \*args: Arguments to pass to the hooks.
'''
if hook_type not in ['before', 'on_route']:
hooks = reversed(hooks)
for hook in hooks:
result = getattr(hook, hook_type)(*args)
# on_error hooks can choose to return a Response, which will
# be used instead of the standard error pages.
if hook_type == 'on_error' and isinstance(result, WebObResponse):
return result
def get_args(self, state, all_params, remainder, argspec, im_self):
'''
Determines the arguments for a controller based upon parameters
passed the argument specification for the controller.
'''
args = []
varargs = []
kwargs = dict()
valid_args = argspec.args[:]
if ismethod(state.controller) or im_self:
valid_args.pop(0) # pop off `self`
pecan_state = state.request.pecan
remainder = [x for x in remainder if x]
if im_self is not None:
args.append(im_self)
# grab the routing args from nested REST controllers
if 'routing_args' in pecan_state:
remainder = pecan_state['routing_args'] + list(remainder)
del pecan_state['routing_args']
# handle positional arguments
if valid_args and remainder:
args.extend(remainder[:len(valid_args)])
remainder = remainder[len(valid_args):]
valid_args = valid_args[len(args):]
# handle wildcard arguments
if [i for i in remainder if i]:
if not argspec[1]:
abort(404)
varargs.extend(remainder)
# get the default positional arguments
if argspec[3]:
defaults = dict(izip(argspec[0][-len(argspec[3]):], argspec[3]))
else:
defaults = dict()
# handle positional GET/POST params
for name in valid_args:
if name in all_params:
args.append(all_params.pop(name))
elif name in defaults:
args.append(defaults[name])
else:
break
# handle wildcard GET/POST params
if argspec[2]:
for name, value in iter(all_params.items()):
if name not in argspec[0]:
kwargs[name] = value
return args, varargs, kwargs
def render(self, template, namespace):
if template == 'json':
renderer = self.renderers.get('json', self.template_path)
elif ':' in template:
renderer_name, template = template.split(':', 1)
renderer = self.renderers.get(
renderer_name,
self.template_path
)
if renderer is None:
raise RuntimeError(
'support for "%s" was not found; ' % renderer_name +
'supported template engines are %s' % self.renderers.keys()
)
else:
renderer = self.renderers.get(
self.default_renderer,
self.template_path
)
return renderer.render(template, namespace)
def find_controller(self, state):
'''
The main request handler for Pecan applications.
'''
# get a sorted list of hooks, by priority (no controller hooks yet)
req = state.request
pecan_state = req.pecan
# store the routing path for the current application to allow hooks to
# modify it
pecan_state['routing_path'] = path = req.path_info
# handle "on_route" hooks
self.handle_hooks(self.hooks, 'on_route', state)
# lookup the controller, respecting content-type as requested
# by the file extension on the URI
pecan_state['extension'] = None
# attempt to guess the content type based on the file extension
if self.guess_content_type_from_ext \
and not pecan_state['content_type'] \
and '.' in path:
_, extension = splitext(path.rstrip('/'))
# preface with a letter to ensure compat for 2.5
potential_type = guess_type('x' + extension)[0]
if extension and potential_type is not None:
path = ''.join(path.rsplit(extension, 1))
pecan_state['extension'] = extension
pecan_state['content_type'] = potential_type
controller, remainder = self.route(req, self.root, path)
cfg = _cfg(controller)
if cfg.get('generic_handler'):
raise exc.HTTPNotFound
# handle generic controllers
im_self = None
if cfg.get('generic'):
im_self = controller.__self__
handlers = cfg['generic_handlers']
controller = handlers.get(req.method, handlers['DEFAULT'])
handle_security(controller, im_self)
cfg = _cfg(controller)
# add the controller to the state so that hooks can use it
state.controller = controller
# if unsure ask the controller for the default content type
content_types = cfg.get('content_types', {})
if not pecan_state['content_type']:
# attempt to find a best match based on accept headers (if they
# exist)
accept = getattr(req.accept, 'header_value', '*/*') or '*/*'
if accept == '*/*' or (
accept.startswith('text/html,') and
list(content_types.keys()) in self.SIMPLEST_CONTENT_TYPES):
pecan_state['content_type'] = cfg.get(
'content_type',
'text/html'
)
else:
best_default = None
accept_header = acceptparse.create_accept_header(accept)
offers = accept_header.acceptable_offers(
list(content_types.keys())
)
if offers:
# If content type matches exactly use matched type
best_default = offers[0][0]
else:
# If content type doesn't match exactly see if something
# matches when not using parameters
for k in content_types.keys():
if accept.startswith(k):
best_default = k
break
if best_default is None:
msg = "Controller '%s' defined does not support " + \
"content_type '%s'. Supported type(s): %s"
logger.error(
msg % (
controller.__name__,
pecan_state['content_type'],
content_types.keys()
)
)
raise exc.HTTPNotAcceptable()
pecan_state['content_type'] = best_default
elif cfg.get('content_type') is not None and \
pecan_state['content_type'] not in content_types:
msg = "Controller '%s' defined does not support content_type " + \
"'%s'. Supported type(s): %s"
logger.error(
msg % (
controller.__name__,
pecan_state['content_type'],
content_types.keys()
)
)
raise exc.HTTPNotFound
# fetch any parameters
if req.method == 'GET':
params = req.GET
elif req.content_type in ('application/json',
'application/javascript'):
try:
if not isinstance(req.json, dict):
raise TypeError('%s is not a dict' % req.json)
params = NestedMultiDict(req.GET, req.json)
except (TypeError, ValueError):
params = req.params
else:
params = req.params
# fetch the arguments for the controller
args, varargs, kwargs = self.get_args(
state,
params.mixed(),
remainder,
cfg['argspec'],
im_self
)
state.arguments = Arguments(args, varargs, kwargs)
# handle "before" hooks
self.handle_hooks(self.determine_hooks(controller), 'before', state)
return controller, args + varargs, kwargs
def invoke_controller(self, controller, args, kwargs, state):
'''
The main request handler for Pecan applications.
'''
cfg = _cfg(controller)
content_types = cfg.get('content_types', {})
req = state.request
resp = state.response
pecan_state = req.pecan
# If a keyword is supplied via HTTP GET or POST arguments, but the
# function signature does not allow it, just drop it (rather than
# generating a TypeError).
argspec = getargspec(controller)
keys = kwargs.keys()
for key in keys:
if key not in argspec.args and not argspec.keywords:
kwargs.pop(key)
# get the result from the controller
result = controller(*args, **kwargs)
# a controller can return the response object which means they've taken
# care of filling it out
if result is response:
return
elif isinstance(result, WebObResponse):
state.response = result
return
raw_namespace = result
# pull the template out based upon content type and handle overrides
template = content_types.get(pecan_state['content_type'])
# check if for controller override of template
template = pecan_state.get('override_template', template)
if template is None and cfg['explicit_content_type'] is False:
if self.default_renderer == 'json':
template = 'json'
pecan_state['content_type'] = pecan_state.get(
'override_content_type',
pecan_state['content_type']
)
# if there is a template, render it
if template:
if template == 'json':
pecan_state['content_type'] = 'application/json'
result = self.render(template, result)
# If we are in a test request put the namespace where it can be
# accessed directly
if req.environ.get('paste.testing'):
testing_variables = req.environ['paste.testing_variables']
testing_variables['namespace'] = raw_namespace
testing_variables['template_name'] = template
testing_variables['controller_output'] = result
# set the body content
if result and isinstance(result, str):
resp.text = result
elif result:
resp.body = result
if pecan_state['content_type']:
# set the content type
resp.content_type = pecan_state['content_type']
def _handle_empty_response_body(self, state):
# Enforce HTTP 204 for responses which contain no body
if state.response.status_int == 200:
# If the response is a generator...
if isinstance(state.response.app_iter, types.GeneratorType):
# Split the generator into two so we can peek at one of them
# and determine if there is any response body content
a, b = tee(state.response.app_iter)
try:
next(a)
except StopIteration:
# If we hit StopIteration, the body is empty
state.response.status = 204
finally:
state.response.app_iter = b
else:
text = None
if state.response.charset:
# `response.text` cannot be accessed without a valid
# charset (because we don't know which encoding to use)
try:
text = state.response.text
except UnicodeDecodeError:
# If a valid charset is not specified, don't bother
# trying to guess it (because there's obviously
# content, so we know this shouldn't be a 204)
pass
if not any((state.response.body, text)):
state.response.status = 204
if state.response.status_int in (204, 304):
state.response.content_type = None
def __call__(self, environ, start_response):
'''
Implements the WSGI specification for Pecan applications, utilizing
``WebOb``.
'''
# create the request and response object
req = self.request_cls(environ)
resp = self.response_cls()
state = RoutingState(req, resp, self)
environ['pecan.locals'] = {
'request': req,
'response': resp
}
controller = None
# track internal redirects
internal_redirect = False
# handle the request
try:
# add context and environment to the request
req.context = environ.get('pecan.recursive.context', {})
req.pecan = dict(content_type=None)
controller, args, kwargs = self.find_controller(state)
self.invoke_controller(controller, args, kwargs, state)
except Exception as e:
# if this is an HTTP Exception, set it as the response
if isinstance(e, exc.HTTPException):
# if the client asked for JSON, do our best to provide it
accept_header = acceptparse.create_accept_header(
getattr(req.accept, 'header_value', '*/*') or '*/*')
offers = accept_header.acceptable_offers(
('text/plain', 'text/html', 'application/json'))
best_match = offers[0][0] if offers else None
state.response = e
if best_match == 'application/json':
json_body = dumps({
'code': e.status_int,
'title': e.title,
'description': e.detail
})
if isinstance(json_body, str):
e.text = json_body
else:
e.body = json_body
state.response.content_type = best_match
environ['pecan.original_exception'] = e
# note if this is an internal redirect
internal_redirect = isinstance(e, ForwardRequestException)
# if this is not an internal redirect, run error hooks
on_error_result = None
if not internal_redirect:
on_error_result = self.handle_hooks(
self.determine_hooks(state.controller),
'on_error',
state,
e
)
# if the on_error handler returned a Response, use it.
if isinstance(on_error_result, WebObResponse):
state.response = on_error_result
else:
if not isinstance(e, exc.HTTPException):
raise
# if this is an HTTP 405, attempt to specify an Allow header
if isinstance(e, exc.HTTPMethodNotAllowed) and controller:
allowed_methods = _cfg(controller).get('allowed_methods', [])
if allowed_methods:
state.response.allow = sorted(allowed_methods)
finally:
# if this is not an internal redirect, run "after" hooks
if not internal_redirect:
self.handle_hooks(
self.determine_hooks(state.controller),
'after',
state
)
self._handle_empty_response_body(state)
# get the response
return state.response(environ, start_response)
class ExplicitPecan(PecanBase):
def get_args(self, state, all_params, remainder, argspec, im_self):
# When comparing the argspec of the method to GET/POST params,
# ignore the implicit (req, resp) at the beginning of the function
# signature
if hasattr(state.controller, '__self__'):
_repr = '.'.join((
state.controller.__self__.__class__.__module__,
state.controller.__self__.__class__.__name__,
state.controller.__name__
))
else:
_repr = '.'.join((
state.controller.__module__,
state.controller.__name__
))
signature_error = TypeError(
'When `use_context_locals` is `False`, pecan passes an explicit '
'reference to the request and response as the first two arguments '
'to the controller.\nChange the `%s` signature to accept exactly '
'2 initial arguments (req, resp)' % _repr
)
try:
positional = argspec.args[:]
positional.pop(1) # req
positional.pop(1) # resp
argspec = argspec._replace(args=positional)
except IndexError:
raise signature_error
args, varargs, kwargs = super(ExplicitPecan, self).get_args(
state, all_params, remainder, argspec, im_self
)
if ismethod(state.controller):
args = [state.request, state.response] + args
else:
# generic controllers have an explicit self *first*
# (because they're decorated functions, not instance methods)
args[1:1] = [state.request, state.response]
return args, varargs, kwargs
class Pecan(PecanBase):
'''
Pecan application object. Generally created using ``pecan.make_app``,
rather than being created manually.
Creates a Pecan application instance, which is a WSGI application.
:param root: A string representing a root controller object (e.g.,
"myapp.controller.root.RootController")
:param default_renderer: The default template rendering engine to use.
Defaults to mako.
:param template_path: A relative file system path (from the project root)
where template files live. Defaults to 'templates'.
:param hooks: A callable which returns a list of
:class:`pecan.hooks.PecanHook`
:param custom_renderers: Custom renderer objects, as a dictionary keyed
by engine name.
:param extra_template_vars: Any variables to inject into the template
namespace automatically.
:param force_canonical: A boolean indicating if this project should
require canonical URLs.
:param 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.
:param use_context_locals: When `True`, `pecan.request` and
`pecan.response` will be available as
thread-local references.
:param request_cls: Can be used to specify a custom `pecan.request` object.
Defaults to `pecan.Request`.
:param response_cls: Can be used to specify a custom `pecan.response`
object. Defaults to `pecan.Response`.
'''
def __new__(cls, *args, **kw):
if kw.get('use_context_locals') is False:
self = super(Pecan, cls).__new__(ExplicitPecan, *args, **kw)
self.__init__(*args, **kw)
return self
return super(Pecan, cls).__new__(cls)
def __init__(self, *args, **kw):
self.init_context_local(kw.get('context_local_factory'))
super(Pecan, self).__init__(*args, **kw)
def __call__(self, environ, start_response):
try:
state.hooks = []
state.app = self
state.controller = None
state.arguments = None
return super(Pecan, self).__call__(environ, start_response)
finally:
del state.hooks
del state.request
del state.response
del state.controller
del state.arguments
del state.app
def init_context_local(self, local_factory):
global state
if local_factory is None:
from threading import local as local_factory
state = local_factory()
def find_controller(self, _state):
state.request = _state.request
state.response = _state.response
controller, args, kw = super(Pecan, self).find_controller(_state)
state.controller = controller
state.arguments = _state.arguments
return controller, args, kw
def handle_hooks(self, hooks, *args, **kw):
state.hooks = hooks
return super(Pecan, self).handle_hooks(hooks, *args, **kw)
pecan-1.5.1/pecan/decorators.py 0000664 0000000 0000000 00000013302 14454530445 0016420 0 ustar 00root root 0000000 0000000 from inspect import getmembers, isclass, isfunction
from .util import _cfg, getargspec
__all__ = [
'expose', 'transactional', 'accept_noncanonical', 'after_commit',
'after_rollback'
]
def when_for(controller):
def when(method, **kw):
def decorate(f):
_cfg(f)['generic_handler'] = True
controller._pecan['generic_handlers'][method.upper()] = f
controller._pecan['allowed_methods'].append(method.upper())
expose(**kw)(f)
return f
return decorate
return when
def expose(template=None,
generic=False,
route=None,
**kw):
'''
Decorator used to flag controller methods as being "exposed" for
access via HTTP, and to configure that access.
:param 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
:ref:`expose_json`.
:param content_type: The content-type to use for this template.
:param generic: A boolean which flags this as a "generic" controller,
which uses generic functions based upon
``functools.singledispatch`` generic functions. Allows you
to split a single controller into multiple paths based upon
HTTP method.
:param 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 to `some-special-path'.
'''
content_type = kw.get('content_type', 'text/html')
if template == 'json':
content_type = 'application/json'
def decorate(f):
# flag the method as exposed
f.exposed = True
cfg = _cfg(f)
cfg['explicit_content_type'] = 'content_type' in kw
if route:
# This import is here to avoid a circular import issue
from pecan import routing
if cfg.get('generic_handler'):
raise ValueError(
'Path segments cannot be overridden for generic '
'controllers.'
)
routing.route(route, f)
# set a "pecan" attribute, where we will store details
cfg['content_type'] = content_type
cfg.setdefault('template', []).append(template)
cfg.setdefault('content_types', {})[content_type] = template
# handle generic controllers
if generic:
if f.__name__ in ('_default', '_lookup', '_route'):
raise ValueError(
'The special method %s cannot be used as a generic '
'controller' % f.__name__
)
cfg['generic'] = True
cfg['generic_handlers'] = dict(DEFAULT=f)
cfg['allowed_methods'] = []
f.when = when_for(f)
# store the arguments for this controller method
cfg['argspec'] = getargspec(f)
return f
return decorate
def transactional(ignore_redirects=True):
'''
If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
to flag a controller method or class as being wrapped in a transaction,
regardless of HTTP method.
:param ignore_redirects: Indicates if the hook should ignore redirects
for this controller or not.
'''
def deco(f):
if isclass(f):
for meth in [
m[1] for m in getmembers(f)
if isfunction(m[1])
]:
if getattr(meth, 'exposed', False):
_cfg(meth)['transactional'] = True
_cfg(meth)['transactional_ignore_redirects'] = _cfg(
meth
).get(
'transactional_ignore_redirects',
ignore_redirects
)
else:
_cfg(f)['transactional'] = True
_cfg(f)['transactional_ignore_redirects'] = ignore_redirects
return f
return deco
def after_action(action_type, action):
'''
If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
to flag a controller method to perform a callable action after the
action_type is successfully issued.
:param action: The callable to call after the commit is successfully
issued. '''
if action_type not in ('commit', 'rollback'):
raise Exception('action_type (%s) is not valid' % action_type)
def deco(func):
_cfg(func).setdefault('after_%s' % action_type, []).append(action)
return func
return deco
def after_commit(action):
'''
If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
to flag a controller method to perform a callable action after the
commit is successfully issued.
:param action: The callable to call after the commit is successfully
issued.
'''
return after_action('commit', action)
def after_rollback(action):
'''
If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
to flag a controller method to perform a callable action after the
rollback is successfully issued.
:param action: The callable to call after the rollback is successfully
issued.
'''
return after_action('rollback', action)
def accept_noncanonical(func):
'''
Flags a controller method as accepting non-canoncial URLs.
'''
_cfg(func)['accept_noncanonical'] = True
return func
pecan-1.5.1/pecan/deploy.py 0000664 0000000 0000000 00000000266 14454530445 0015554 0 ustar 00root root 0000000 0000000 from .core import load_app
def deploy(config):
"""
Given a config (dictionary of relative filename), returns a configured
WSGI app.
"""
return load_app(config)
pecan-1.5.1/pecan/ext/ 0000775 0000000 0000000 00000000000 14454530445 0014502 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/ext/__init__.py 0000664 0000000 0000000 00000000205 14454530445 0016610 0 ustar 00root root 0000000 0000000 def install():
from pecan.extensions import PecanExtensionImporter
PecanExtensionImporter().install()
install()
del install
pecan-1.5.1/pecan/extensions.py 0000664 0000000 0000000 00000005015 14454530445 0016454 0 ustar 00root root 0000000 0000000 import sys
import pkg_resources
import inspect
import logging
log = logging.getLogger(__name__)
class PecanExtensionMissing(ImportError):
pass
class PecanExtensionImporter(object):
"""
Short circuits imports for extensions.
This is used in combination with ``pecan.ext`` so that when a user does
``from pecan.ext import foo``, it will attempt to map ``foo`` to a
registered setuptools entry point in some other (Pecan extension) project.
Conversely, an extension developer may define an entry point in his
``setup.py``, e.g.,
setup(
...
entry_points='''
[pecan.extension]
celery = pecancelery.lib.core
'''
)
This is mostly for convenience and consistency. In this way, Pecan can
maintain an ecosystem of extensions that share a common namespace,
``pecan.ext``, while still maintaining backwards compatibility for simple
package names (e.g., ``pecancelery``).
"""
extension_module = 'pecan.ext'
prefix = extension_module + '.'
def install(self):
if self not in sys.meta_path:
sys.meta_path.append(self)
def __eq__(self, b):
return self.__class__.__module__ == b.__class__.__module__ and \
self.__class__.__name__ == b.__class__.__name__
def __ne__(self, b):
return not self.__eq__(b)
def find_module(self, fullname, path=None):
if fullname.startswith(self.prefix):
return self
def load_module(self, fullname):
if fullname in sys.modules:
return self
extname = fullname.split(self.prefix)[1]
module = self.find_module_for_extension(extname)
realname = module.__name__
try:
__import__(realname)
except ImportError:
raise sys.exc_info()
module = sys.modules[fullname] = sys.modules[realname]
if '.' not in extname:
setattr(sys.modules[self.extension_module], extname, module)
return module
def find_module_for_extension(self, name):
for ep in pkg_resources.iter_entry_points('pecan.extension'):
if ep.name != name:
continue
log.debug('%s loading extension %s', self.__class__.__name__, ep)
module = ep.load()
if not inspect.ismodule(module):
log.debug('%s is not a module, skipping...' % module)
continue
return module
raise PecanExtensionMissing(
'The `pecan.ext.%s` extension is not installed.' % name
)
pecan-1.5.1/pecan/hooks.py 0000664 0000000 0000000 00000031011 14454530445 0015373 0 ustar 00root root 0000000 0000000 import builtins
import types
import sys
from inspect import getmembers
from webob.exc import HTTPFound
from .util import iscontroller, _cfg
__all__ = [
'PecanHook', 'TransactionHook', 'HookController',
'RequestViewerHook'
]
def walk_controller(root_class, controller, hooks, seen=None):
seen = seen or set()
if type(controller) not in vars(builtins).values():
# Avoid recursion loops
try:
if controller in seen:
return
seen.add(controller)
except TypeError:
# If we discover an unhashable item (like a list), it's not
# something that we want to traverse because it's not the sort of
# thing we would add a hook to
return
for hook in getattr(controller, '__hooks__', []):
# Append hooks from controller class definition
hooks.add(hook)
for name, value in getmembers(controller):
if name == 'controller':
continue
if name.startswith('__') and name.endswith('__'):
continue
if iscontroller(value):
for hook in hooks:
value._pecan.setdefault('hooks', set()).add(hook)
elif hasattr(value, '__class__'):
# Skip non-exposed methods that are defined in parent classes;
# they're internal implementation details of that class, and
# not actual routable controllers, so we shouldn't bother
# assigning hooks to them.
if (
isinstance(value, types.MethodType) and
any(filter(lambda c: value.__func__ in c.__dict__.values(),
value.__self__.__class__.mro()[1:]))
):
continue
walk_controller(root_class, value, hooks, seen)
class HookControllerMeta(type):
'''
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 member of the controller's namespace.
'''
def __init__(cls, name, bases, dict_):
hooks = set(dict_.get('__hooks__', []))
for base in bases:
# Add hooks from parent class and mixins
for hook in getattr(base, '__hooks__', []):
hooks.add(hook)
walk_controller(cls, cls, hooks)
HookController = HookControllerMeta(
'HookController',
(object,),
{'__doc__': ("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.")}
)
class PecanHook(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.
'''
priority = 100
def on_route(self, state):
'''
Override this method to create a hook that gets called upon
the start of routing.
:param state: The Pecan ``state`` object for the current request.
'''
return
def before(self, state):
'''
Override this method to create a hook that gets called after
routing, but before the request gets passed to your controller.
:param state: The Pecan ``state`` object for the current request.
'''
return
def after(self, state):
'''
Override this method to create a hook that gets called after
the request has been handled by the controller.
:param state: The Pecan ``state`` object for the current request.
'''
return
def on_error(self, state, e):
'''
Override this method to create a hook that gets called upon
an exception being raised in your controller.
:param state: The Pecan ``state`` object for the current request.
:param e: The ``Exception`` object that was raised.
'''
return
class TransactionHook(PecanHook):
'''
:param start: A callable that will bind to a writable database and
start a transaction.
:param start_ro: A callable that will bind to a readable database.
:param commit: A callable that will commit the active transaction.
:param rollback: A callable that will roll back the active
transaction.
:param 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.
'''
def __init__(self, start, start_ro, commit, rollback, clear):
self.start = start
self.start_ro = start_ro
self.commit = commit
self.rollback = rollback
self.clear = clear
def is_transactional(self, 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.
:param state: The Pecan state object for the current request.
'''
controller = getattr(state, 'controller', None)
if controller:
force_transactional = _cfg(controller).get('transactional', False)
else:
force_transactional = False
if state.request.method not in ('GET', 'HEAD') or force_transactional:
return True
return False
def on_route(self, state):
state.request.error = False
if self.is_transactional(state):
state.request.transactional = True
self.start()
else:
state.request.transactional = False
self.start_ro()
def before(self, state):
if self.is_transactional(state) \
and not getattr(state.request, 'transactional', False):
self.clear()
state.request.transactional = True
self.start()
def on_error(self, state, e):
#
# If we should ignore redirects,
# (e.g., shouldn't consider them rollback-worthy)
# don't set `state.request.error = True`.
#
trans_ignore_redirects = (
state.request.method not in ('GET', 'HEAD')
)
if state.controller is not None:
trans_ignore_redirects = (
_cfg(state.controller).get(
'transactional_ignore_redirects',
trans_ignore_redirects
)
)
if type(e) is HTTPFound and trans_ignore_redirects is True:
return
state.request.error = True
def after(self, state):
if getattr(state.request, 'transactional', False):
action_name = None
if state.request.error:
action_name = 'after_rollback'
self.rollback()
else:
action_name = 'after_commit'
self.commit()
#
# If a controller was routed to, find any
# after_* actions it may have registered, and perform
# them.
#
if action_name:
controller = getattr(state, 'controller', None)
if controller is not None:
actions = _cfg(controller).get(action_name, [])
for action in actions:
action()
self.clear()
class RequestViewerHook(PecanHook):
'''
:param config: A (optional) dictionary that can hold ``items`` and/or
``blacklist`` keys.
:param writer: The stream writer to use. Can redirect output to other
streams as long as the passed in stream has a
``write`` callable method.
:param terminal: Outputs to the chosen stream writer (usually
the terminal)
:param 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.
**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:
* path
* status
* method
* controller
* params
* hooks
.. 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
:ref:`requestviewerhook`
'''
available = ['path', 'status', 'method', 'controller', 'params', 'hooks']
def __init__(self, config=None, writer=sys.stdout, terminal=True,
headers=True):
if not config:
self.config = {'items': self.available}
else:
if config.__class__.__name__ == 'Config':
self.config = config.to_dict()
else:
self.config = config
self.writer = writer
self.items = self.config.get('items', self.available)
self.blacklist = self.config.get('blacklist', [])
self.terminal = terminal
self.headers = headers
def after(self, state):
# Default and/or custom response information
responses = {
'controller': lambda self, state: self.get_controller(state),
'method': lambda self, state: state.request.method,
'path': lambda self, state: state.request.path,
'params': lambda self, state: [
(p[0].encode('utf-8'), p[1].encode('utf-8'))
for p in state.request.params.items()
],
'status': lambda self, state: state.response.status,
'hooks': lambda self, state: self.format_hooks(state.app.hooks),
}
is_available = [
i for i in self.items
if i in self.available or hasattr(state.request, i)
]
terminal = []
headers = []
will_skip = [
i for i in self.blacklist
if state.request.path.startswith(i)
]
if will_skip:
return
for request_info in is_available:
try:
value = responses.get(request_info)
if not value:
value = getattr(state.request, request_info)
else:
value = value(self, state)
except Exception as e:
value = e
terminal.append('%-12s - %s\n' % (request_info, value))
headers.append((request_info, value))
if self.terminal:
self.writer.write(''.join(terminal))
self.writer.write('\n\n')
if self.headers:
for h in headers:
key = str(h[0])
value = str(h[1])
name = 'X-Pecan-%s' % key
state.response.headers[name] = value
def get_controller(self, state):
'''
Retrieves the actual controller name from the application
Specific to Pecan (not available in the request object)
'''
path = state.request.pecan['routing_path'].split('/')[1:]
return state.controller.__str__().split()[2]
def format_hooks(self, hooks):
'''
Tries to format the hook objects to be more readable
Specific to Pecan (not available in the request object)
'''
str_hooks = [str(i).split()[0].strip('<') for i in hooks]
return [i.split('.')[-1] for i in str_hooks if '.' in i]
pecan-1.5.1/pecan/jsonify.py 0000664 0000000 0000000 00000011455 14454530445 0015743 0 ustar 00root root 0000000 0000000 from datetime import datetime, date
from decimal import Decimal
from json import JSONEncoder
# depending on the version WebOb might have 2 types of dicts
try:
# WebOb <= 1.1.1
from webob.multidict import MultiDict, UnicodeMultiDict
webob_dicts = (MultiDict, UnicodeMultiDict) # pragma: no cover
except ImportError: # pragma no cover
# WebOb >= 1.2
from webob.multidict import MultiDict
webob_dicts = (MultiDict,)
try:
from functools import singledispatch
except ImportError: # pragma: no cover
from singledispatch import singledispatch
try:
import sqlalchemy # noqa
try:
# SQLAlchemy 2.0 support
from sqlalchemy.engine import CursorResult as ResultProxy
from sqlalchemy.engine import Row as RowProxy
except ImportError:
from sqlalchemy.engine.result import ResultProxy, RowProxy
except ImportError:
# dummy classes since we don't have SQLAlchemy installed
class ResultProxy(object): # noqa
pass
class RowProxy(object): # noqa
pass
try:
from sqlalchemy.engine.cursor import LegacyCursorResult, LegacyRow
except ImportError: # pragma no cover
# dummy classes since we don't have SQLAlchemy installed
# or we're using SQLAlchemy < 1.4
class LegacyCursorResult(object): # noqa
pass
class LegacyRow(object): # noqa
pass
#
# encoders
#
def is_saobject(obj):
return hasattr(obj, '_sa_class_manager')
class GenericJSON(JSONEncoder):
'''
Generic JSON encoder. Makes several attempts to correctly JSONify
requested response objects.
'''
def default(self, obj):
'''
Converts an object and returns a ``JSON``-friendly structure.
:param 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__()
* date and datetime objects
returns the object cast to str
* Decimal objects
returns the object cast to float
* SQLAlchemy objects
returns a copy of the object.__dict__ with internal SQLAlchemy
parameters removed
* SQLAlchemy ResultProxy objects
Casts the iterable ResultProxy into 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 RowProxy objects
Casts the RowProxy cursor object into a dictionary, probably
losing its ordered dictionary behavior in the process but
making it JSON-friendly.
* webob_dicts objects
returns webob_dicts.mixed() dictionary, which is guaranteed
to be JSON-friendly.
'''
if hasattr(obj, '__json__') and callable(obj.__json__):
return obj.__json__()
elif isinstance(obj, (date, datetime)):
return str(obj)
elif isinstance(obj, Decimal):
# XXX What to do about JSONEncoder crappy handling of Decimals?
# SimpleJSON has better Decimal encoding than the std lib
# but only in recent versions
return float(obj)
elif is_saobject(obj):
props = {}
for key in obj.__dict__:
if not key.startswith('_sa_'):
props[key] = getattr(obj, key)
return props
elif isinstance(obj, ResultProxy):
props = dict(rows=list(obj), count=obj.rowcount)
if props['count'] < 0:
props['count'] = len(props['rows'])
return props
elif isinstance(obj, LegacyCursorResult):
rows = [dict(row._mapping) for row in obj.fetchall()]
return {'count': len(rows), 'rows': rows}
elif isinstance(obj, LegacyRow):
return dict(obj._mapping)
elif isinstance(obj, RowProxy):
if obj.__class__.__name__ == 'Row':
# SQLAlchemy 2.0 support
obj = obj._mapping
return dict(obj)
elif isinstance(obj, webob_dicts):
return obj.mixed()
else:
return JSONEncoder.default(self, obj)
_default = GenericJSON()
def with_when_type(f):
# Add some backwards support for simplegeneric's API
f.when_type = f.register
return f
@with_when_type
@singledispatch
def jsonify(obj):
return _default.default(obj)
class GenericFunctionJSON(GenericJSON):
def default(self, obj):
return jsonify(obj)
_instance = GenericFunctionJSON()
def encode(obj):
return _instance.encode(obj)
pecan-1.5.1/pecan/log.py 0000664 0000000 0000000 00000003206 14454530445 0015036 0 ustar 00root root 0000000 0000000 import logging
from logutils.colorize import ColorizingStreamHandler
class DefaultColorizer(ColorizingStreamHandler):
level_map = {
logging.DEBUG: (None, 'blue', True),
logging.INFO: (None, None, True),
logging.WARNING: (None, 'yellow', True),
logging.ERROR: (None, 'red', True),
logging.CRITICAL: (None, 'red', True),
}
class ColorFormatter(logging.Formatter):
"""
A very basic logging formatter that not only applies color to the
levels of the ouput but can also add padding to the the level names so that
they do not alter the visuals of logging when presented on the terminal.
The padding is provided by a convenient keyword that adds padding to the
``levelname`` so that log output is easier to follow::
%(padded_color_levelname)s
Which would result in log level output that looks like::
[INFO ]
[WARNING ]
[ERROR ]
[DEBUG ]
[CRITICAL]
If colored output is not supported, it falls back to non-colored output
without any extra settings.
"""
def __init__(self, _logging=None, colorizer=None, *a, **kw):
self.logging = _logging or logging
self.color = colorizer or DefaultColorizer()
logging.Formatter.__init__(self, *a, **kw)
def format(self, record):
levelname = record.levelname
padded_level = '%-8s' % levelname
record.color_levelname = self.color.colorize(levelname, record)
record.padded_color_levelname = self.color.colorize(
padded_level,
record
)
return self.logging.Formatter.format(self, record)
pecan-1.5.1/pecan/middleware/ 0000775 0000000 0000000 00000000000 14454530445 0016017 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/middleware/__init__.py 0000664 0000000 0000000 00000000111 14454530445 0020121 0 ustar 00root root 0000000 0000000 from . import errordocument
from . import recursive
from . import static
pecan-1.5.1/pecan/middleware/debug.py 0000664 0000000 0000000 00000005642 14454530445 0017466 0 ustar 00root root 0000000 0000000 __CONFIG_HELP__ = b'''
''' # noqa
try:
import re
from backlash.debug import DebuggedApplication
class DebugMiddleware(DebuggedApplication):
body_re = re.compile(b'(]*>)', re.I)
def debug_application(self, environ, start_response):
for part in super(DebugMiddleware, self).debug_application(
environ, start_response
):
yield self.body_re.sub(b'<1>%s' % __CONFIG_HELP__, part)
except ImportError:
import logging
from traceback import print_exc
from pprint import pformat
from mako.template import Template
from io import StringIO
from webob import Response
from webob.exc import HTTPException
LOG = logging.getLogger(__file__)
debug_template_raw = '''
Pecan - Application Error
An error occurred!
%(config_help)s
Pecan offers support for interactive debugging by installing the backlash package:
pip install backlash
...and reloading this page.
Traceback
${traceback}
WSGI Environment
${environment}
''' % {'config_help': __CONFIG_HELP__} # noqa
debug_template = Template(debug_template_raw)
class DebugMiddleware(object):
def __init__(self, app, *args, **kwargs):
self.app = app
def __call__(self, environ, start_response):
try:
return self.app(environ, start_response)
except Exception as exc:
# get a formatted exception
out = StringIO()
print_exc(file=out)
LOG.exception(exc)
# get formatted WSGI environment
formatted_environ = pformat(environ)
# render our template
result = debug_template.render(
traceback=out.getvalue(),
environment=formatted_environ
)
# construct and return our response
response = Response()
if isinstance(exc, HTTPException):
response.status_int = exc.status
else:
response.status_int = 500
response.unicode_body = result
return response(environ, start_response)
pecan-1.5.1/pecan/middleware/errordocument.py 0000664 0000000 0000000 00000005000 14454530445 0021254 0 ustar 00root root 0000000 0000000 import sys
from .recursive import ForwardRequestException, RecursionLoop
class StatusPersist(object):
def __init__(self, app, status, url):
self.app = app
self.status = status
self.url = url
def __call__(self, environ, start_response):
def keep_status_start_response(status, headers, exc_info=None):
return start_response(self.status, headers, exc_info)
parts = self.url.split('?')
environ['PATH_INFO'] = parts[0]
if len(parts) > 1:
environ['QUERY_STRING'] = parts[1]
else:
environ['QUERY_STRING'] = ''
try:
return self.app(environ, keep_status_start_response)
except RecursionLoop as e:
environ['wsgi.errors'].write(
'Recursion error getting error page: %s\n' % e
)
keep_status_start_response(
'500 Server Error',
[('Content-type', 'text/plain')],
sys.exc_info()
)
return [
b'Error: %s. (Error page could not be fetched)' % (
self.status.encode('utf-8')
)
]
class ErrorDocumentMiddleware(object):
'''
Intersects HTTP response status code, looks it up in the error map defined
in the Pecan app config.py, and routes to the controller assigned to that
status.
'''
def __init__(self, app, error_map):
self.app = app
self.error_map = error_map
def __call__(self, environ, start_response):
def replacement_start_response(status, headers, exc_info=None):
'''
Overrides the default response if the status is defined in the
Pecan app error map configuration.
'''
try:
status_code = int(status.split(' ')[0])
except (ValueError, TypeError): # pragma: nocover
raise Exception((
'ErrorDocumentMiddleware received an invalid '
'status %s' % status
))
if status_code in self.error_map:
def factory(app):
return StatusPersist(
app,
status,
self.error_map[status_code]
)
raise ForwardRequestException(factory=factory)
return start_response(status, headers, exc_info)
app_iter = self.app(environ, replacement_start_response)
return app_iter
pecan-1.5.1/pecan/middleware/recursive.py 0000664 0000000 0000000 00000015444 14454530445 0020410 0 ustar 00root root 0000000 0000000 # (c) 2005 Ian Bicking and contributors; written for Paste
# Licensed under the MIT license:
# http://www.opensource.org/licenses/mit-license.php
"""
Middleware to make internal requests and forward requests internally.
Raise ``ForwardRequestException(new_path_info)`` to do a forward
(aborting the current request).
"""
__all__ = ['RecursiveMiddleware']
class RecursionLoop(AssertionError):
# Subclasses AssertionError for legacy reasons
"""Raised when a recursion enters into a loop"""
class CheckForRecursionMiddleware(object):
def __init__(self, app, env):
self.app = app
self.env = env
def __call__(self, environ, start_response):
path_info = environ.get('PATH_INFO', '')
if path_info in self.env.get('pecan.recursive.old_path_info', []):
raise RecursionLoop(
"Forwarding loop detected; %r visited twice (internal "
"redirect path: %s)"
% (path_info, self.env['pecan.recursive.old_path_info'])
)
old_path_info = self.env.setdefault(
'pecan.recursive.old_path_info', []
)
old_path_info.append(self.env.get('PATH_INFO', ''))
return self.app(environ, start_response)
class RecursiveMiddleware(object):
"""
A WSGI middleware that allows for recursive and forwarded calls.
All these calls go to the same 'application', but presumably that
application acts differently with different URLs. The forwarded
URLs must be relative to this container.
"""
def __init__(self, application, global_conf=None):
self.application = application
def __call__(self, environ, start_response):
my_script_name = environ.get('SCRIPT_NAME', '')
environ['pecan.recursive.script_name'] = my_script_name
try:
return self.application(environ, start_response)
except ForwardRequestException as e:
middleware = CheckForRecursionMiddleware(
e.factory(self), environ)
return middleware(environ, start_response)
class ForwardRequestException(Exception):
"""
Used to signal that a request should be forwarded to a different location.
``url``
The URL to forward to starting with a ``/`` and relative to
``RecursiveMiddleware``. URL fragments can also contain query strings
so ``/error?code=404`` would be a valid URL fragment.
``environ``
An altertative WSGI environment dictionary to use for the forwarded
request. If specified is used *instead* of the ``url_fragment``
``factory``
If specifed ``factory`` is used instead of ``url`` or ``environ``.
``factory`` is a callable that takes a WSGI application object
as the first argument and returns an initialised WSGI middleware
which can alter the forwarded response.
Basic usage (must have ``RecursiveMiddleware`` present) :
.. code-block:: python
from pecan.middleware.recursive import ForwardRequestException
def app(environ, start_response):
if environ['PATH_INFO'] == '/hello':
start_response("200 OK", [('Content-type', 'text/plain')])
return ['Hello World!']
elif environ['PATH_INFO'] == '/error':
start_response("404 Not Found",
[('Content-type', 'text/plain')]
)
return ['Page not found']
else:
raise ForwardRequestException('/error')
from pecan.middleware.recursive import RecursiveMiddleware
app = RecursiveMiddleware(app)
If you ran this application and visited ``/hello`` you would get a
``Hello World!`` message. If you ran the application and visited
``/not_found`` a ``ForwardRequestException`` would be raised and the caught
by the ``RecursiveMiddleware``. The ``RecursiveMiddleware`` would then
return the headers and response from the ``/error`` URL but would display
a ``404 Not found`` status message.
You could also specify an ``environ`` dictionary instead of a url. Using
the same example as before:
.. code-block:: python
def app(environ, start_response):
... same as previous example ...
else:
new_environ = environ.copy()
new_environ['PATH_INFO'] = '/error'
raise ForwardRequestException(environ=new_environ)
"""
def __init__(self, url=None, environ={}, factory=None, path_info=None):
# Check no incompatible options have been chosen
if factory and url:
raise TypeError(
'You cannot specify factory and a url in '
'ForwardRequestException'
) # pragma: nocover
elif factory and environ:
raise TypeError(
'You cannot specify factory and environ in '
'ForwardRequestException'
) # pragma: nocover
if url and environ:
raise TypeError(
'You cannot specify environ and url in '
'ForwardRequestException'
) # pragma: nocover
# set the path_info or warn about its use.
if path_info:
self.path_info = path_info
# If the url can be treated as a path_info do that
if url and '?' not in str(url):
self.path_info = url
# Base middleware
class ForwardRequestExceptionMiddleware(object):
def __init__(self, app):
self.app = app
# Otherwise construct the appropriate middleware factory
if hasattr(self, 'path_info'):
p = self.path_info
def factory_pi(app):
class PathInfoForward(ForwardRequestExceptionMiddleware):
def __call__(self, environ, start_response):
environ['PATH_INFO'] = p
return self.app(environ, start_response)
return PathInfoForward(app)
self.factory = factory_pi
elif url:
def factory_url(app):
class URLForward(ForwardRequestExceptionMiddleware):
def __call__(self, environ, start_response):
environ['PATH_INFO'] = url.split('?')[0]
environ['QUERY_STRING'] = url.split('?')[1]
return self.app(environ, start_response)
return URLForward(app)
self.factory = factory_url
elif environ:
def factory_env(app):
class EnvironForward(ForwardRequestExceptionMiddleware):
def __call__(self, environ_, start_response):
return self.app(environ, start_response)
return EnvironForward(app)
self.factory = factory_env
else:
self.factory = factory
pecan-1.5.1/pecan/middleware/static.py 0000664 0000000 0000000 00000012724 14454530445 0017666 0 ustar 00root root 0000000 0000000 """
This code is adapted from the Werkzeug project, under the BSD license.
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
import os
import mimetypes
from datetime import datetime
from time import gmtime
class FileWrapper(object):
"""This class can be used to convert a :class:`file`-like object into
an iterable. It yields `buffer_size` blocks until the file is fully
read.
You should not use this class directly but rather use the
:func:`wrap_file` function that uses the WSGI server's file wrapper
support if it's available.
:param file: a :class:`file`-like object with a :meth:`~file.read` method.
:param buffer_size: number of bytes for one iteration.
"""
def __init__(self, file, buffer_size=8192):
self.file = file
self.buffer_size = buffer_size
def close(self):
if hasattr(self.file, 'close'):
self.file.close()
def __iter__(self):
return self
def __next__(self):
data = self.file.read(self.buffer_size)
if data:
return data
raise StopIteration()
def wrap_file(environ, file, buffer_size=8192):
"""Wraps a file. This uses the WSGI server's file wrapper if available
or otherwise the generic :class:`FileWrapper`.
If the file wrapper from the WSGI server is used it's important to not
iterate over it from inside the application but to pass it through
unchanged.
More information about file wrappers are available in :pep:`333`.
:param file: a :class:`file`-like object with a :meth:`~file.read` method.
:param buffer_size: number of bytes for one iteration.
"""
return environ.get('wsgi.file_wrapper', FileWrapper)(file, buffer_size)
def _dump_date(d, delim):
"""Used for `http_date` and `cookie_date`."""
if d is None:
d = gmtime()
elif isinstance(d, datetime):
d = d.utctimetuple()
elif isinstance(d, (int, float)):
d = gmtime(d)
return '%s, %02d%s%s%s%s %02d:%02d:%02d GMT' % (
('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')[d.tm_wday],
d.tm_mday, delim,
('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec')[d.tm_mon - 1],
delim, str(d.tm_year), d.tm_hour, d.tm_min, d.tm_sec
)
def http_date(timestamp=None):
"""Formats the time to match the RFC1123 date format.
Accepts a floating point number expressed in seconds since the epoch in, a
datetime object or a timetuple. All times in UTC.
Outputs a string in the format ``Wdy, DD Mon YYYY HH:MM:SS GMT``.
:param timestamp: If provided that date is used, otherwise the current.
"""
return _dump_date(timestamp, ' ')
class StaticFileMiddleware(object):
"""A WSGI middleware that provides static content for development
environments.
Currently the middleware does not support non ASCII filenames. If the
encoding on the file system happens to be the encoding of the URI it may
work but this could also be by accident. We strongly suggest using ASCII
only file names for static files.
The middleware will guess the mimetype using the Python `mimetype`
module. If it's unable to figure out the charset it will fall back
to `fallback_mimetype`.
:param app: the application to wrap. If you don't want to wrap an
application you can pass it :exc:`NotFound`.
:param directory: the directory to serve up.
:param fallback_mimetype: the fallback mimetype for unknown files.
"""
def __init__(self, app, directory, fallback_mimetype='text/plain'):
self.app = app
self.loader = self.get_directory_loader(directory)
self.fallback_mimetype = fallback_mimetype
def _opener(self, filename):
return lambda: (
open(filename, 'rb'),
datetime.utcfromtimestamp(os.path.getmtime(filename)),
int(os.path.getsize(filename))
)
def get_directory_loader(self, directory):
def loader(path):
path = path or directory
if path is not None:
path = os.path.join(directory, path)
if os.path.isfile(path):
return os.path.basename(path), self._opener(path)
return None, None
return loader
def __call__(self, environ, start_response):
# sanitize the path for non unix systems
cleaned_path = environ.get('PATH_INFO', '').strip('/')
for sep in os.sep, os.altsep:
if sep and sep != '/':
cleaned_path = cleaned_path.replace(sep, '/')
path = '/'.join([''] + [x for x in cleaned_path.split('/')
if x and x != '..'])
# attempt to find a loader for the file
real_filename, file_loader = self.loader(path[1:])
if file_loader is None:
return self.app(environ, start_response)
# serve the file with the appropriate name if we found it
guessed_type = mimetypes.guess_type(real_filename)
mime_type = guessed_type[0] or self.fallback_mimetype
f, mtime, file_size = file_loader()
headers = [('Date', http_date())]
headers.append(('Cache-Control', 'public'))
headers.extend((
('Content-Type', mime_type),
('Content-Length', str(file_size)),
('Last-Modified', http_date(mtime))
))
start_response('200 OK', headers)
return wrap_file(environ, f)
pecan-1.5.1/pecan/rest.py 0000664 0000000 0000000 00000036001 14454530445 0015231 0 ustar 00root root 0000000 0000000 from inspect import ismethod, getmembers
import warnings
from webob import exc
from .core import abort
from .decorators import expose
from .routing import lookup_controller, handle_lookup_traversal
from .util import iscontroller, getargspec
class RestController(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
:ref:`rest`.
'''
_custom_actions = {}
def __new__(cls, *args, **kwargs):
"""
RestController does not support the `route` argument to
:func:`~pecan.decorators.expose`
Implement this with __new__ rather than a metaclass, because it's very
common for pecan users to mixin RestController (with other bases that
have their own metaclasses).
"""
for name, value in getmembers(cls):
if iscontroller(value) and getattr(value, 'custom_route', None):
raise ValueError(
'Path segments cannot be used in combination with '
'pecan.rest.RestController. Remove the `route` argument '
'to @pecan.expose on %s.%s.%s' % (
cls.__module__, cls.__name__, value.__name__
)
)
# object.__new__ will error if called with extra arguments, and either
# __new__ is overridden or __init__ is not overridden;
# https://hg.python.org/cpython/file/78d36d54391c/Objects/typeobject.c#l3034
# In PY3, this is a TypeError
new = super(RestController, cls).__new__
if new is object.__new__:
return new(cls)
return new(cls, *args, **kwargs)
def _get_args_for_controller(self, controller):
"""
Retrieve the arguments we actually care about. For Pecan applications
that utilize thread locals, we should truncate the first argument,
`self`. For applications that explicitly pass request/response
references as the first controller arguments, we should truncate the
first three arguments, `self, req, resp`.
"""
argspec = getargspec(controller)
from pecan import request
try:
request.path
except AttributeError:
return argspec.args[3:]
return argspec.args[1:]
def _handle_bad_rest_arguments(self, controller, remainder, request):
"""
Ensure that the argspec for a discovered controller actually matched
the positional arguments in the request path. If not, raise
a webob.exc.HTTPBadRequest.
"""
argspec = self._get_args_for_controller(controller)
fixed_args = len(argspec) - len(
request.pecan.get('routing_args', [])
)
if len(remainder) < fixed_args:
# For controllers that are missing intermediate IDs
# (e.g., /authors/books vs /authors/1/books), return a 404 for an
# invalid path.
abort(404)
def _lookup_child(self, remainder):
"""
Lookup a child controller with a named path (handling Unicode paths
properly for Python 2).
"""
try:
controller = getattr(self, remainder, None)
except UnicodeEncodeError:
return None
return controller
@expose()
def _route(self, args, request=None):
'''
Routes a request to the appropriate controller and returns its result.
Performs a bit of validation - refuses to route delete and put actions
via a GET request).
'''
if request is None:
from pecan import request
# convention uses "_method" to handle browser-unsupported methods
method = request.params.get('_method', request.method).lower()
# make sure DELETE/PUT requests don't use GET
if request.method == 'GET' and method in ('delete', 'put'):
abort(405)
# check for nested controllers
result = self._find_sub_controllers(args, request)
if result:
return result
# handle the request
handler = getattr(
self,
'_handle_%s' % method,
self._handle_unknown_method
)
try:
if len(getargspec(handler).args) == 3:
result = handler(method, args)
else:
result = handler(method, args, request)
#
# If the signature of the handler does not match the number
# of remaining positional arguments, attempt to handle
# a _lookup method (if it exists)
#
argspec = self._get_args_for_controller(result[0])
num_args = len(argspec)
if num_args < len(args):
_lookup_result = self._handle_lookup(args, request)
if _lookup_result:
return _lookup_result
except (exc.HTTPClientError, exc.HTTPNotFound,
exc.HTTPMethodNotAllowed) as e:
#
# If the matching handler results in a 400, 404, or 405, attempt to
# handle a _lookup method (if it exists)
#
_lookup_result = self._handle_lookup(args, request)
if _lookup_result:
return _lookup_result
# Build a correct Allow: header
if isinstance(e, exc.HTTPMethodNotAllowed):
def method_iter():
for func in ('get', 'get_one', 'get_all', 'new', 'edit',
'get_delete'):
if self._find_controller(func):
yield 'GET'
break
for method in ('HEAD', 'POST', 'PUT', 'DELETE', 'TRACE',
'PATCH'):
func = method.lower()
if self._find_controller(func):
yield method
e.allow = sorted(method_iter())
raise
# return the result
return result
def _handle_lookup(self, args, request=None):
if request is None:
self._raise_method_deprecation_warning(self.handle_lookup)
# filter empty strings from the arg list
args = list(filter(bool, args))
# check for lookup controllers
lookup = getattr(self, '_lookup', None)
if args and iscontroller(lookup):
result = handle_lookup_traversal(lookup, args)
if result:
obj, remainder = result
return lookup_controller(obj, remainder, request)
def _find_controller(self, *args):
'''
Returns the appropriate controller for routing a custom action.
'''
for name in args:
obj = self._lookup_child(name)
if obj and iscontroller(obj):
return obj
return None
def _find_sub_controllers(self, remainder, request):
'''
Identifies the correct controller to route to by analyzing the
request URI.
'''
# need either a get_one or get to parse args
method = None
for name in ('get_one', 'get'):
if hasattr(self, name):
method = name
break
if not method:
return
# get the args to figure out how much to chop off
args = self._get_args_for_controller(getattr(self, method))
fixed_args = len(args) - len(
request.pecan.get('routing_args', [])
)
var_args = getargspec(getattr(self, method)).varargs
# attempt to locate a sub-controller
if var_args:
for i, item in enumerate(remainder):
controller = self._lookup_child(item)
if controller and not ismethod(controller):
self._set_routing_args(request, remainder[:i])
return lookup_controller(controller, remainder[i + 1:],
request)
elif fixed_args < len(remainder) and hasattr(
self, remainder[fixed_args]
):
controller = self._lookup_child(remainder[fixed_args])
if not ismethod(controller):
self._set_routing_args(request, remainder[:fixed_args])
return lookup_controller(
controller,
remainder[fixed_args + 1:],
request
)
def _handle_unknown_method(self, method, remainder, request=None):
'''
Routes undefined actions (like TRACE) to the appropriate controller.
'''
if request is None:
self._raise_method_deprecation_warning(self._handle_unknown_method)
# try finding a post_{custom} or {custom} method first
controller = self._find_controller('post_%s' % method, method)
if controller:
return controller, remainder
# if no controller exists, try routing to a sub-controller; note that
# since this isn't a safe GET verb, any local exposes are 405'd
if remainder:
if self._find_controller(remainder[0]):
abort(405)
sub_controller = self._lookup_child(remainder[0])
if sub_controller:
return lookup_controller(sub_controller, remainder[1:],
request)
abort(405)
def _handle_get(self, method, remainder, request=None):
'''
Routes ``GET`` actions to the appropriate controller.
'''
if request is None:
self._raise_method_deprecation_warning(self._handle_get)
# route to a get_all or get if no additional parts are available
if not remainder or remainder == ['']:
remainder = list(filter(bool, remainder))
controller = self._find_controller('get_all', 'get')
if controller:
self._handle_bad_rest_arguments(controller, remainder, request)
return controller, []
abort(405)
method_name = remainder[-1]
# check for new/edit/delete GET requests
if method_name in ('new', 'edit', 'delete'):
if method_name == 'delete':
method_name = 'get_delete'
controller = self._find_controller(method_name)
if controller:
return controller, remainder[:-1]
match = self._handle_custom_action(method, remainder, request)
if match:
return match
controller = self._lookup_child(remainder[0])
if controller and not ismethod(controller):
return lookup_controller(controller, remainder[1:], request)
# finally, check for the regular get_one/get requests
controller = self._find_controller('get_one', 'get')
if controller:
self._handle_bad_rest_arguments(controller, remainder, request)
return controller, remainder
abort(405)
def _handle_delete(self, method, remainder, request=None):
'''
Routes ``DELETE`` actions to the appropriate controller.
'''
if request is None:
self._raise_method_deprecation_warning(self._handle_delete)
if remainder:
match = self._handle_custom_action(method, remainder, request)
if match:
return match
controller = self._lookup_child(remainder[0])
if controller and not ismethod(controller):
return lookup_controller(controller, remainder[1:], request)
# check for post_delete/delete requests first
controller = self._find_controller('post_delete', 'delete')
if controller:
return controller, remainder
# if no controller exists, try routing to a sub-controller; note that
# since this is a DELETE verb, any local exposes are 405'd
if remainder:
if self._find_controller(remainder[0]):
abort(405)
sub_controller = self._lookup_child(remainder[0])
if sub_controller:
return lookup_controller(sub_controller, remainder[1:],
request)
abort(405)
def _handle_post(self, method, remainder, request=None):
'''
Routes ``POST`` requests.
'''
if request is None:
self._raise_method_deprecation_warning(self._handle_post)
# check for custom POST/PUT requests
if remainder:
match = self._handle_custom_action(method, remainder, request)
if match:
return match
controller = self._lookup_child(remainder[0])
if controller and not ismethod(controller):
return lookup_controller(controller, remainder[1:], request)
# check for regular POST/PUT requests
controller = self._find_controller(method)
if controller:
return controller, remainder
abort(405)
def _handle_put(self, method, remainder, request=None):
return self._handle_post(method, remainder, request)
def _handle_custom_action(self, method, remainder, request=None):
if request is None:
self._raise_method_deprecation_warning(self._handle_custom_action)
remainder = [r for r in remainder if r]
if remainder:
if method in ('put', 'delete'):
# For PUT and DELETE, additional arguments are supplied, e.g.,
# DELETE /foo/XYZ
method_name = remainder[0]
remainder = remainder[1:]
else:
method_name = remainder[-1]
remainder = remainder[:-1]
if method.upper() in self._custom_actions.get(method_name, []):
controller = self._find_controller(
'%s_%s' % (method, method_name),
method_name
)
if controller:
return controller, remainder
def _set_routing_args(self, request, args):
'''
Sets default routing arguments.
'''
request.pecan.setdefault('routing_args', []).extend(args)
def _raise_method_deprecation_warning(self, handler):
warnings.warn(
(
"The function signature for %s.%s.%s is changing "
"in the next version of pecan.\nPlease update to: "
"`%s(self, method, remainder, request)`." % (
self.__class__.__module__,
self.__class__.__name__,
handler.__name__,
handler.__name__
)
),
DeprecationWarning
)
pecan-1.5.1/pecan/routing.py 0000664 0000000 0000000 00000026515 14454530445 0015754 0 ustar 00root root 0000000 0000000 import logging
import re
import warnings
from inspect import getmembers, ismethod
from webob import exc
from .secure import handle_security, cross_boundary
from .util import iscontroller, getargspec, _cfg
__all__ = ['lookup_controller', 'find_object', 'route']
__observed_controllers__ = set()
__custom_routes__ = {}
logger = logging.getLogger(__name__)
def 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.
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())
"""
def _validate_route(route):
if not isinstance(route, str):
raise TypeError('%s must be a string' % route)
if route in ('.', '..') or not re.match(
'^[0-9a-zA-Z-_$\(\)\.~!,;:*+@=]+$', route
):
raise ValueError(
'%s must be a valid path segment. Keep in mind '
'that path segments should not contain path separators '
'(e.g., /) ' % route
)
if len(args) == 2:
# The handler in this situation is a @pecan.expose'd callable,
# and is generally only used by the @expose() decorator itself.
#
# This sets a special attribute, `custom_route` on the callable, which
# pecan's routing logic knows how to make use of (as a special case)
route, handler = args
if ismethod(handler):
handler = handler.__func__
if not iscontroller(handler):
raise TypeError(
'%s must be a callable decorated with @pecan.expose' % handler
)
obj, attr, value = handler, 'custom_route', route
if handler.__name__ in ('_lookup', '_default', '_route'):
raise ValueError(
'%s is a special method in pecan and cannot be used in '
'combination with custom path segments.' % handler.__name__
)
elif len(args) == 3:
# This is really just a setattr on the parent controller (with some
# additional validation for the path segment itself)
_, route, handler = args
obj, attr, value = args
if hasattr(obj, attr):
raise RuntimeError(
(
"%(module)s.%(class)s already has an "
"existing attribute named \"%(route)s\"." % {
'module': obj.__module__,
'class': obj.__name__,
'route': attr
}
),
)
else:
raise TypeError(
'pecan.route should be called in the format '
'route(ParentController, "path-segment", SubController())'
)
_validate_route(route)
setattr(obj, attr, value)
class PecanNotFound(Exception):
pass
class NonCanonicalPath(Exception):
'''
Exception Raised when a non-canonical path is encountered when 'walking'
the URI. This is typically a ``POST`` request which requires a trailing
slash.
'''
def __init__(self, controller, remainder):
self.controller = controller
self.remainder = remainder
def lookup_controller(obj, remainder, request=None):
'''
Traverses the requested url path and returns the appropriate controller
object, including default routes.
Handles common errors gracefully.
'''
if request is None:
warnings.warn(
(
"The function signature for %s.lookup_controller is changing "
"in the next version of pecan.\nPlease update to: "
"`lookup_controller(self, obj, remainder, request)`." % (
__name__,
)
),
DeprecationWarning
)
notfound_handlers = []
while True:
try:
obj, remainder = find_object(obj, remainder, notfound_handlers,
request)
handle_security(obj)
return obj, remainder
except (exc.HTTPNotFound, exc.HTTPMethodNotAllowed,
PecanNotFound) as e:
if isinstance(e, PecanNotFound):
e = exc.HTTPNotFound()
while notfound_handlers:
name, obj, remainder = notfound_handlers.pop()
if name == '_default':
# Notfound handler is, in fact, a controller, so stop
# traversal
return obj, remainder
else:
# Notfound handler is an internal redirect, so continue
# traversal
result = handle_lookup_traversal(obj, remainder)
if result:
# If no arguments are passed to the _lookup, yet the
# argspec requires at least one, raise a 404
if (
remainder == [''] and
len(obj._pecan['argspec'].args) > 1
):
raise e
obj_, remainder_ = result
return lookup_controller(obj_, remainder_, request)
else:
raise e
def handle_lookup_traversal(obj, args):
try:
result = obj(*args)
except TypeError as te:
logger.debug('Got exception calling lookup(): %s (%s)',
te, te.args)
else:
if result:
prev_obj = obj
obj, remainder = result
# crossing controller boundary
cross_boundary(prev_obj, obj)
return result
def 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.
'''
prev_obj = None
while True:
if obj is None:
raise PecanNotFound
if iscontroller(obj):
if getattr(obj, 'custom_route', None) is None:
return obj, remainder
_detect_custom_path_segments(obj)
if remainder:
custom_route = __custom_routes__.get((obj.__class__, remainder[0]))
if custom_route:
return getattr(obj, custom_route), remainder[1:]
# are we traversing to another controller
cross_boundary(prev_obj, obj)
try:
next_obj, rest = remainder[0], remainder[1:]
if next_obj == '':
index = getattr(obj, 'index', None)
if iscontroller(index):
return index, rest
except IndexError:
# the URL has hit an index method without a trailing slash
index = getattr(obj, 'index', None)
if iscontroller(index):
raise NonCanonicalPath(index, [])
default = getattr(obj, '_default', None)
if iscontroller(default):
notfound_handlers.append(('_default', default, remainder))
lookup = getattr(obj, '_lookup', None)
if iscontroller(lookup):
notfound_handlers.append(('_lookup', lookup, remainder))
route = getattr(obj, '_route', None)
if iscontroller(route):
if len(getargspec(route).args) == 2:
warnings.warn(
(
"The function signature for %s.%s._route is changing "
"in the next version of pecan.\nPlease update to: "
"`def _route(self, args, request)`." % (
obj.__class__.__module__,
obj.__class__.__name__
)
),
DeprecationWarning
)
next_obj, next_remainder = route(remainder)
else:
next_obj, next_remainder = route(remainder, request)
cross_boundary(route, next_obj)
return next_obj, next_remainder
if not remainder:
raise PecanNotFound
prev_remainder = remainder
prev_obj = obj
remainder = rest
try:
obj = getattr(obj, next_obj, None)
except UnicodeEncodeError:
obj = None
# Last-ditch effort: if there's not a matching subcontroller, no
# `_default`, no `_lookup`, and no `_route`, look to see if there's
# an `index` that has a generic method defined for the current request
# method.
if not obj and not notfound_handlers and hasattr(prev_obj, 'index'):
if request.method in _cfg(prev_obj.index).get('generic_handlers',
{}):
return prev_obj.index, prev_remainder
def _detect_custom_path_segments(obj):
# Detect custom controller routes (on the initial traversal)
if obj.__class__.__module__ == '__builtin__':
return
attrs = set(dir(obj))
if obj.__class__ not in __observed_controllers__:
for key, val in getmembers(obj):
if iscontroller(val) and isinstance(
getattr(val, 'custom_route', None),
str,
):
route = val.custom_route
# Detect class attribute name conflicts
for conflict in attrs.intersection(set((route,))):
raise RuntimeError(
(
"%(module)s.%(class)s.%(function)s has "
"a custom path segment, \"%(route)s\", "
"but %(module)s.%(class)s already has an "
"existing attribute named \"%(route)s\"." % {
'module': obj.__class__.__module__,
'class': obj.__class__.__name__,
'function': val.__name__,
'route': conflict
}
),
)
existing = __custom_routes__.get(
(obj.__class__, route)
)
if existing:
# Detect custom path conflicts between functions
raise RuntimeError(
(
"%(module)s.%(class)s.%(function)s and "
"%(module)s.%(class)s.%(other)s have a "
"conflicting custom path segment, "
"\"%(route)s\"." % {
'module': obj.__class__.__module__,
'class': obj.__class__.__name__,
'function': val.__name__,
'other': existing,
'route': route
}
),
)
__custom_routes__[
(obj.__class__, route)
] = key
__observed_controllers__.add(obj.__class__)
pecan-1.5.1/pecan/scaffolds/ 0000775 0000000 0000000 00000000000 14454530445 0015646 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/scaffolds/__init__.py 0000664 0000000 0000000 00000007616 14454530445 0017771 0 ustar 00root root 0000000 0000000 import sys
import os
import re
import pkg_resources
from string import Template
DEFAULT_SCAFFOLD = 'base'
_bad_chars_re = re.compile('[^a-zA-Z0-9_]')
class PecanScaffold(object):
"""
A base Pecan scaffold. New scaffolded implementations should extend this
class and define a ``_scaffold_dir`` attribute, e.g.,
class CoolAddOnScaffold(PecanScaffold):
_scaffold_dir = ('package', os.path.join('scaffolds', 'scaffold_name'))
...where...
pkg_resources.resource_listdir(_scaffold_dir[0], _scaffold_dir[1]))
...points to some scaffold directory root.
"""
def normalize_output_dir(self, dest):
return os.path.abspath(os.path.normpath(dest))
def normalize_pkg_name(self, dest):
return _bad_chars_re.sub('', dest.lower())
def copy_to(self, dest, **kw):
output_dir = self.normalize_output_dir(dest)
pkg_name = self.normalize_pkg_name(dest)
copy_dir(self._scaffold_dir, output_dir, {'package': pkg_name}, **kw)
class BaseScaffold(PecanScaffold):
_scaffold_dir = ('pecan', os.path.join('scaffolds', 'base'))
class RestAPIScaffold(PecanScaffold):
_scaffold_dir = ('pecan', os.path.join('scaffolds', 'rest-api'))
def copy_dir(source, dest, variables, out_=sys.stdout, i=0):
"""
Copies the ``source`` directory to the ``dest`` directory, where
``source`` is some tuple representing an installed package and a
subdirectory in the package, e.g.,
('pecan', os.path.join('scaffolds', 'base'))
('pecan_extension', os.path.join('scaffolds', 'scaffold_name'))
``variables``: A dictionary of variables to use in any substitutions.
Substitution is performed via ``string.Template``.
``out_``: File object to write to (default is sys.stdout).
"""
def out(msg):
out_.write('%s%s' % (' ' * (i * 2), msg))
out_.write('\n')
out_.flush()
names = sorted(pkg_resources.resource_listdir(source[0], source[1]))
if not os.path.exists(dest):
out('Creating %s' % dest)
makedirs(dest)
else:
out('%s already exists' % dest)
return
for name in names:
full = '/'.join([source[1], name])
dest_full = os.path.join(dest, substitute_filename(name, variables))
sub_file = False
if dest_full.endswith('_tmpl'):
dest_full = dest_full[:-5]
sub_file = True
if pkg_resources.resource_isdir(source[0], full):
out('Recursing into %s' % os.path.basename(full))
copy_dir((source[0], full), dest_full, variables, out_, i + 1)
continue
else:
content = pkg_resources.resource_string(source[0], full)
if sub_file:
content = render_template(content, variables)
if content is None:
continue # pragma: no cover
out('Copying %s to %s' % (full, dest_full))
f = open(dest_full, 'wb')
f.write(content)
f.close()
def makedirs(directory):
""" Resursively create a named directory. """
parent = os.path.dirname(os.path.abspath(directory))
if not os.path.exists(parent):
makedirs(parent)
os.mkdir(directory)
def substitute_filename(fn, variables):
""" Substitute +variables+ in file directory names. """
for var, value in variables.items():
fn = fn.replace('+%s+' % var, str(value))
return fn
def render_template(content, variables):
"""
Return a bytestring representing a templated file based on the
input (content) and the variable names defined (vars).
"""
fsenc = sys.getfilesystemencoding()
def to_native(s, encoding='latin-1', errors='strict'):
if isinstance(s, str):
return s
return str(s, encoding, errors)
output = Template(
to_native(content, fsenc)
).substitute(variables)
if isinstance(output, str):
output = output.encode(fsenc, 'strict')
return output
pecan-1.5.1/pecan/scaffolds/base/ 0000775 0000000 0000000 00000000000 14454530445 0016560 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/scaffolds/base/+package+/ 0000775 0000000 0000000 00000000000 14454530445 0020301 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/scaffolds/base/+package+/__init__.py 0000664 0000000 0000000 00000000000 14454530445 0022400 0 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/scaffolds/base/+package+/app.py_tmpl 0000664 0000000 0000000 00000000406 14454530445 0022467 0 ustar 00root root 0000000 0000000 from pecan import make_app
from ${package} import model
def setup_app(config):
model.init_model()
app_conf = dict(config.app)
return make_app(
app_conf.pop('root'),
logging=getattr(config, 'logging', {}),
**app_conf
)
pecan-1.5.1/pecan/scaffolds/base/+package+/controllers/ 0000775 0000000 0000000 00000000000 14454530445 0022647 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/scaffolds/base/+package+/controllers/__init__.py 0000664 0000000 0000000 00000000000 14454530445 0024746 0 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/scaffolds/base/+package+/controllers/root.py 0000664 0000000 0000000 00000001165 14454530445 0024207 0 ustar 00root root 0000000 0000000 from pecan import expose, redirect
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: # pragma: no cover
status = 500
message = getattr(status_map.get(status), 'explanation', '')
return dict(status=status, message=message)
pecan-1.5.1/pecan/scaffolds/base/+package+/model/ 0000775 0000000 0000000 00000000000 14454530445 0021401 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/scaffolds/base/+package+/model/__init__.py 0000664 0000000 0000000 00000000717 14454530445 0023517 0 ustar 00root root 0000000 0000000 from pecan import conf # noqa
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
pecan-1.5.1/pecan/scaffolds/base/+package+/templates/ 0000775 0000000 0000000 00000000000 14454530445 0022277 5 ustar 00root root 0000000 0000000 pecan-1.5.1/pecan/scaffolds/base/+package+/templates/error.html 0000664 0000000 0000000 00000000421 14454530445 0024313 0 ustar 00root root 0000000 0000000 <%inherit file="layout.html" />
## provide definitions for blocks we want to redefine
<%def name="title()">
Server Error ${status}
%def>
## now define the body of the template
Server Error ${status}
${message}
pecan-1.5.1/pecan/scaffolds/base/+package+/templates/index.html 0000664 0000000 0000000 00000001541 14454530445 0024275 0 ustar 00root root 0000000 0000000 <%inherit file="layout.html" />
## provide definitions for blocks we want to redefine
<%def name="title()">
Welcome to Pecan!
%def>
## now define the body of the template
This is a sample Pecan project.
Instructions for getting started can be found online at pecanpy.org