Repository: Pylons/pylons Branch: master Commit: 8625d5af7905 Files: 231 Total size: 1.1 MB Directory structure: gitextract_jwapr307/ ├── .gitignore ├── .hgignore ├── .hgtags ├── .travis.yml ├── CHANGELOG ├── LICENSE ├── MANIFEST.in ├── README.rst ├── UPGRADING ├── ez_setup.py ├── pylons/ │ ├── __init__.py │ ├── commands.py │ ├── configuration.py │ ├── controllers/ │ │ ├── __init__.py │ │ ├── core.py │ │ ├── jsonrpc.py │ │ ├── util.py │ │ └── xmlrpc.py │ ├── decorators/ │ │ ├── __init__.py │ │ ├── cache.py │ │ ├── rest.py │ │ ├── secure.py │ │ └── util.py │ ├── docs/ │ │ ├── en/ │ │ │ ├── .gitignore │ │ │ ├── Makefile │ │ │ ├── _oldstatic/ │ │ │ │ ├── akhet.css │ │ │ │ ├── default.css │ │ │ │ ├── pygments.css │ │ │ │ └── pylons_as_onion.ai │ │ │ ├── _oldtemplates/ │ │ │ │ ├── genindex.html │ │ │ │ ├── layout.html │ │ │ │ ├── modindex.html │ │ │ │ └── page.html │ │ │ ├── advanced_models.rst │ │ │ ├── advanced_pylons/ │ │ │ │ ├── creating_paste_templates.rst │ │ │ │ ├── entry_points_and_plugins.rst │ │ │ │ ├── index.rst │ │ │ │ ├── paster.rst │ │ │ │ └── paster_commands.rst │ │ │ ├── caching.rst │ │ │ ├── concepts.rst │ │ │ ├── conf.py │ │ │ ├── configuration.rst │ │ │ ├── controllers.rst │ │ │ ├── debugging.rst │ │ │ ├── deployment.rst │ │ │ ├── events.rst │ │ │ ├── execution.rst │ │ │ ├── forms.rst │ │ │ ├── gettingstarted.rst │ │ │ ├── glossary.rst │ │ │ ├── helpers.rst │ │ │ ├── i18n.rst │ │ │ ├── index.rst │ │ │ ├── jython.rst │ │ │ ├── logging.rst │ │ │ ├── models.rst │ │ │ ├── modules/ │ │ │ │ ├── commands.rst │ │ │ │ ├── configuration.rst │ │ │ │ ├── controllers.rst │ │ │ │ ├── controllers_core.rst │ │ │ │ ├── controllers_util.rst │ │ │ │ ├── controllers_xmlrpc.rst │ │ │ │ ├── decorators.rst │ │ │ │ ├── decorators_cache.rst │ │ │ │ ├── decorators_rest.rst │ │ │ │ ├── decorators_secure.rst │ │ │ │ ├── error.rst │ │ │ │ ├── i18n_translation.rst │ │ │ │ ├── index.rst │ │ │ │ ├── log.rst │ │ │ │ ├── middleware.rst │ │ │ │ ├── templating.rst │ │ │ │ ├── test.rst │ │ │ │ ├── util.rst │ │ │ │ └── wsgiapp.rst │ │ │ ├── objects.inv │ │ │ ├── python23_install.rst │ │ │ ├── security_policy_for_bugs.rst │ │ │ ├── sessions.rst │ │ │ ├── testing.rst │ │ │ ├── thirdparty/ │ │ │ │ ├── formencode_api.rst │ │ │ │ ├── index.rst │ │ │ │ ├── weberror.rst │ │ │ │ ├── webob.rst │ │ │ │ └── webtest.rst │ │ │ ├── tutorials/ │ │ │ │ ├── index.rst │ │ │ │ ├── quickwiki_tutorial.rst │ │ │ │ └── understanding_unicode.rst │ │ │ ├── upgrading.rst │ │ │ ├── views.rst │ │ │ ├── windowsnotes.rst │ │ │ └── wsgi_support.rst │ │ └── uploader.py │ ├── error.py │ ├── i18n/ │ │ ├── __init__.py │ │ └── translation.py │ ├── log.py │ ├── media/ │ │ ├── javascripts/ │ │ │ └── traceback.js │ │ └── style/ │ │ ├── black.css │ │ ├── itraceback.css │ │ └── orange.css │ ├── middleware.py │ ├── templates/ │ │ ├── __init__.py │ │ ├── controller.py_tmpl │ │ ├── default_project/ │ │ │ ├── +package+/ │ │ │ │ ├── __init__.py_tmpl │ │ │ │ ├── config/ │ │ │ │ │ ├── __init__.py_tmpl │ │ │ │ │ ├── deployment.ini_tmpl_tmpl │ │ │ │ │ ├── environment.py_tmpl │ │ │ │ │ ├── middleware.py_tmpl │ │ │ │ │ └── routing.py_tmpl │ │ │ │ ├── controllers/ │ │ │ │ │ ├── __init__.py_tmpl │ │ │ │ │ └── error.py_tmpl │ │ │ │ ├── lib/ │ │ │ │ │ ├── __init__.py_tmpl │ │ │ │ │ ├── app_globals.py_tmpl │ │ │ │ │ ├── base.py_tmpl │ │ │ │ │ └── helpers.py_tmpl │ │ │ │ ├── model/ │ │ │ │ │ ├── __init__.py_tmpl │ │ │ │ │ └── meta.py_tmpl │ │ │ │ ├── public/ │ │ │ │ │ └── index.html_tmpl │ │ │ │ ├── templates/ │ │ │ │ │ ├── .distutils_placeholder │ │ │ │ │ └── __init__.py_tmpl │ │ │ │ ├── tests/ │ │ │ │ │ ├── __init__.py_tmpl │ │ │ │ │ ├── functional/ │ │ │ │ │ │ └── __init__.py_tmpl │ │ │ │ │ └── test_models.py_tmpl │ │ │ │ └── websetup.py_tmpl │ │ │ ├── MANIFEST.in_tmpl │ │ │ ├── README.txt_tmpl │ │ │ ├── development.ini_tmpl │ │ │ ├── ez_setup.py │ │ │ ├── setup.cfg_tmpl │ │ │ ├── setup.py_tmpl │ │ │ └── test.ini_tmpl │ │ ├── minimal_project/ │ │ │ ├── +package+/ │ │ │ │ ├── __init__.py_tmpl │ │ │ │ ├── config/ │ │ │ │ │ └── deployment.ini_tmpl_tmpl │ │ │ │ ├── controllers/ │ │ │ │ │ └── __init__.py_tmpl │ │ │ │ ├── helpers.py_tmpl │ │ │ │ ├── public/ │ │ │ │ │ └── index.html_tmpl │ │ │ │ ├── routing.py_tmpl │ │ │ │ ├── templates/ │ │ │ │ │ ├── .distutils_placeholder │ │ │ │ │ └── __init__.py_tmpl │ │ │ │ ├── tests/ │ │ │ │ │ └── __init__.py_tmpl │ │ │ │ └── wsgiapp.py_tmpl │ │ │ ├── MANIFEST.in_tmpl │ │ │ ├── README.txt_tmpl │ │ │ ├── development.ini_tmpl │ │ │ ├── ez_setup.py │ │ │ ├── setup.cfg_tmpl │ │ │ ├── setup.py_tmpl │ │ │ └── test.ini_tmpl │ │ ├── restcontroller.py_tmpl │ │ ├── test_controller.py_tmpl │ │ └── test_restcontroller.py_tmpl │ ├── templating.py │ ├── test.py │ ├── testutil.py │ ├── url.py │ ├── util.py │ └── wsgiapp.py ├── rtd.txt ├── scripts/ │ ├── gen-go-pylons.py │ ├── go-pylons.py │ └── pylintrc ├── setup.cfg ├── setup.py ├── test_files/ │ ├── __init__.py │ ├── event_file.py │ └── sample_controllers/ │ ├── __init__.py │ ├── controllers/ │ │ ├── __init__.py │ │ ├── goodbye.py │ │ ├── hello.py │ │ └── i18nc.py │ ├── i18n/ │ │ ├── es/ │ │ │ └── LC_MESSAGES/ │ │ │ ├── sample_controllers.mo │ │ │ └── sample_controllers.po │ │ ├── fr/ │ │ │ └── LC_MESSAGES/ │ │ │ ├── sample_controllers.mo │ │ │ └── sample_controllers.po │ │ └── ja/ │ │ └── LC_MESSAGES/ │ │ ├── sample_controllers.mo │ │ └── sample_controllers.po │ └── templates/ │ ├── hello.html │ └── time.html └── tests/ ├── __init__.py ├── conftest.py ├── test_units/ │ ├── __init__.py │ ├── test_basic_app.py │ ├── test_controller.py │ ├── test_decorator_authenticate_form.py │ ├── test_decorator_cache.py │ ├── test_decorator_https.py │ ├── test_decorator_jsonify.py │ ├── test_decorator_validate.py │ ├── test_helpers.py │ ├── test_i18n.py │ ├── test_jsonrpc.py │ ├── test_middleware.py │ ├── test_templating.py │ └── test_xmlrpc.py └── test_webapps/ ├── __init__.py ├── filestotest/ │ ├── app_globals.py │ ├── base_with_xmlrpc.py │ ├── cache_controller.py │ ├── controller_sample.py │ ├── controller_sqlatest.py │ ├── controller_xmlrpc.py │ ├── development.ini │ ├── development_sqlatesting.ini │ ├── environment_def_engine.py │ ├── environment_def_sqlamodel.py │ ├── functional_controller_cache_decorator.py │ ├── functional_controller_xmlrpc.py │ ├── functional_sample_controller_i18n.py │ ├── functional_sample_controller_jinja2.py │ ├── functional_sample_controller_mako.py │ ├── functional_sample_controller_sample1.py │ ├── functional_sample_controller_sample2.py │ ├── functional_sample_controller_sample3.py │ ├── functional_sample_controller_sample4.py │ ├── functional_sample_controller_sqlatesting.py │ ├── helpers_sample.py │ ├── messages.ja.mo │ ├── messages.ja.po │ ├── messages.pot │ ├── middleware_mako.py │ ├── model__init__.py │ ├── rest_routing.py │ ├── test_mako.html │ ├── test_sqlalchemy.html │ ├── testgenshi.html │ ├── testjinja2.html │ ├── tests__init__.py │ └── websetup.py └── test_make_project.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.pyc *.egg-info *.egg tests/test_units/cache/ tests/test_units/session/ ================================================ FILE: .hgignore ================================================ # Automatically generated by `hgimportsvn` syntax:glob *.DS_Store .svn .coverage *.pyc *.egg-info *.egg ez_setup pylons/docs/*/_build container_file container_dbm* *~ build/ dist/ docs/html/docs output/ProjectName tests/html_coverage syntax: glob ================================================ FILE: .hgtags ================================================ b7002f68d541940f32fa5d5fecb83483008fd489 v0.9.6 88112f696d4aba81405753713ca06070a92ebcf9 v0.9.5 1bc21f0b2820a39801278b322aab939da893ed4d v0.9.4.1 a8e89fafa6d836c5ec9de712db4afd68acd02b30 v0.9.4 896da05e4c2679f39756194d65c3e87eb92bc2f1 v0.9.3 a71e37726e2909c358a6fb699f26a314643d1c89 v0.9.2 37614170faa944723382bf885023085bc3ece616 v0.9.1 f650ccf099785af6be11d3e91f66e4dc5178dc1b v0.9 0da35cd7624a67c43948c00f60b35eb9202e2b8d v0.8.2 0fa5a7b12de0795c95a425e89d802f09e8e709ab v0.8.1 1880858f1fb30fbf4b1717ac49d172712dfc5b9d v0.8 9e6916dca073e83deb25ed401a928e834bc0d262 v0.9.6rc2 f2b68bd9f177d08f8caf7e5d33629190d097fbc0 v0.9.6rc1 f04d465aa10861b96c1050a4c6347c4aa12247c1 v0.9.6rc3 a7749485525a8aade8dbe089abae9a539af24682 v0.9.7beta1 46c5edc17b643e07158dc4c6b5e14a7d839fe066 v0.9.7beta1 9cb761f0f0476dad15b370765919ae96a968fea4 v0.9.7beta2 3819db8d2307f8f5725b2a27c6a34e8f5738d687 v0.9.7beta3 e546c53613c2c0647ab94ee0d376a5bc8e28dcb4 v0.9.7beta5 ca3cbefec25e0e9ec7b579123cf97ed98934ccd8 v0.9.7rc1 a6f56791a83a53db7a7484fb8bd8821ffba740c4 v0.9.7rc2 7ceef102aff2cce44634132d21638358b4129197 v0.9.7rc3 4a94734906711e56f9d34a34527f4b8a6e9811a8 v0.9.7rc4 fa5aad0d57deb99f8a25596e9c7d601b1ced1d5d v0.9.7rc5 e7a19deba3f66b4e6dfc050f070295a0a622308a v0.9.7rc6 2154554f4ffb458555023afd6e810ce88e58e64b v0.9.7 165565778a3c518d6b5c383095fffdf6e154fa78 v0.10b1 25eb46de0380e20799c419557a86c61f144101a2 v1.0b1 cd08f0df626b89f3e6ee9df71539e009d9be1ba9 v0.10rc1 ================================================ FILE: .travis.yml ================================================ language: python python: - "2.6" - "2.7" install: - python setup.py develop script: - python setup.py test ================================================ FILE: CHANGELOG ================================================ Pylons Changelog ================ 1.0.2 (July 21, 2015) * In the event of a NilAccept for the language, request.languages() would throw an AttributeError exception. Fixes #24. * Encode Location HTTP header in redirect responses in UTF-8. Per RFC 3987. Refers to #15. * Remove "Post Traceback" as it was a possible XSS vector with prior versions of WebError, and the PylonsHQ site is no longer in existence to support them. 1.0.1 (August 13th, 2012) * No changes since RC1. 1.0.1RC1 (December 12, 2011) * WARNING: pylons.lib.decorators has had two functions removed: ``determine_response_charset`` and ``encode_formencode_errors``. * Updated dependencies to latest versions of Paste, PasteDeploy, and compatibility for the latest WebOb 1.2 betas. * authenticate_form allows for GET. Patch by Domen Kožar. * jsonify now properly sets charset to utf-8. * Add ability for jsonify to handle objects with a __json__ attribute using custom JSONEncoder class similar to TG2. Patch by Bob Farrell. * Added ability for __before__ to reference a callable function. Patch contributed by mverdone. * Pulled in JSON-RPC support from agentultra's pylons fork. * Apply patch for proper pylons.__version__ under Windows. Contributed by Christoph Zwerschke. * Utilize MarkupSafe for faster HTML escaping. * Fix signed cookies by using standard base64 alphabet, and prevent timing attacks on signature comparison. * Added setup of app_globals and config to Pylons config.init_app to ensure as long as the Pylons application is loaded, the app_globals and config will be appropriately initialized. * Documentation updates. 1.0 (May 27, 2010) * Minor tweak to allow proper importing of pylons. 1.0RC1 (March 1, 2010) * Switched to using Routes 1.12 with support for no longer using the odd routes singleton. * Removed pylons.middleware.StaticJavascripts, this is not used anymore. * Added more unit tests. 1.0b1 (February 5, 2010) * Removed CacheMiddleware. cache object is now setup as an attribute on the app_globals object for use where needed. * WARNING: config only supports dict access * WARNING: Method arguments no longer assigned to 'tmpl_context' by default. * WARNING: Changed default to strict_tmpl_context. * WARNING: Removed legacy pylons.c and pylons.g globals. * WARNING: Removed legacy pylons.database module. * WARNING: Removed legacy pylons.config module. * WARNING: Removed Buffet options, setup, and legacy render/render_response function from pylons.templating. This also means config no longer accepts the add_template_engine option. * WARNING: Removed legacy redirect_to function. * WARNING: @https decorator no longer accepts url_for-like arguments. * Add a "paster routes" command. This prints the mapper, which from Routes 1.12 onwards gives sensibly formatted output. * Fix unit tests on Windows * Prepare for Routes 1.12, ensure tests don't assume implicit routing 0.10 (May 27, 2010) * Fix legacy warning replacement. 0.10RC1 (March 1, 2010) * No changes to Pylons core since b1. 0.10b1 (February 5, 2010) * redirect_to is now deprecated, use redirect(url(*args, **kwargs)) instead. * url_for like args to the https decorator are now deprecated, pass it a url or a callable returning a url instead. * Changed 1.0 deprecated pylons.c, pylons.g, pylons.buffet instances to throw deprecation warnings. * Fixed etag_cache matching when the If-None-Match header contains a comma separated list of etags. Fixes #557. Thanks magicbronson. * Added tests for restcontroller with sub-directory, and fixed generated unit tests. Patches supplied by Michael van Tellingen, fixes #571. * Retain the original controller exception when thrown under environ['pylons.controller.exception'] for use in the error controller. * Fixed bug with unit tests running the app load twice during testing. Fixes #620. * Updated project templates to use actual config instance, rather than the StackedObjectProxy classes. * Changed PylonsConfig to be dict subclass, rather than DispatchingConfig subclass. 0.9.7 (February 23, 2009) * WARNING: A new option is available to determine whether or not an actions arguments should be automatically attached to 'c'. To turn off this implicit behavior in environment.py: config['pylons.c_attach_args'] = False This is set to True by default. * WARNING: Fixed a minor security hole in the default Pylons error page that could result in an XSS security hole. * WARNING: Fixed a security hole in the default project template to use the StaticURLParser to ensure arbitrary files can't be sent. * WARNING: Refactored PylonsApp to remove legacy PylonsApp, moved session/cache and routes middleware into the project template. This will require projects to be updated to include those 3 middleware in the projects middleware.py. * Added redirect, preferred over redirect_to. Takes an explicit url instead of url_for like arguments * Changed to using WebTest instead of paste.fixture for app testing. * Added render_mako_def to render def blocks within a mako template. * Changes to cache_decorator and cached_template to support Beaker API changes in version 1.1. 1.0.3 is still supported. * Fix HEAD requests causing an Exception as if no content was returned by the controller. Fixes #507. Thanks mvtellingen, Petr Kobalicek. * Fix a crash when returning the result of ``etag_cache`` in a controller. Fixes #508. * "response" flag has been removed from pylons.decorators.cache.beaker_cache, as it sends all headers along unconditionally including cookies; additionally, the flag was taking effect in all cases previously so prior versions of beaker_cache are not secure. In its place, a new option "cache_headers" is provided, which is a tuple of specific header names to be cached. It defaults to ('content-type','content-length'). * "invalidate_on_startup" flag added to beaker_cache, which provides a "starttime" to the cache such that when the application is started or restarted, the cache entry is invalidated. * Updating host to use 127.0.0.1 for development binding. * Added option to specify the controller name with a __controller__ variable in the controller's module. This name will be used for the controller class rather than the default naming scheme. * setup.py egg_info now restores projects' paster_plugins.txt, allowing paster shell to work again after the egg-info directory was lost. fixes #282. Thanks sevkin. * The paste_deploy_config.ini_tmpl template is now located at package/config/deployment.ini_tmpl for new projects. * Project's default test fixtures no longer hardcode test.ini; the ini file used can now be specified via the nosetests --with-pylons argument (defaults to test.ini in setup.cfg). fixes #400. * @validate now defaults to translating FormEncode error messages via Pylons' gettext catalog, then falls back to FormEncode's. fixes #296. Thanks Max Ischenko. * Fixed SQLAlchemy logging not working in paster shell. Fixes #363. Thanks Christoph Haas. * Added optionally engine initialization, to prevent Buffet from loading if there's no 'buffet.template_engines' in the config. * Updated minimal template to work with Tempita and other new templating changes. * Fixed websetup to parse location config file properly when the section isn't 'main'. Fixes #399. * Added default Mako filter of escape for all template rendering. * Fixed template for Session.remove inclusion when using SA. Fixed render_genshi to properly use fragment/format options. Thanks Antonin Enfrun. * Remove template engine from load_environment call. * Removing template controller from projects. Fixes #383. * Added signed_cookie method to WebOb Request/Response sub-classes. * Updated project template to setup appropriate template loader and controller template to doc how to import render. * Added documentation for render functions in pylons.templating. * Adding specific render functions that don't require Buffet. * Added forward controller.util function for forwarding the request to WSGI apps. Fixes #355. * Added default input encoding for Mako to utf-8. Suggested in #348. * Fixed paster controller to raise an error if the controller for it already exists. Fixes #279. * Added __init__.py to template dir in project template if the template engine is genshi or kid. Fixes #353. * Fixed jsonify to use application/json as its the proper mime-type and now used all over the net. * Fixed minimal template not replacing variables properly. Fixes #377. * Fixed @validate decorator to no longer catch exceptions should they be raised in the action that is supposed to display a form. Fixes #374. * Fixed paster shell command to no longer search for egg_info dir. Allows usage of paster shell with installed packages. Suggested by Gavin Carothers. * Added mimetype function and MIMETypes class for registering mimetypes. * WARNING: Usage of pylons.Response is now deprecated. Please use pylons.response instead. * Removed use of WSGIRequest/WSGIResponse and replaced with WebOb subclasses that implement methods to make it backwards compatible with the Paste wsgiwrappers. * Fixed missing import in template controller. * Deprecated function uses string substitution to avoid Nonetype error when Python optimization is on. Fixes #334. * E-tag cache no longer returns Content-Type in the headers. Fixes #323. * XMLRPCController now properly includes the Content-Length of the response. Fixes #310, thanks Nicholas. * Added SQLAlchemy option to template, which adds SQLAlchemy setup to the project template. * Switched project templating to use Tempita. * Updated abort/redirect_to to use appropriate Response object when WebOb is used. * Updated so that 404's properly return as Response objects when WebOb is in use instead of WSGIResponse. * Added beaker_cache option to avoid caching/restoring global Response values that were present during the first cache operation. * Adding StatusCodeRedirect to handle internal redirects based on the status code returned by the app. This replaces the use of ErrorDocuments in projects. * Refactored error exceptions to use WebError. * WSGIController now uses the environ references to response, request, and the c object for higher performance. * Added optional use of WebOb instead of paste.wsgiwrapper objects. * Fixed bug with beaker_cache defaulting to dbm rather than the beaker cache app-wide default. * The --with-pylons nose plugin no longer requires a project to have been registered with setuptools to work. * The config object is now included in the template namespace. * StaticJavascripts now accepts keyword arguments for StaticURLParser. Suggested by Marcin Kasperski. * Fix pylons.database.AutoConnectHub's doInTransaction not automatically connecting when necessary. Fixes #327. 0.9.6.1 (September 27th, 2007) * Fixed validate decorator to resume pre-0.9.6 behavior of only validating POST requests by default. Added option to validate during GET as well and a recursion avoidance check to prevent validate from running more than once. * WARNING: Fixed a security hole allowing private controller methods (those beginning with an underscore) to be accessed from the outside. Found by Tomasz Nazar. * Added nose plugin '--with-pylons=test.ini' option to load the Pylons app before scanning for unit tests. This enables Pylons apps to be unit tested with doc tests. * PylonsBaseWSGIApp now caches controller lookup and the effective logging level for a little better performance. 0.9.6 (September 8th, 2007) * Updated requirements for newer WebHelpers for SQLAlchemy 0.4 compatibility. Fixes #300. * Fixed pylons.templating to not pull session objects if there are none in use for the request. Thanks Bob Ippolito. * Catch UnicodeEncodeErrors when finding the Controller action method and fail gracefully. Thanks max. Fixes #298. * Allow passing of a state keyword to the validate decorator for the to_python methods. Fixes #297. * paster shell now configures logging from the config file, like paster serve and setup-app. This can be disabled via the -q option. Thanks Yannick Gingras. 0.9.6rc3 (August 18, 2007) * Fixed controllers.core to allow responses of None (empty bodies). Logs a message indicating the response was empty. * pylons.helpers has been moved to pylons.controllers.util, to differentiate between controller utility functions and projects' helpers modules. * Fixed non-basestring/generator/WSGIResponse objects returned from Controllers not being set as the response content. Thanks Alex Conrad. * development.ini now configures the app's Logger level to DEBUG by default. Thanks Christoph Haas 0.9.6rc2 (August 2, 2007) * Projects now include a MANIFEST.in file: it directs distutils to recursively include all files in the project's public/ and templates/ dir. This fixes these dirs not being included in dists unless they were checked into an RCS recognized by setuptools. This is at the expense of dists now globbing all files in those dirs (even those not checked into your RCS). Thanks Christoph Haas. * Fixed the validate decorator not setting c.form_errors in certain circumstances. Thanks max. Fixes #286. * email_to lines commented out in development.ini and test.ini files to avoid emails being sent to a non-existent address by mistake. If an error occurs it is logged but no email is sent unless email_to is specified. * [paste.app_factory] entry points changed to point to the actual make_app() function to make it simpler for someone to work out how Pylons works (tests updated accordingly too). * All use of the ez_setup module is now tested by an ImportError to make Pylons compatible with Buildout. Note: Tags and releases should be made using an svn export and an svn add to ensure a real copy of the ez_setup module is included and not just an svn:external so that the module is tied to time of the release. * More full-featured README.txt included. * Updated beaker_cache to cache global response cookies/status/headers. Fixes #280. * Fixed missing abort name import in restrict rest decorator. Fixes #281. * Added cheetah as a supported template language for template_engine option. * Fixed public/ and templates/ directories not being created with paster create. 0.9.6rc1 (July 15, 2007) * Fixed cookie header addition to use add instead of append. Thanks to anonymous patcher. Fixes #268, again. * Added ability to pass _code option to specify the status code type for redirect_to. * Fixed redirect_to to not copy all headers into redirect for old _response usage. Fixes #268. * WARNING: By default, the Pylons request object now returns unicode parameter (pylons.GET/POST/params) values (and assumes those parameters were sent to Pylons as utf-8). Unicode parameters can cause major problems if your application is not setup to handle unicode. To disable unicode parameters (0.9.5 default behavior), add the following to your load_environment function (0.9.6 syntax): config['request_options']['charset'] = None or, if still using the deprecated pre-0.9.6 pylons.config syntax, add: request_settings = pylons.config.request_defaults.copy() request_settings['charset'] = None return pylons.config.Config(tmpl_options, map, paths, request_settings=request_settings) * WARNING: Template names beginning with a / (or the OS's path separator) will now result in the name not having the separator's replaced with '.'s for the template engine. This shouldn't affect most projects as they usually assume a dot notation will be used with dot notation template engines (Kid, Genshi, etc.). This change allows template engines that can take filename paths to function properly. Fixes #233. * WARNING: The pylons.util.get_prefix(environ) function is deprecated. Please use: environ.get('SCRIPT_NAME', '') instead (the get_prefix function is used in the default ErrorController). Fixes #243. * WARNING: The paths dictionary's 'root_path' has moved to the less redundant 'root'. * Fixed the Error Documents/EvalException css referencing non-existent images. Thanks Shannon -jj Behrens. Fixes #238. * Added ability to pass _code option to specify the status code type for redirect_to. * Fixed redirect_to to not copy all headers into redirect for old _response usage. Fixes #268. * Added logging statements throughout Pylons code, added logging setup to default template. Fixes #98. * Refactored global response to be setup in wsgiapp along with the other globals. Updated WSGIController to copy in global response headers and cookies into a WSGI app's output. * Added global pylons.response object. Thanks Shannon -jj Behrens and Damjan Georgievski. Fixes #268 and #201. * Updated default project template files for new configuration layout. Options to handle config now just in environment.py, and middleware.py handling just middleware. Fixes #203. * Removing mako tests, as its now the default. Default test changed from Myghty to Mako. * Changing default templating to mako. * Added the https decorator. It requires an action to be loaded via https. Patch by ido. Fixes #241. * Added upgrade instructions, and posted a copy on the wiki. Fixes #230. * Added deprecation warnings for usage of the Pylons Controller class, all controllers should inherit from WSGIController instead. Fixes #239. * Removed deprecated attach_locals function from Controller class. * Added an authenticate_form decorator for use with WebHelpers' secure_form_tag functions for preventing CSRF attacks. Original patch by David Turner. Fixes #157. * Fix Buffet's include_pylons_variables not being upheld. Thanks Jonathan LaCour. * The validate decorator now accepts extra keyword arguments (**htmlfill_kwargs) to pass along to formencode's htmlfill.render function. * Removed POST-only restriction on validate decorator, now handles GET requests. No form arg required during a GET request, which will run the current action with c.form_errors set to the errors. Fixes #246. * Added PylonsConfig, which gets accessed as pylons.config dict. Contains all the merged ini options, in addition to the Config options such as 'routes.map', 'pylons.paths', 'buffet.template_options', etc. Check the pylons.config docs on PylonsConfig for dict keys populated by it. * Split up resolution stages in wsgiapp, so that controller lookup is a separate function making it easier to subclass. PylonsApp now takes a base_wsgi_app argument which is then used for the BaseWSGIApp instead of the one from wsgiapp.py. * Added mako template render tests. * Added storage of the action func used to handle a call, for later code that might need a reference to the action that originally handled the request. Fixes #253. * Updated config object to optionally take a single merged conf dict, updated project templates to pass around the single merged conf dict. * Changed project template to use new Beaker session keys. * Changed default routing for raw template matching to not unicode decode the route argument. Fixes #242. * Catch any exceptions raised by template engine entry points and emit a warning instead of crashing. Thanks markbradley. Fixes #249 * Fixed the validate decorator not working with formencode's CompoundValidators when variable_decode=False. Fixes #209. * Fixed the validate decorator failing with a KeyError when no value is specified to validate against for separate validators (as opposed to a schema). Reported by Graham Stratton. * Fixed paster shell not merging app_conf and global_conf into the main CONFIG dict namespace. Original patch by David Smith. Fixes #244. * Added logging to decorators. Refs #98. * Fixed paster restcontroller to test for lib.base and only add that import statement when its present. This fixes the restcontroller template when used with minimal Pylons project templates. Fixes #237. * Fixed the EvalException debugger showing broken links and buttons when the app's ErrorController was broken (such as when BaseController's __before__ raises an exception). Suggested by Ian Bicking. Fixes #228. * paster create now accepts a 'template_engine' option to setup the new project's default template engine. E.g. to create a new project that uses Genshi by default, use: paster create --template=pylons mygenshiproj template_engine=genshi Suggested by Ian Bicking. Fixes #141. * Fixed the validate decorator triggering the following error with FormEncode>=0.7 and non-ascii rendered form content: UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 10: ordinal not in range(128) the form was passed in as an encoded string, but some data or error messages were unicode strings; the form should be passed in as a unicode string Reported by Christoph Haas. * HTTPExceptions are now converted to Response objects (for __after__), making the httpexceptions middleware no longer required. * Added Warning to jsonify to warn about cross-site attacks when returning a list as the outer-most element to jsonify. Fixes #232. * Fixed beaker_cache decorator to take optional keyword arguments intended for the backend cache container (such as url for memcached). * Fixed paster controller assuming the minimal template was in use when the lib.base module existed but raised an exception. * Fixed bug in XMLRPC Controller not setting proper Content-Type. Fixes #236. * Added the '-d' ('--disable-ipython') option to paster shell for disabling IPython. * Allow creation of controllers named 'setup' via paster controller. Reported by Matt Good. * Added support for generic arguments to SQLAlchemy's create_engine of the form sqlalchemy.* from the PasteDeploy config file. 0.9.5 (Apr 11th, 2007) * Fixed a Python 2.3 incompatibility with paster shell, causing the Exception: File "Pylons-0.9.5-py2.3.egg/pylons/commands.py", line 357, in command locs.update([(name, getattr(base, name)) for name in base_public]) AttributeError: keys * Fixed paster shell breaking for projects where the base package was not the first package listed in top_level.txt. Patch from Alberto Valverde. Fixes #229. * Fixed doc references to config['app_conf']. Fixes #116. * Changed `get_engine_conf` to properly evaluate sqlalchemy echo statement when its 'debug'. Fixes #226. * make_session and create_engine now accept keyword arguments to pass to SQLAlchemy's create_engine. * make_session now accepts the keyword argument 'session_kwargs' to pass to SQLAlchemy's create_session. * Fixed _inspect_call to call function with keyword arguments instead of list args. Corrects issue with action defaults that caused the value for the latter args to be in the wrong spots. Spotted by Topher. Fixes #223. * Added the allow_none option (passed to xmlrpc.dumps) to XMLRPCController. Suggested by Jaroslaw Zabiello. * Updated XMLRPC Controller with patch for name lookup and additional unit tests for the patch. Fixes #216. * Updated docs for validate decorator to more clearly illustrate what the post_only args apply to. Fixes #221. * Added ability to return strings in the WSGIController. Fixes #218. * Added lazy i18n translation functions. Patch from David Smith. Fixes #181. * Added fix for XMLRPCController system.methodHelp function and unit test. Patch and unit test submitted by Graham Higgins. * Fixed bug in validate decorator with new UnicodeMultiDict response content not properly retaining form content as unicode for formencode's htmlfill. * Fixed bug in XMLRPC Controller with xmlrpclib Faults not being properly transformed into a WSGI response within the controller. * WARNING: Pylons now requires the decorator module: it no longer packages it as pylons.decorator. Code relying on the pylons.decorator.decorator function will trigger a deprecation warning and should be changed to use decorator.decorator. * WARNING: pylons.h was deprecated for using projects' lib.helpers module directly in 0.9.3. pylons.h is now formally deprecated (emits DeprecationWarnings). Projects still accessing pylons.h must change the following import: from pylons import h to: import MYPROJ.lib.helpers as h * pylons.jsonify and pylons.Controller references have been deprecated (they are misplaced references). They continue to be available at pylons.decorators.jsonify and pylons.controllers.Controller, as they always have been. * Updated templating Buffet to recognize format parameter and properly pass it to the template engine. * Updated LICENSE for new year and to indicate license covering templates generated. Fixes #188. * Interactive debugger now supports Mako. After r1780 if you are using a custom theme you will need to change '%(myghty_data)s' to '%(template_data)s' in your template. If you are using JavaScript the tab id is now "template_data". * Fixed bug in WSGIController with private function attempts not returning a valid WSGI response. * Added full unit test coverage of cache decorator. * Adding messages binary file, enabling i18n unit tests. Updating pylons.i18n to import LanguageError. Fixes #193. * Adding i18n tests, not active yet as they're waiting on a binary file from a patch. Refs #193. * Updated tests so that they now work with nose, removing py.test requirement. * Switching config setup to load keys into main config dict with app_conf and global_conf keys set for any code looking for those keys. Fixes #116. * PylonsInstaller is now the default paste.app_install entry point for new projects: this makes Cheetah no longer required for the paster make-config command. (Thanks Alexander Schremmer, Ian Bicking) * Added custom redirect_to function in pylons.helpers that will take an optional _response arg to pull headers and cookies out for preservation during a redirect. Fixes #136. * Changed config.Config.__init__ to take all options as keyword args so unused args can be skipped. Fixes #162. * The request object can now automatically decode GET/POST/params vars to unicode, when its charset attribute is set. * Added a new request_settings keyword arg to Config's constructor. Allows setting the default charset and errors values of of the request object. * Deprecated Config constructor's default_charset keyword arg. Use Config's response_settings keyword arg instead. * Fixed paster controller to test for lib.base and only add that import statement when its present. This fixes the controller template when used with minimal Pylons project templates. Fixes #140 and fixes #139. * Fixed the paster shell error: KeyError: 'pylons.routes_dict' when calling app.get and app.post. * Fixed paster shell not working on projects with names containing hyphens. * Fixed the config directive 'sqlalchemy.echo' set to False being interpreted as True. Patch by Alex Conrad. * Fixed paster shell not restoring CONFIG['global_conf']. 0.9.4.1 (Jan. 5th, 2007) * Added restcontroller command that generates a RESTful controller template and provides the appropriate map.resource command to add. Suggested by Matthew Scott. * Fixed SQLObject pylons.database.PackageHub error: exceptions.NameError: global name 'CONFIG' is not defined * Fixed pylons.database.session_context not working outside of requests (such as in websetup.py). * Updated template options config to take template options for multiple engines for less binding to Myghty. * Fixed paster shell incorrectly importing the the tuple (model,) as the model object. 0.9.4 (Dec. 29th, 2006) * WARNING: Removed the lang_extract and lang_compile commands. They used pygettext.py and its associated msgfmt.py, which lacked the ability to extract ngettext style function calls and had issues with unicode strings. The new I18NToolBox project aims to provide this functionality (and more) via the gettext command line utilities. http://i18ntoolbox.ufsoft.org * All Pylons special objects are now available within paster shell (not just h and g). * WARNING: Myghty's allow_globals config var has changed, causing the following when running pre-compiled templates: Error(TypeError): do_run_component() takes exactly 13 non-keyword arguments (5 given) Delete the compiled Myghty templates directory (specified by cache_dir or myghty_data_dir in the config file) to resolve the error. * Changed i18n functions in templates to use proxy objects so that using set_lang in a template works right. Fixes #153. * Now allowing any template plugin to overwrite global PYLONS_VARS (such as c, g), not just pylonsmyghty. * Adding SQLAlchemy support to the database.py file. Saves the session engine to g to maintain it during the apps lifetime. Uses SessionContext plugin for management of the current session. * Updated config object so that init_app can take an optional template engine argument to declare the default template engine. * Updated Myghty plugin to use extra_vars_func when passed in. * Fixed Buffet to use extra_vars_func properly. * Fixed the validate decorator when there are validation errors and variable_decode=True: now passing the original params to htmlfill.render instead of the varable_decode'd version. Patch by FlimFlamMan. * Added ungettext function for use with pluralized i18n, and the N_ function (gettext_noop) to mark global strings for translation. Added ungettext, N_ and translator objects to be globals for templates. Refs #126. * WARNING: The localization function '_' now uses ugettext (returns unicode strings) instead of gettext. To preserve the old behavior, append the following line to your project's lib.base and lib.helpers imports: from pylons.helpers import gettext as _ * Pylons special objects are now available within the interactive debugger (deprecating _attach_locals). * Added setup-app run before unit tests run so that webapp has proper setup tasks handled. Fixes #113. * Added paste.deploy.CONFIG setup to middleware.py, websetup.py and testing files in the Pylons project templates. Closes #112. * Added security policy doc to index for use as Pylons security policy. Closes #91. * Improved the repr() of the c context object to show attributes. * Set environ['paste.testing_variables'] whenever that key is available, not just in testing mode. * Added capability to have an action be a generator function. * Added introspection capability to XMLRPCController and signature checking. * Updated Controller to use additional arg lookup scheme so that the source of the function args for _inspect_call can be easily overridden. * Updated RPCController, renamed to XMLRPCController. XMLRPCController now functions properly and will automatically return proper xmlrpc responses. * Added test configuration ini file to default template. Closes #114. * Fixed problem with pylons.database.PackageHub.__get__ raising errors other than AttributeError when the database isn't configured. Added new UnconfiguredConnectionError exception, instead of just KeyError or TypeError (depending on what part of the configuration failed). * Fixed default g init, since bare object has no init method. Reported by Ian Bicking. * Fixed issue with SQLObject method override having wrong name. Reported by climbus with patch. Fixes #133. * Moved log function to pylons.helpers and translation functions to pylons.i18n. using pylons.util purely for Pylons internal util functions. * WARNING: Removed 0.8.x legacy code and backwards compatibility functions. * PylonsApp now has option to not use Routes middleware, default resolving uses new wsgi.org routing_args spec. * Refactored routes dispatching to use new Routes middleware. * Fixed paster shell command to properly acquire mapper object without relying on the template being configured in a specific manner. * Added keyword argument pool_connection to pylons.database.PackageHub; if set to false then SQLObject connections won't use pooled database connections (a new connection will be opened for each request). 0.9.3 (Nov 1st, 2006) * Updated project template to support full_stack option to make it easier to use Pylons apps within larger WSGI stacks. * Added deprecation warnings to legacy objects and for 1.0 functionality that will change. * Added cache decorator and Cheetah template functional tests. Patch and unit tests provided by Climbus. * Fixed readline support in the stock interactive console paster shell. Reported by Alex Conrad. * A controller's __after__ method will now be called after actions invoke redirect_to (or raise any HTTPException). Reported by David Turner. * Fixed config to use myghty_data_dir instead of cache_dir setting if its set as well. Reported by Shannon -jj Behrens. * Added traceback hiding so that more of the traceback relating to Pylons code is removed from the default traceback making it easier to focus on the code in an app that actually caused the problem. Closes #119. * Added ability to use '_' translation method directly from templates and in the controller without having to use it under h._. * Added 's' and 'l' Myghty escaping flags to default project templates. Suggested by Jamie Wilkinson. Closes #110. * Fixed SCGI bug with QUERY_STRING test when WSGI states it doesn't have to exist. Reported by Riklaunim. * Added pylons_minimal template, prone to fine-tuning. * Added option for PylonsApp to take a globals object to avoid checking a hardcoded path for a Globals object. * Removed old Helpers legacy object entirely, replaced pylons.h with proper StackedObjectProxy. Cleaned up PylonsApp and PylonsBaseApp to accept a helpers reference so that Pylons can be ignorant of where the helpers came from. * Fixed bug with lang app_conf option being set improperly. Reported by Laurent. * Fixed pylons.h to work proper with new and old-style helper imports. * Fixed render functions always passing disable_unicode=False to Myghty. 0.9.2 (Sept. 7th, 2006) * Fixed problem with dashes in controller names, resolves #107. * Updated default ini file to use localhost from address. Refs #104. * Updated default development.ini to use a single cache_dir setting which is the base dir that cache files, session files, and template caching will be saved in. Config object now looks to cache_dir setting properly for Myghty templates. Fixes #104. * Updated default template controller to provide better example of directly serving Myghty templates. * Fixed legacy (0.8) controller methods' **ARGS (also m.request_args and pylons.params) to be of the expected mixed dictionary instead of MultiDict. * Fixed _attach_locals not attaching g and cache objects. * Added g object to the shell. Works as long as the Pylons app can respond to a path of '/'. The pylons.g variable will also be setup properly for use in the shell. * Myghty template options are now passed properly to the template creation, and allow_globals now works to add Myghty template globals. * Re-organized helpers, switched Helpers class to use static methods to reduce code duplication. * Helpers cleanup: - Old-style Helper object uses StackedObjectProxy just like the new scheme, thus avoiding possible WSGI stack issues. - New project templates use new-style Helpers scheme. - Updated wsgiapp to utilize new Helpers cleanup style. The 'h' object is now friendlier to use, and maps directly to a projects lib.helpers file. No more wacky Helpers object proxying to it. - Added translator global to __init__.py for use with new Helpers cleanup. - Copied Helpers function methods directly into util so they can be used stand-alone. - Deprecated h.lang (for h.set_lang/h.get_lang) * Moved the 'default_charset' option from PylonsApp's constructor to Config's. * Added 'error' controller route to the top of the Pylons template to avoid the common issue people discover when removing the generic default route. * Changing validate decorator to have variable_decode option, which will also run formencode's variable_decode function over the form input. * Switched to using Context obj classes instead of RequestLocal which is being phased out. * Added an 'encode_variables' option to the validate decorator. * Switched all current_obj calls to _current_obj to avoid triggering deprecation warnings. * Added is_xhr to Request object in Paste. * Bumping up dependency to latest Paste. * Switching back to prior controller import check, throwing a more detailed error with a suggest fix should the user really want a URL with that name in it. (refs #67) * Fixes bug with prior fix for #67. Wasn't properly testing the full package name to include the current project which would incorrectly restrict valid controller names (refs #67). * Fixed '_method_' checking to test in a more efficient manner. * Added deprecation warning for legacy mode activation. Not necessary to update multiple files, as all of legacy mode is enabled via the Legacy WSGI app. Fixes #89. * Fixed controller command to check controller name and refuse to create controllers that have name clashes with existing modules that could be imported. Reported (with patch) by Chuck Adams. Fixes #67. * Added capability for 'c' object to throw an exception when an attribute is used that doesn't exist. Closes #84. * Fix for endless error document call when an error document controller wants to throw a error that the error_mapper catches. 0.9.1 (August 11th, 2006) * Fixed __all__ export typo in templating.py. Added example of render with a template. * Fixed issue with set_lang not using proper CONFIG var. * Minor tweaks to update docs in pylons.helpers and move remaining legacy code into legacy module. Updated wsgiapp to refer to new locations of legacy compatibility objects. * The interactive debugger's 'full traceback' link is now only displayed when the full traceback differs from the regular (includes hidden frames). * Providing an optional text version of exception tracebacks and the associated extra data within the interactive debugger. * The 'Debug at: http://hostname/_debug/view/XXXXX' URLs now display the interactive debugger properly skinned. * Fixed issue in PasteScript with new controller sub-directories lacking a __init__.py file. This fixes an import error when using the controller. PasteScript dependency updated to fixed version. Reported by Artur Lew. * Removed lowercasing of the module name during resolver import. * Removed [full] recommendation from docs. 0.9 (July 28th, 2006) * config file option 'debug' now defaults to false when undefined * Removed the components directory from the template * Updated paste.errordocuments support * Fix for multi-word controller names such that front_page / FrontPageController can be used as a controller name. Patch contributed by Timo Mihaljov. * Cleaned up imports in wsgiapp and new project to better reflect where things really come from. * Removed unnecessary myghty usage from wsgiapp for url unescaping, now uses urllib as the myghty function did. * Removing 'response' to import, sticking with Response as its more consistent with the fact that Response is a class/factory and not an instance like request, and the other lower-case objects. * Added redirect_to under pylons.helpers, and added import from helpers directly into lib/base.py for use in controllers. * Consolidated legacy objects into legacy module. * Adding abort method that raises the appropriate exception for the status code. * Removing form_fill, obsolete by the validate decorator. * Relocated 'params' object to only take effect in legacy mode. * Updated Pylons template to use WSGIController as the new default Controller. * Altered the wsgi dispatch to examine the controller, and instantiate it if it's just a class. Otherwise, if the controller is a subclass of Controller but not of WSGIController, it assumes its an older Controller class that may return a WSGIResponse, and calls it appropriately. * Dispatch now fixes up environ to move 'path_info' Route var into the WSGI PATH_INFO, and the rest is pushed into SCRIPT_NAME. This is for use with other WSGI apps under controller mount points. * Added WSGIController which takes a normal WSGI interface call and returns the appropriate WSGI response. * Added automatic copying of Route variables declared in an action's function definition to the 'c' object. * WebHelpers' .js files are now automatically published under the '/javascripts/' URL path. Individual WebHelpers' .js files can be overridden by other .js files inside the project's 'public/javascripts' directory * Added exception toss when a template engine doesn't exist. * Added alias option to Buffet to support aliasing more template engines to other engine names * Buffet enhancements to support caching of any template with any template engine * All render commands processed through Buffet * Backwards compatibility 'm' object for use with legacy projects * Added use of Beaker middleware for session and caching functionality * Fixed error traceback and updated template to use proper error images and stylesheets. 0.8.2 (**dev**) * Fixed default controller to allow for actions with - in them. The - will be replaced with an underscore, while the original action name in the mapper dict is unchanged. Patch by Thomas Sidwick. 0.8.1 (May 10th, 2006) * Added REST decorators and re-arranged decorator layout to support more styles of decorators for future expansion. * Fixed dependency requirement bug that had Pylons locked to simplejson 1.1 even though a newer version was out. ================================================ FILE: LICENSE ================================================ Copyright (c) 2005-2015 Ben Bangert, James Gardner, Philip Jenvey and contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. 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. 3. The name of the author or contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 THE AUTHOR OR CONTRIBUTORS 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. ------------------------------------------------------------------------------- ALL TEMPLATES GENERATED ARE COVERED UNDER THE FOLLOWING LICENSE: Copyright (c) 2005-2015 Ben Bangert, James Gardner, Philip Jenvey and contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following condition is met: The name of the author or contributors may not be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 THE AUTHOR OR CONTRIBUTORS 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. ================================================ FILE: MANIFEST.in ================================================ recursive-include pylons/templates * recursive-include pylons/media * recursive-include tests * recursive-include test_files * include CHANGELOG include LICENSE include UPGRADING recursive-exclude tests/test_units/session * global-exclude .DS_Store *.hgignore *.hgtags ================================================ FILE: README.rst ================================================ Pylons ++++++ .. image:: https://secure.travis-ci.org/Pylons/pylons.png?branch=master :alt: Build Status :target: https://secure.travis-ci.org/Pylons/pylons Pylons is a rapid web application development framework. .. note:: Pylons has merged with repoze.bfg, and is now in maintenance-only mode. It's highly recommended that new projects start with the new merged web framework, `pyramid `_. Install ======= `Read the online Installation instructions `_. If you want to install from source you can run the following command: .. code-block :: bash $ python setup.py install This will display a message and download setuptools if the module is not already installed. It will then install Pylons and all its dependencies. You may need root privileges to install setuptools. Testing ======= To test the source distribution run the following command: .. code-block :: bash $ python setup.py test This will install additional dependencies needed for the tests. As above, you may need root privileges. Documentation ============= `Read the complete Pylons web framework documentation `_. `Definitive Guide to Pylons `_ is a book about Pylons published by Apress, written by James Gardner, with free HTML rendering. Generating documentation requires Sphinx: .. code-block :: bash $ easy_install Sphinx Then to build the documentation use the commands: .. code-block :: bash $ cd pylons/docs/ $ make html ================================================ FILE: UPGRADING ================================================ Upgrading your Pylons Project ============================= Pylons projects should be updated using the paster command create. In addition to creating new projects, paster create when run over an existing project will provide several ways to update the project template to the latest version. Using this tool properly can make upgrading a fairly minor task. For the purpose of this document, the project being upgraded will be called 'demoapp' and all commands will use that name. Running paster create to upgrade -------------------------------- You'll first need to cd to the directory *above* your projects main directory. The main directory is the one that contains your setup.py, setup.cfg, and development.ini files. .. code:: bash /home/joe/demoapp $ cd .. /home/joe $ Then run paster create on the project directory: .. code:: bash /home/joe $ paster create demoapp -t pylons paster will prompt you on how to handle conflicts and updates to the existing project files. The options let you (hit the key in the parens to perform the operation): (d)iff them, and show you the changes between your projects file and the one that has changed in Pylons (b)ackup the file, and copy the new version into its place. The old one will end in .bak (y)es to overwrite the existing file with the new one. *Not recommended* since you will then have no way to see your existing one, unless you have seen the diff first and know there is no changes you're losing. (n)o to overwrite, and just keep your existing file. Also safe if you know that nothing has changed. It's recommended when upgrading your project that you always look at the diff first to see whats changed. Then either overwrite your existing one if you are not going to lose changes you want, or backup yours and write the new one in. You can then manually compare and add your changes back in. ================================================ FILE: ez_setup.py ================================================ #!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c9" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import sys, os try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) ================================================ FILE: pylons/__init__.py ================================================ """Base objects to be exported for use in Controllers""" # Import pkg_resources first so namespace handling is properly done so the # paste imports work import pkg_resources from paste.registry import StackedObjectProxy from pylons.configuration import config from pylons.controllers.util import Request from pylons.controllers.util import Response __all__ = ['app_globals', 'cache', 'config', 'request', 'response', 'session', 'tmpl_context', 'url', 'Request', 'Response'] def __figure_version(): try: from pkg_resources import require import os # NOTE: this only works when the package is either installed, # or has an .egg-info directory present (i.e. wont work with raw # SVN checkout) info = require('pylons')[0] if os.path.normcase(os.path.realpath(os.path.dirname( os.path.dirname(__file__)))) == info.location: return info.version else: return '(not installed)' except: return '(not installed)' __version__ = __figure_version() app_globals = StackedObjectProxy(name="app_globals") cache = StackedObjectProxy(name="cache") request = StackedObjectProxy(name="request") response = StackedObjectProxy(name="response") session = StackedObjectProxy(name="session") tmpl_context = StackedObjectProxy(name="tmpl_context or C") url = StackedObjectProxy(name="url") translator = StackedObjectProxy(name="translator") ================================================ FILE: pylons/commands.py ================================================ """Paster Commands, for use with paster in your project .. highlight:: bash The following commands are made available via paster utilizing setuptools points discovery. These can be used from the command line when the directory is the Pylons project. Commands available: ``controller`` Create a Controller and accompanying functional test ``restcontroller`` Create a REST Controller and accompanying functional test ``shell`` Open an interactive shell with the Pylons app loaded Example usage:: ~/sample$ paster controller account Creating /Users/ben/sample/sample/controllers/account.py Creating /Users/ben/sample/sample/tests/functional/test_account.py ~/sample$ .. admonition:: How it Works :command:`paster` is a command line script (from the PasteScript package) that allows the creation of context sensitive commands. :command:`paster` looks in the current directory for a ``.egg-info`` directory, then loads the ``paster_plugins.txt`` file. Using setuptools entry points, :command:`paster` looks for functions registered with setuptools as :func:`paste.paster_command`. These are defined in the entry_points block in each packages :file:`setup.py` module. This same system is used when running :command:`paster create` to determine what templates are available when creating new projects. """ import os import sys import paste.fixture import paste.registry from paste.deploy import loadapp from paste.script.command import Command, BadCommand from paste.script.filemaker import FileOp from tempita import paste_script_template_renderer import pylons import pylons.util as util __all__ = ['ControllerCommand', 'RestControllerCommand', 'ShellCommand'] def can_import(name): """Attempt to __import__ the specified package/module, returning True when succeeding, otherwise False""" try: __import__(name) return True except ImportError: return False def is_minimal_template(package, fail_fast=False): """Determine if the specified Pylons project (package) uses the Pylons Minimal Template. fail_fast causes ImportErrors encountered during detection to be raised. """ minimal_template = False try: # Check if PACKAGE.lib.base exists __import__(package + '.lib.base') except ImportError, ie: if 'No module named lib.base' in str(ie): minimal_template = True except: # PACKAGE.lib.base exists but throws an error if fail_fast: raise return minimal_template def defines_render(package): """Determine if the specified Pylons project (package) defines a render callable in their base module """ base_module = (is_minimal_template(package) and package + '.controllers' or package + '.lib.base') try: base = __import__(base_module, globals(), locals(), ['__doc__']) except: return False return callable(getattr(base, 'render', None)) def validate_name(name): """Validate that the name for the controller isn't present on the path already""" if not name: # This happens when the name is an existing directory raise BadCommand('Please give the name of a controller.') # 'setup' is a valid controller name, but when paster controller is ran # from the root directory of a project, importing setup will import the # project's setup.py causing a sys.exit(). Blame relative imports if name != 'setup' and can_import(name): raise BadCommand( "\n\nA module named '%s' is already present in your " "PYTHON_PATH.\nChoosing a conflicting name will likely cause " "import problems in\nyour controller at some point. It's " "suggested that you choose an\nalternate name, and if you'd " "like that name to be accessible as\n'%s', add a route " "to your projects config/routing.py file similar\nto:\n" " map.connect('%s', controller='my_%s')" \ % (name, name, name, name)) return True def check_controller_existence(base_package, directory, name): """Check if given controller already exists in project.""" filename = os.path.join(base_package, 'controllers', directory, name + '.py') if os.path.exists(filename): raise BadCommand('Controller %s already exists.' % os.path.join(directory, name)) class ControllerCommand(Command): """Create a Controller and accompanying functional test The Controller command will create the standard controller template file and associated functional test to speed creation of controllers. Example usage:: yourproj% paster controller comments Creating yourproj/yourproj/controllers/comments.py Creating yourproj/yourproj/tests/functional/test_comments.py If you'd like to have controllers underneath a directory, just include the path as the controller name and the necessary directories will be created for you:: yourproj% paster controller admin/trackback Creating yourproj/controllers/admin Creating yourproj/yourproj/controllers/admin/trackback.py Creating yourproj/yourproj/tests/functional/test_admin_trackback.py """ summary = __doc__.splitlines()[0] usage = '\n' + __doc__ min_args = 1 max_args = 1 group_name = 'pylons' default_verbosity = 3 parser = Command.standard_parser(simulate=True) parser.add_option('--no-test', action='store_true', dest='no_test', help="Don't create the test; just the controller") def command(self): """Main command to create controller""" try: file_op = FileOp(source_dir=('pylons', 'templates')) try: name, directory = file_op.parse_path_name_args(self.args[0]) except: raise BadCommand('No egg_info directory was found') # Check the name isn't the same as the package base_package = file_op.find_dir('controllers', True)[0] if base_package.lower() == name.lower(): raise BadCommand( 'Your controller name should not be the same as ' 'the package name %r.' % base_package) # Validate the name name = name.replace('-', '_') validate_name(name) # Determine the module's import statement if is_minimal_template(base_package): importstatement = ('from %s.controllers import BaseController' % base_package) else: importstatement = ('from %s.lib.base import BaseController' % base_package) if defines_render(base_package): importstatement += ', render' # Setup the controller fullname = os.path.join(directory, name) controller_name = util.class_name_from_module_name( name.split('/')[-1]) if not fullname.startswith(os.sep): fullname = os.sep + fullname testname = fullname.replace(os.sep, '_')[1:] module_dir = directory.replace('/', os.path.sep) check_controller_existence(base_package, module_dir, name) file_op.template_vars.update( {'name': controller_name, 'fname': os.path.join(directory, name).replace('\\', '/'), 'tmpl_name': name, 'package': base_package, 'importstatement': importstatement}) file_op.copy_file(template='controller.py_tmpl', dest=os.path.join('controllers', directory), filename=name, template_renderer=paste_script_template_renderer) if not self.options.no_test: file_op.copy_file( template='test_controller.py_tmpl', dest=os.path.join('tests', 'functional'), filename='test_' + testname, template_renderer=paste_script_template_renderer) except BadCommand, e: raise BadCommand('An error occurred. %s' % e) except: msg = str(sys.exc_info()[1]) raise BadCommand('An unknown error occurred. %s' % msg) class RestControllerCommand(Command): """Create a REST Controller and accompanying functional test The RestController command will create a REST-based Controller file for use with the :meth:`~routes.mapper.Mapper.resource` REST-based dispatching. This template includes the methods that :meth:`~routes.mapper.Mapper.resource` dispatches to in addition to doc strings for clarification on when the methods will be called. The first argument should be the singular form of the REST resource. The second argument is the plural form of the word. If its a nested controller, put the directory information in front as shown in the second example below. Example usage:: yourproj% paster restcontroller comment comments Creating yourproj/yourproj/controllers/comments.py Creating yourproj/yourproj/tests/functional/test_comments.py If you'd like to have controllers underneath a directory, just include the path as the controller name and the necessary directories will be created for you:: yourproj% paster restcontroller admin/tracback admin/trackbacks Creating yourproj/controllers/admin Creating yourproj/yourproj/controllers/admin/trackbacks.py Creating yourproj/yourproj/tests/functional/test_admin_trackbacks.py """ summary = __doc__.splitlines()[0] usage = '\n' + __doc__ min_args = 2 max_args = 2 group_name = 'pylons' default_verbosity = 3 parser = Command.standard_parser(simulate=True) parser.add_option('--no-test', action='store_true', dest='no_test', help="Don't create the test; just the controller") def command(self): """Main command to create controller""" try: file_op = FileOp(source_dir=('pylons', 'templates')) try: singularname, singulardirectory = \ file_op.parse_path_name_args(self.args[0]) pluralname, pluraldirectory = \ file_op.parse_path_name_args(self.args[1]) except: raise BadCommand('No egg_info directory was found') # Check the name isn't the same as the package base_package = file_op.find_dir('controllers', True)[0] if base_package.lower() == pluralname.lower(): raise BadCommand( 'Your controller name should not be the same as ' 'the package name %r.' % base_package) # Validate the name for name in [pluralname]: name = name.replace('-', '_') validate_name(name) # Determine the module's import statement if is_minimal_template(base_package): importstatement = ('from %s.controllers import BaseController' % base_package) else: importstatement = ('from %s.lib.base import BaseController' % base_package) if defines_render(base_package): importstatement += ', render' module_dir = pluraldirectory.replace('/', os.path.sep) check_controller_existence(base_package, module_dir, name) # Setup the controller fullname = os.path.join(pluraldirectory, pluralname) controller_name = util.class_name_from_module_name( pluralname.split('/')[-1]) if not fullname.startswith(os.sep): fullname = os.sep + fullname testname = fullname.replace(os.sep, '_')[1:] nameprefix = '' path = '' if pluraldirectory: nameprefix = pluraldirectory.replace(os.path.sep, '_') + '_' path = pluraldirectory + '/' controller_c = '' if nameprefix: controller_c = ", controller='%s', \n\t" % \ '/'.join([pluraldirectory, pluralname]) controller_c += "path_prefix='/%s', name_prefix='%s'" % \ (pluraldirectory, nameprefix) command = "map.resource('%s', '%s'%s)\n" % \ (singularname, pluralname, controller_c) file_op.template_vars.update( {'classname': controller_name, 'pluralname': pluralname, 'singularname': singularname, 'name': controller_name, 'nameprefix': nameprefix, 'package': base_package, 'path': path, 'resource_command': command.replace('\n\t', '\n%s#%s' % \ (' ' * 4, ' ' * 9)), 'fname': os.path.join(pluraldirectory, pluralname), 'importstatement': importstatement}) resource_command = ("\nTo create the appropriate RESTful mapping, " "add a map statement to your\n") resource_command += ("config/routing.py file near the top like " "this:\n\n") resource_command += command file_op.copy_file(template='restcontroller.py_tmpl', dest=os.path.join('controllers', pluraldirectory), filename=pluralname, template_renderer=paste_script_template_renderer) if not self.options.no_test: file_op.copy_file( template='test_restcontroller.py_tmpl', dest=os.path.join('tests', 'functional'), filename='test_' + testname, template_renderer=paste_script_template_renderer) print resource_command except BadCommand, e: raise BadCommand('An error occurred. %s' % e) except: msg = str(sys.exc_info()[1]) raise BadCommand('An unknown error occurred. %s' % msg) class RoutesCommand(Command): """Print the applications routes The optional CONFIG_FILE argument specifies the config file to use. CONFIG_FILE defaults to 'development.ini'. Example:: $ paster routes my-development.ini """ summary = __doc__.splitlines()[0] usage = '\n' + __doc__ min_args = 0 max_args = 1 group_name = 'pylons' parser = Command.standard_parser(simulate=True) parser.add_option('-q', action='count', dest='quiet', default=0, help=("Do not load logging configuration from the " "config file")) def command(self): """Main command to create a new shell""" self.verbose = 3 if len(self.args) == 0: # Assume the .ini file is ./development.ini config_file = 'development.ini' if not os.path.isfile(config_file): raise BadCommand('%sError: CONFIG_FILE not found at: .%s%s\n' 'Please specify a CONFIG_FILE' % \ (self.parser.get_usage(), os.path.sep, config_file)) else: config_file = self.args[0] config_name = 'config:%s' % config_file here_dir = os.getcwd() if not self.options.quiet: # Configure logging from the config file self.logging_file_config(config_file) # Load the wsgi app first so that everything is initialized right wsgiapp = loadapp(config_name, relative_to=here_dir) test_app = paste.fixture.TestApp(wsgiapp) # Query the test app to setup the environment and get the mapper tresponse = test_app.get('/_test_vars') mapper = tresponse.config.get('routes.map') if mapper: print mapper class ShellCommand(Command): """Open an interactive shell with the Pylons app loaded The optional CONFIG_FILE argument specifies the config file to use for the interactive shell. CONFIG_FILE defaults to 'development.ini'. This allows you to test your mapper, models, and simulate web requests using ``paste.fixture``. Example:: $ paster shell my-development.ini """ summary = __doc__.splitlines()[0] usage = '\n' + __doc__ min_args = 0 max_args = 1 group_name = 'pylons' parser = Command.standard_parser(simulate=True) parser.add_option('-d', '--disable-ipython', action='store_true', dest='disable_ipython', help="Don't use IPython if it is available") parser.add_option('-q', action='count', dest='quiet', default=0, help=("Do not load logging configuration from the " "config file")) def command(self): """Main command to create a new shell""" self.verbose = 3 if len(self.args) == 0: # Assume the .ini file is ./development.ini config_file = 'development.ini' if not os.path.isfile(config_file): raise BadCommand('%sError: CONFIG_FILE not found at: .%s%s\n' 'Please specify a CONFIG_FILE' % \ (self.parser.get_usage(), os.path.sep, config_file)) else: config_file = self.args[0] config_name = 'config:%s' % config_file here_dir = os.getcwd() locs = dict(__name__="pylons-admin") if not self.options.quiet: # Configure logging from the config file self.logging_file_config(config_file) # Load locals and populate with objects for use in shell sys.path.insert(0, here_dir) # Load the wsgi app first so that everything is initialized right wsgiapp = loadapp(config_name, relative_to=here_dir) test_app = paste.fixture.TestApp(wsgiapp) # Query the test app to setup the environment tresponse = test_app.get('/_test_vars') request_id = int(tresponse.body) # Disable restoration during test_app requests test_app.pre_request_hook = lambda self: \ paste.registry.restorer.restoration_end() test_app.post_request_hook = lambda self: \ paste.registry.restorer.restoration_begin(request_id) # Restore the state of the Pylons special objects # (StackedObjectProxies) paste.registry.restorer.restoration_begin(request_id) # Determine the package name from the pylons.config object pkg_name = pylons.config['pylons.package'] # Start the rest of our imports now that the app is loaded if is_minimal_template(pkg_name, True): model_module = None helpers_module = pkg_name + '.helpers' base_module = pkg_name + '.controllers' else: model_module = pkg_name + '.model' helpers_module = pkg_name + '.lib.helpers' base_module = pkg_name + '.lib.base' if model_module and can_import(model_module): locs['model'] = sys.modules[model_module] if can_import(helpers_module): locs['h'] = sys.modules[helpers_module] exec ('from pylons import app_globals, config, request, response, ' 'session, tmpl_context, url') in locs exec ('from pylons.controllers.util import abort, redirect') in locs exec 'from pylons.i18n import _, ungettext, N_' in locs locs.pop('__builtins__', None) # Import all objects from the base module __import__(base_module) base = sys.modules[base_module] base_public = [__name for __name in dir(base) if not \ __name.startswith('_') or __name == '_'] locs.update((name, getattr(base, name)) for name in base_public) locs.update(dict(wsgiapp=wsgiapp, app=test_app)) mapper = tresponse.config.get('routes.map') if mapper: locs['mapper'] = mapper banner = " All objects from %s are available\n" % base_module banner += " Additional Objects:\n" if mapper: banner += " %-10s - %s\n" % ('mapper', 'Routes mapper object') banner += " %-10s - %s\n" % ('wsgiapp', "This project's WSGI App instance") banner += " %-10s - %s\n" % ('app', 'paste.fixture wrapped around wsgiapp') try: if self.options.disable_ipython: raise ImportError() # try to use IPython if possible try: try: # 1.0 <= ipython from IPython.terminal.embed import InteractiveShellEmbed except ImportError: # 0.11 <= ipython < 1.0 from IPython.frontend.terminal.embed import InteractiveShellEmbed shell = InteractiveShellEmbed(banner2=banner) except ImportError: # ipython < 0.11 from IPython.Shell import IPShellEmbed shell = IPShellEmbed(argv=self.args) shell.set_banner(shell.IP.BANNER + '\n\n' + banner) try: shell(local_ns=locs, global_ns={}) finally: paste.registry.restorer.restoration_end() except ImportError: import code py_prefix = sys.platform.startswith('java') and 'J' or 'P' newbanner = "Pylons Interactive Shell\n%sython %s\n\n" % \ (py_prefix, sys.version) banner = newbanner + banner shell = code.InteractiveConsole(locals=locs) try: import readline except ImportError: pass try: shell.interact(banner) finally: paste.registry.restorer.restoration_end() ================================================ FILE: pylons/configuration.py ================================================ """Configuration object and defaults setup The PylonsConfig object is initialized in pylons projects inside the :file:`config/environment.py` module. Importing the :data:`config` object from module causes the PylonsConfig object to be created, and setup in app-safe manner so that multiple apps being setup avoid conflicts. After importing :data:`config`, the project should then call :meth:`~PylonsConfig.init_app` with the appropriate options to setup the configuration. In the config data passed with :meth:`~PylonsConfig.init_app`, various defaults are set use with Paste and Routes. """ import copy import logging import os from paste.config import DispatchingConfig from paste.deploy.converters import asbool from webhelpers.mimehelper import MIMETypes request_defaults = dict(charset='utf-8', errors='replace', decode_param_names=False, language='en-us') response_defaults = dict(content_type='text/html', charset='utf-8', errors='strict', headers={'Cache-Control': 'no-cache', 'Pragma': 'no-cache'}) log = logging.getLogger(__name__) config = DispatchingConfig() class PylonsConfig(dict): """Pylons configuration object The Pylons configuration object is a per-application instance object that retains the information regarding the global and app conf's as well as per-application instance specific data such as the mapper, and the paths for this instance. The config object is available in your application as the Pylons global :data:`pylons.config`. For example:: from pylons import config template_paths = config['pylons.paths']['templates'] There's several useful keys of the config object most people will be interested in: ``pylons.paths`` A dict of absolute paths that were defined in the applications ``config/environment.py`` module. ``pylons.environ_config`` Dict of environ keys for where in the environ to pickup various objects for registering with Pylons. If these are present then PylonsApp will use them from environ rather than using default middleware from Beaker. Valid keys are: ``session, cache`` ``pylons.strict_tmpl_context`` Whether or not the ``tmpl_context`` object should throw an attribute error when access is attempted to an attribute that doesn't exist. Defaults to True. ``pylons.tmpl_context_attach_args`` Whethor or not Routes variables should automatically be attached to the tmpl_context object when specified in a controllers method. ``pylons.request_options`` A dict of Content-Type related default settings for new instances of :class:`~pylons.controllers.util.Request`. May contain the values ``charset`` and ``errors`` and ``decode_param_names``. Overrides the Pylons default values specified by the ``request_defaults`` dict. ``pylons.response_options`` A dict of Content-Type related default settings for new instances of :class:`~pylons.controllers.util.Response`. May contain the values ``content_type``, ``charset`` and ``errors``. Overrides the Pylons default values specified by the ``response_defaults`` dict. ``routes.map`` Mapper object used for Routing. Yes, it is possible to add routes after your application has started running. """ defaults = { 'debug': False, 'pylons.package': None, 'pylons.paths': {'root': None, 'controllers': None, 'templates': [], 'static_files': None}, 'pylons.environ_config': dict(session='beaker.session', cache='beaker.cache'), 'pylons.app_globals': None, 'pylons.h': None, 'pylons.request_options': request_defaults.copy(), 'pylons.response_options': response_defaults.copy(), 'pylons.strict_tmpl_context': True, 'pylons.tmpl_context_attach_args': False, } def init_app(self, global_conf, app_conf, package=None, paths=None): """Initialize configuration for the application .. note This *must* be called at least once, as soon as possible to setup all the configuration options. ``global_conf`` Several options are expected to be set for a Pylons web application. They will be loaded from the global_config which has the main Paste options. If ``debug`` is not enabled as a global config option, the following option *must* be set: * error_to - The email address to send the debug error to The optional config options in this case are: * smtp_server - The SMTP server to use, defaults to 'localhost' * error_log - A logfile to write the error to * error_subject_prefix - The prefix of the error email subject * from_address - Whom the error email should be from ``app_conf`` Defaults supplied via the [app:main] section from the Paste config file. ``load_config`` only cares about whether a 'prefix' option is set, if so it will update Routes to ensure URL's take that into account. ``package`` The name of the application package, to be stored in the app_conf. .. versionchanged:: 1.0 ``template_engine`` option is no longer supported. """ log.debug("Initializing configuration, package: '%s'", package) conf = global_conf.copy() conf.update(app_conf) conf.update(dict(app_conf=app_conf, global_conf=global_conf)) conf.update(self.pop('environment_load', {})) if paths: conf['pylons.paths'] = paths conf['pylons.package'] = package conf['debug'] = asbool(conf.get('debug')) # Load the MIMETypes with its default types MIMETypes.init() # Ensure all the keys from defaults are present, load them if not for key, val in copy.deepcopy(PylonsConfig.defaults).iteritems(): conf.setdefault(key, val) # Load the errorware configuration from the Paste configuration file # These all have defaults, and emails are only sent if configured and # if this application is running in production mode errorware = {} errorware['debug'] = conf['debug'] if not errorware['debug']: errorware['debug'] = False errorware['error_email'] = conf.get('email_to') errorware['error_log'] = conf.get('error_log', None) errorware['smtp_server'] = conf.get('smtp_server', 'localhost') errorware['error_subject_prefix'] = conf.get( 'error_subject_prefix', 'WebApp Error: ') errorware['from_address'] = conf.get( 'from_address', conf.get('error_email_from', 'pylons@yourapp.com')) errorware['error_message'] = conf.get('error_message', 'An internal server error occurred') # Copy in some defaults if 'cache_dir' in conf: conf.setdefault('beaker.session.data_dir', os.path.join(conf['cache_dir'], 'sessions')) conf.setdefault('beaker.cache.data_dir', os.path.join(conf['cache_dir'], 'cache')) conf['pylons.cache_dir'] = conf.pop('cache_dir', conf['app_conf'].get('cache_dir')) # Save our errorware values conf['pylons.errorware'] = errorware # Load conf dict into self self.update(conf) pylons_config = PylonsConfig() # Push an empty config so all accesses to config at import time have something # to look at and modify. This config will be merged with the app's when it's # built in the paste.app_factory entry point. pylons_config.update(copy.deepcopy(PylonsConfig.defaults)) config.push_process_config(pylons_config) ================================================ FILE: pylons/controllers/__init__.py ================================================ """Standard Controllers intended for subclassing by web developers""" from pylons.controllers.core import WSGIController from pylons.controllers.xmlrpc import XMLRPCController from pylons.controllers.jsonrpc import JSONRPCController, JSONRPCError ================================================ FILE: pylons/controllers/core.py ================================================ """The core WSGIController""" import inspect import logging import types from webob.exc import HTTPException, HTTPNotFound import pylons __all__ = ['WSGIController'] log = logging.getLogger(__name__) class WSGIController(object): """WSGI Controller that follows WSGI spec for calling and return values The Pylons WSGI Controller handles incoming web requests that are dispatched from the PylonsBaseWSGIApp. These requests result in a new instance of the WSGIController being created, which is then called with the dict options from the Routes match. The standard WSGI response is then returned with start_response called as per the WSGI spec. Special WSGIController methods you may define: ``__before__`` This method is called before your action is, and should be used for setting up variables/objects, restricting access to other actions, or other tasks which should be executed before the action is called. ``__after__`` This method is called after the action is, unless an unexpected exception was raised. Subclasses of :class:`~webob.exc.HTTPException` (such as those raised by ``redirect_to`` and ``abort``) are expected; e.g. ``__after__`` will be called on redirects. Each action to be called is inspected with :meth:`_inspect_call` so that it is only passed the arguments in the Routes match dict that it asks for. The arguments passed into the action can be customized by overriding the :meth:`_get_method_args` function which is expected to return a dict. In the event that an action is not found to handle the request, the Controller will raise an "Action Not Found" error if in debug mode, otherwise a ``404 Not Found`` error will be returned. """ _pylons_log_debug = False def _perform_call(self, func, args): """Hide the traceback for everything above this method""" __traceback_hide__ = 'before_and_this' return func(**args) def _inspect_call(self, func): """Calls a function with arguments from :meth:`_get_method_args` Given a function, inspect_call will inspect the function args and call it with no further keyword args than it asked for. If the function has been decorated, it is assumed that the decorator preserved the function signature. """ # Check to see if the class has a cache of argspecs yet try: cached_argspecs = self.__class__._cached_argspecs except AttributeError: self.__class__._cached_argspecs = cached_argspecs = {} # function could be callable func_key = getattr(func, 'im_func', func.__call__) try: argspec = cached_argspecs[func_key] except KeyError: argspec = cached_argspecs[func_key] = inspect.getargspec(func_key) kargs = self._get_method_args() log_debug = self._pylons_log_debug c = self._py_object.tmpl_context environ = self._py_object.request.environ args = None if argspec[2]: if self._py_object.config['pylons.tmpl_context_attach_args']: for k, val in kargs.iteritems(): setattr(c, k, val) args = kargs else: args = {} argnames = argspec[0][isinstance(func, types.MethodType) and 1 or 0:] for name in argnames: if name in kargs: if self._py_object.config['pylons.tmpl_context_attach_args']: setattr(c, name, kargs[name]) args[name] = kargs[name] if log_debug: log.debug("Calling %r method with keyword args: **%r", func.__name__, args) try: result = self._perform_call(func, args) except HTTPException, httpe: if log_debug: log.debug("%r method raised HTTPException: %s (code: %s)", func.__name__, httpe.__class__.__name__, httpe.wsgi_response.code, exc_info=True) result = httpe # Store the exception in the environ environ['pylons.controller.exception'] = httpe # 304 Not Modified's shouldn't have a content-type set if result.wsgi_response.status_int == 304: result.wsgi_response.headers.pop('Content-Type', None) result._exception = True return result def _get_method_args(self): """Retrieve the method arguments to use with inspect call By default, this uses Routes to retrieve the arguments, override this method to customize the arguments your controller actions are called with. This method should return a dict. """ req = self._py_object.request kargs = req.environ['pylons.routes_dict'].copy() kargs['environ'] = req.environ kargs['start_response'] = self.start_response kargs['pylons'] = self._py_object return kargs def _dispatch_call(self): """Handles dispatching the request to the function using Routes""" log_debug = self._pylons_log_debug req = self._py_object.request try: action = req.environ['pylons.routes_dict']['action'] except KeyError: raise Exception("No action matched from Routes, unable to" "determine action dispatch.") action_method = action.replace('-', '_') if log_debug: log.debug("Looking for %r method to handle the request", action_method) try: func = getattr(self, action_method, None) except UnicodeEncodeError: func = None if action_method != 'start_response' and callable(func): # Store function used to handle request req.environ['pylons.action_method'] = func response = self._inspect_call(func) else: if log_debug: log.debug("Couldn't find %r method to handle response", action) if pylons.config['debug']: raise NotImplementedError('Action %r is not implemented' % action) else: response = HTTPNotFound() return response def __call__(self, environ, start_response): """The main call handler that is called to return a response""" log_debug = self._pylons_log_debug # Keep a local reference to the req/response objects self._py_object = environ['pylons.pylons'] # Keep private methods private try: if environ['pylons.routes_dict']['action'][:1] in ('_', '-'): if log_debug: log.debug("Action starts with _, private action not " "allowed. Returning a 404 response") return HTTPNotFound()(environ, start_response) except KeyError: # The check later will notice that there's no action pass start_response_called = [] def repl_start_response(status, headers, exc_info=None): response = self._py_object.response start_response_called.append(None) # Copy the headers from the global response if log_debug: log.debug("Merging pylons.response headers into " "start_response call, status: %s", status) headers.extend(header for header in response.headerlist if header[0] == 'Set-Cookie' or header[0].startswith('X-')) return start_response(status, headers, exc_info) self.start_response = repl_start_response if hasattr(self, '__before__'): response = self._inspect_call(self.__before__) if hasattr(response, '_exception'): return response(environ, self.start_response) response = self._dispatch_call() if not start_response_called: self.start_response = start_response py_response = self._py_object.response # If its not a WSGI response, and we have content, it needs to # be wrapped in the response object if isinstance(response, str): if log_debug: log.debug("Controller returned a string " ", writing it to pylons.response") py_response.body = py_response.body + response elif isinstance(response, unicode): if log_debug: log.debug("Controller returned a unicode string " ", writing it to pylons.response") py_response.unicode_body = py_response.unicode_body + \ response elif hasattr(response, 'wsgi_response'): # It's an exception that got tossed. if log_debug: log.debug("Controller returned a Response object, merging " "it with pylons.response") for name, value in py_response.headers.items(): if name.lower() == 'set-cookie': response.headers.add(name, value) else: response.headers.setdefault(name, value) try: registry = environ['paste.registry'] registry.replace(pylons.response, response) except KeyError: # Ignore the case when someone removes the registry pass py_response = response elif response is None: if log_debug: log.debug("Controller returned None") else: if log_debug: log.debug("Assuming controller returned an iterable, " "setting it as pylons.response.app_iter") py_response.app_iter = response response = py_response if hasattr(self, '__after__'): after = self._inspect_call(self.__after__) if hasattr(after, '_exception'): after.wsgi_response = True response = after if hasattr(response, 'wsgi_response'): # Copy the response object into the testing vars if we're testing if 'paste.testing_variables' in environ: environ['paste.testing_variables']['response'] = response if log_debug: log.debug("Calling Response object to return WSGI data") return response(environ, self.start_response) if log_debug: log.debug("Response assumed to be WSGI content, returning " "un-touched") return response ================================================ FILE: pylons/controllers/jsonrpc.py ================================================ """The base WSGI JSONRPCController""" import inspect import json import logging import types import urllib from paste.response import replace_header from pylons.controllers import WSGIController from pylons.controllers.util import abort, Response __all__ = ['JSONRPCController', 'JSONRPCError', 'JSONRPC_PARSE_ERROR', 'JSONRPC_INVALID_REQUEST', 'JSONRPC_METHOD_NOT_FOUND', 'JSONRPC_INVALID_PARAMS', 'JSONRPC_INTERNAL_ERROR'] log = logging.getLogger(__name__) JSONRPC_VERSION = '2.0' class JSONRPCError(BaseException): def __init__(self, code, message): self.code = code self.message = message self.data = None def __str__(self): return str(self.code) + ': ' + self.message def as_dict(self): """Return a dictionary representation of this object for serialization in a JSON-RPC response.""" error = dict(code=self.code, message=self.message) if self.data: error['data'] = self.data return error JSONRPC_PARSE_ERROR = JSONRPCError(-32700, "Parse error") JSONRPC_INVALID_REQUEST = JSONRPCError(-32600, "Invalid Request") JSONRPC_METHOD_NOT_FOUND = JSONRPCError(-32601, "Method not found") JSONRPC_INVALID_PARAMS = JSONRPCError(-32602, "Invalid params") JSONRPC_INTERNAL_ERROR = JSONRPCError(-32603, "Internal error") _reserved_errors = dict(parse_error=JSONRPC_PARSE_ERROR, invalid_request=JSONRPC_INVALID_REQUEST, method_not_found=JSONRPC_METHOD_NOT_FOUND, invalid_params=JSONRPC_INVALID_PARAMS, internal_error=JSONRPC_INTERNAL_ERROR) def jsonrpc_error(req_id, error): """Generate a Response object with a JSON-RPC error body. Used to raise top-level pre-defined errors that happen outside the controller.""" if error in _reserved_errors: err = _reserved_errors[error] return Response(body=json.dumps(dict(jsonrpc=JSONRPC_VERSION, id=req_id, error=err.as_dict()))) class JSONRPCController(WSGIController): """ A WSGI-speaking JSON-RPC 2.0 controller class See the specification: ``. Many parts of this controller are modelled after XMLRPCController from Pylons 0.9.7 Valid controller return values should be json-serializable objects. Sub-classes should catch their exceptions and raise JSONRPCError if they want to pass meaningful errors to the client. Unhandled errors should be caught and return JSONRPC_INTERNAL_ERROR to the client. Parts of the specification not supported (yet): - Notifications - Batch """ def _get_method_args(self): """Return `self._rpc_args` to dispatched controller method chosen by __call__""" return self._rpc_args def __call__(self, environ, start_response): """Parse the request body as JSON, look up the method on the controller and if it exists, dispatch to it. """ length = 0 if 'CONTENT_LENGTH' not in environ: log.debug("No Content-Length") abort(411) else: if environ['CONTENT_LENGTH'] == '': abort(411) length = int(environ['CONTENT_LENGTH']) log.debug('Content-Length: %s', length) if length == 0: log.debug("Content-Length is 0") abort(411) raw_body = environ['wsgi.input'].read(length) json_body = json.loads(urllib.unquote_plus(raw_body)) self._req_id = json_body['id'] self._req_method = json_body['method'] self._req_params = json_body['params'] log.debug('id: %s, method: %s, params: %s', self._req_id, self._req_method, self._req_params) self._error = None try: self._func = self._find_method() except AttributeError: err = jsonrpc_error(self._req_id, 'method_not_found') return err(environ, start_response) # now that we have a method, make sure we have enough # parameters and pass off control to the controller. if not isinstance(self._req_params, dict): # JSON-RPC version 1 request. arglist = inspect.getargspec(self._func)[0][1:] if len(self._req_params) < len(arglist): err = jsonrpc_error(self._req_id, 'invalid_params') return err(environ, start_response) else: kargs = dict(zip(arglist, self._req_params)) else: # JSON-RPC version 2 request. Params may be default, and # are already a dict, so skip the parameter length check here. kargs = self._req_params # XX Fix this namespace clash. One cannot use names below as # method argument names as this stands! kargs['action'], kargs['environ'] = self._req_method, environ kargs['start_response'] = start_response self._rpc_args = kargs status = [] headers = [] exc_info = [] def change_content(new_status, new_headers, new_exc_info=None): status.append(new_status) headers.extend(new_headers) exc_info.append(new_exc_info) output = WSGIController.__call__(self, environ, change_content) output = list(output) headers.append(('Content-Length', str(len(output[0])))) replace_header(headers, 'Content-Type', 'application/json') start_response(status[0], headers, exc_info[0]) return output def _dispatch_call(self): """Implement dispatch interface specified by WSGIController""" try: raw_response = self._inspect_call(self._func) except JSONRPCError, e: self._error = e.as_dict() except TypeError, e: # Insufficient args in an arguments dict v2 call. if 'takes at least' in str(e): err = _reserved_errors['invalid_params'] self._error = err.as_dict() else: raise except Exception, e: log.debug('Encountered unhandled exception: %s', repr(e)) err = _reserved_errors['internal_error'] self._error = err.as_dict() response = dict(jsonrpc=JSONRPC_VERSION, id=self._req_id) if self._error is not None: response['error'] = self._error else: response['result'] = raw_response try: return json.dumps(response) except TypeError, e: log.debug('Error encoding response: %s', e) err = _reserved_errors['internal_error'] return json.dumps(dict( jsonrpc=JSONRPC_VERSION, id=self._req_id, error=err.as_dict())) def _find_method(self): """Return method named by `self._req_method` in controller if able""" log.debug('Trying to find JSON-RPC method: %s', self._req_method) if self._req_method.startswith('_'): raise AttributeError("Method not allowed") try: func = getattr(self, self._req_method, None) except UnicodeEncodeError: # XMLRPCController catches this, not sure why. raise AttributeError("Problem decoding unicode in requested " "method name.") if isinstance(func, types.MethodType): return func else: raise AttributeError("No such method: %s" % self._req_method) ================================================ FILE: pylons/controllers/util.py ================================================ """Utility functions and classes available for use by Controllers Pylons subclasses the `WebOb `_ :class:`webob.Request` and :class:`webob.Response` classes to provide backwards compatible functions for earlier versions of Pylons as well as add a few helper functions to assist with signed cookies. For reference use, refer to the :class:`Request` and :class:`Response` below. Functions available: :func:`abort`, :func:`forward`, :func:`etag_cache`, :func:`mimetype` and :func:`redirect` """ import base64 import binascii import hmac import logging import re try: import cPickle as pickle except ImportError: import pickle try: from hashlib import sha1 except ImportError: import sha as sha1 from webob import BaseRequest as WebObRequest from webob import Response as WebObResponse from webob.exc import status_map import pylons __all__ = ['abort', 'etag_cache', 'redirect', 'Request', 'Response'] log = logging.getLogger(__name__) IF_NONE_MATCH = re.compile('(?:W/)?(?:"([^"]*)",?\s*)') class Request(WebObRequest): """WebOb Request subclass The WebOb :class:`webob.Request` has no charset, or other defaults. This subclass adds defaults, along with several methods for backwards compatibility with paste.wsgiwrappers.WSGIRequest. """ def determine_browser_charset(self): """Legacy method to return the :attr:`webob.Request.accept_charset`""" return self.accept_charset def languages(self): # And we now have the old best_matches code that webob ditched! al = self.accept_language try: items = [i for i, q in sorted(al._parsed, key=lambda iq: -iq[1])] for index, item in enumerate(items): if al._match(item, self.language): items[index:] = [self.language] break else: items.append(self.language) return items except AttributeError: # If its a NilAccept, there won't be a _parsed attribute # Return the best match instead return [self.accept_language.best_match(self.language)] languages = property(languages) def match_accept(self, mimetypes): return self.accept.first_match(mimetypes) def signed_cookie(self, name, secret): """Extract a signed cookie of ``name`` from the request The cookie is expected to have been created with ``Response.signed_cookie``, and the ``secret`` should be the same as the one used to sign it. Any failure in the signature of the data will result in None being returned. """ cookie = self.str_cookies.get(name) if not cookie: return None try: input_sig, pickled = cookie[:40], base64.standard_b64decode(cookie[40:]) except binascii.Error: # Badly formed data can make base64 die return None sig = hmac.new(secret, pickled, sha1).hexdigest() # Avoid timing attacks invalid_bits = 0 if len(sig) != len(input_sig): return None for a, b in zip(sig, input_sig): invalid_bits += a != b if invalid_bits: return None else: return pickle.loads(pickled) class Response(WebObResponse): """WebOb Response subclass The WebOb Response has no default content type, or error defaults. This subclass adds defaults, along with several methods for backwards compatibility with paste.wsgiwrappers.WSGIResponse. """ content = WebObResponse.body def determine_charset(self): return self.charset def has_header(self, header): return header in self.headers def get_content(self): return self.body def wsgi_response(self): return self.status, self.headers, self.body def signed_cookie(self, name, data, secret=None, **kwargs): """Save a signed cookie with ``secret`` signature Saves a signed cookie of the pickled data. All other keyword arguments that ``WebOb.set_cookie`` accepts are usable and passed to the WebOb set_cookie method after creating the signed cookie value. """ pickled = pickle.dumps(data, pickle.HIGHEST_PROTOCOL) sig = hmac.new(secret, pickled, sha1).hexdigest() self.set_cookie(name, sig + base64.standard_b64encode(pickled), **kwargs) def etag_cache(key=None): """Use the HTTP Entity Tag cache for Browser side caching If a "If-None-Match" header is found, and equivilant to ``key``, then a ``304`` HTTP message will be returned with the ETag to tell the browser that it should use its current cache of the page. Otherwise, the ETag header will be added to the response headers. Suggested use is within a Controller Action like so: .. code-block:: python import pylons class YourController(BaseController): def index(self): etag_cache(key=1) return render('/splash.mako') .. note:: This works because etag_cache will raise an HTTPNotModified exception if the ETag received matches the key provided. """ if_none_matches = IF_NONE_MATCH.findall( pylons.request.environ.get('HTTP_IF_NONE_MATCH', '')) response = pylons.response._current_obj() response.headers['ETag'] = '"%s"' % key if str(key) in if_none_matches: log.debug("ETag match, returning 304 HTTP Not Modified Response") response.headers.pop('Content-Type', None) response.headers.pop('Cache-Control', None) response.headers.pop('Pragma', None) raise status_map[304]() else: log.debug("ETag didn't match, returning response object") def forward(wsgi_app): """Forward the request to a WSGI application. Returns its response. .. code-block:: python return forward(FileApp('filename')) """ environ = pylons.request.environ controller = environ.get('pylons.controller') if not controller or not hasattr(controller, 'start_response'): raise RuntimeError("Unable to forward: environ['pylons.controller'] " "is not a valid Pylons controller") return wsgi_app(environ, controller.start_response) def abort(status_code=None, detail="", headers=None, comment=None): """Aborts the request immediately by returning an HTTP exception In the event that the status_code is a 300 series error, the detail attribute will be used as the Location header should one not be specified in the headers attribute. """ exc = status_map[status_code](detail=detail, headers=headers, comment=comment) log.debug("Aborting request, status: %s, detail: %r, headers: %r, " "comment: %r", status_code, detail, headers, comment) raise exc def redirect(url, code=302): """Raises a redirect exception to the specified URL Optionally, a code variable may be passed with the status code of the redirect, ie:: redirect(url(controller='home', action='index'), code=303) """ log.debug("Generating %s redirect" % code) exc = status_map[code] raise exc(location=url.encode('utf-8')) ================================================ FILE: pylons/controllers/xmlrpc.py ================================================ """The base WSGI XMLRPCController""" import inspect import logging import types import xmlrpclib from paste.response import replace_header from pylons.controllers import WSGIController from pylons.controllers.util import abort, Response __all__ = ['XMLRPCController'] log = logging.getLogger(__name__) XMLRPC_MAPPING = ((basestring, 'string'), (list, 'array'), (bool, 'boolean'), (int, 'int'), (float, 'double'), (dict, 'struct'), (xmlrpclib.DateTime, 'dateTime.iso8601'), (xmlrpclib.Binary, 'base64')) def xmlrpc_sig(args): """Returns a list of the function signature in string format based on a tuple provided by xmlrpclib.""" signature = [] for param in args: for type, xml_name in XMLRPC_MAPPING: if isinstance(param, type): signature.append(xml_name) break return signature def xmlrpc_fault(code, message): """Convienence method to return a Pylons response XMLRPC Fault""" fault = xmlrpclib.Fault(code, message) return Response(body=xmlrpclib.dumps(fault, methodresponse=True)) class XMLRPCController(WSGIController): """XML-RPC Controller that speaks WSGI This controller handles XML-RPC responses and complies with the `XML-RPC Specification `_ as well as the `XML-RPC Introspection `_ specification. By default, methods with names containing a dot are translated to use an underscore. For example, the `system.methodHelp` is handled by the method :meth:`system_methodHelp`. Methods in the XML-RPC controller will be called with the method given in the XMLRPC body. Methods may be annotated with a signature attribute to declare the valid arguments and return types. For example:: class MyXML(XMLRPCController): def userstatus(self): return 'basic string' userstatus.signature = [ ['string'] ] def userinfo(self, username, age=None): user = LookUpUser(username) response = {'username':user.name} if age and age > 10: response['age'] = age return response userinfo.signature = [['struct', 'string'], ['struct', 'string', 'int']] Since XML-RPC methods can take different sets of data, each set of valid arguments is its own list. The first value in the list is the type of the return argument. The rest of the arguments are the types of the data that must be passed in. In the last method in the example above, since the method can optionally take an integer value both sets of valid parameter lists should be provided. Valid types that can be checked in the signature and their corresponding Python types:: 'string' - str 'array' - list 'boolean' - bool 'int' - int 'double' - float 'struct' - dict 'dateTime.iso8601' - xmlrpclib.DateTime 'base64' - xmlrpclib.Binary The class variable ``allow_none`` is passed to xmlrpclib.dumps; enabling it allows translating ``None`` to XML (an extension to the XML-RPC specification) .. note:: Requiring a signature is optional. """ allow_none = False max_body_length = 4194304 def _get_method_args(self): return self.rpc_kargs def __call__(self, environ, start_response): """Parse an XMLRPC body for the method, and call it with the appropriate arguments""" # Pull out the length, return an error if there is no valid # length or if the length is larger than the max_body_length. log_debug = self._pylons_log_debug length = environ.get('CONTENT_LENGTH') if length: length = int(length) else: # No valid Content-Length header found if log_debug: log.debug("No Content-Length found, returning 411 error") abort(411) if length > self.max_body_length or length == 0: if log_debug: log.debug("Content-Length larger than max body length. Max: " "%s, Sent: %s. Returning 413 error", self.max_body_length, length) abort(413, "XML body too large") body = environ['wsgi.input'].read(int(environ['CONTENT_LENGTH'])) rpc_args, orig_method = xmlrpclib.loads(body) method = self._find_method_name(orig_method) func = self._find_method(method) if not func: if log_debug: log.debug("Method: %r not found, returning xmlrpc fault", method) return xmlrpc_fault(0, "No such method name %r" % method)(environ, start_response) # Signature checking for params if hasattr(func, 'signature'): if log_debug: log.debug("Checking XMLRPC argument signature") valid_args = False params = xmlrpc_sig(rpc_args) for sig in func.signature: # Next sig if we don't have the same amount of args if len(sig) - 1 != len(rpc_args): continue # If the params match, we're valid if params == sig[1:]: valid_args = True break if not valid_args: if log_debug: log.debug("Bad argument signature recieved, returning " "xmlrpc fault") msg = ("Incorrect argument signature. %r recieved does not " "match %r signature for method %r" % \ (params, func.signature, orig_method)) return xmlrpc_fault(0, msg)(environ, start_response) # Change the arg list into a keyword dict based off the arg # names in the functions definition arglist = inspect.getargspec(func)[0][1:] kargs = dict(zip(arglist, rpc_args)) kargs['action'], kargs['environ'] = method, environ kargs['start_response'] = start_response self.rpc_kargs = kargs self._func = func # Now that we know the method is valid, and the args are valid, # we can dispatch control to the default WSGIController status = [] headers = [] exc_info = [] def change_content(new_status, new_headers, new_exc_info=None): status.append(new_status) headers.extend(new_headers) exc_info.append(new_exc_info) output = WSGIController.__call__(self, environ, change_content) output = list(output) headers.append(('Content-Length', str(len(output[0])))) replace_header(headers, 'Content-Type', 'text/xml') start_response(status[0], headers, exc_info[0]) return output def _dispatch_call(self): """Dispatch the call to the function chosen by __call__""" raw_response = self._inspect_call(self._func) if not isinstance(raw_response, xmlrpclib.Fault): raw_response = (raw_response,) response = xmlrpclib.dumps(raw_response, methodresponse=True, allow_none=self.allow_none) return response def _find_method(self, name): """Locate a method in the controller by the specified name and return it""" # Keep private methods private if name.startswith('_'): if self._pylons_log_debug: log.debug("Action starts with _, private action not allowed") return if self._pylons_log_debug: log.debug("Looking for XMLRPC method: %r", name) try: func = getattr(self, name, None) except UnicodeEncodeError: return if isinstance(func, types.MethodType): return func def _find_method_name(self, name): """Locate a method in the controller by the appropriate name By default, this translates method names like 'system.methodHelp' into 'system_methodHelp'. """ return name.replace('.', '_') def _publish_method_name(self, name): """Translate an internal method name to a publicly viewable one By default, this translates internal method names like 'blog_view' into 'blog.view'. """ return name.replace('_', '.') def system_listMethods(self): """Returns a list of XML-RPC methods for this XML-RPC resource""" methods = [] for method in dir(self): meth = getattr(self, method) if not method.startswith('_') and isinstance(meth, types.MethodType): methods.append(self._publish_method_name(method)) return methods system_listMethods.signature = [['array']] def system_methodSignature(self, name): """Returns an array of array's for the valid signatures for a method. The first value of each array is the return value of the method. The result is an array to indicate multiple signatures a method may be capable of. """ method = self._find_method(self._find_method_name(name)) if method: return getattr(method, 'signature', '') else: return xmlrpclib.Fault(0, 'No such method name') system_methodSignature.signature = [['array', 'string'], ['string', 'string']] def system_methodHelp(self, name): """Returns the documentation for a method""" method = self._find_method(self._find_method_name(name)) if method: help = MethodHelp.getdoc(method) sig = getattr(method, 'signature', None) if sig: help += "\n\nMethod signature: %s" % sig return help return xmlrpclib.Fault(0, "No such method name") system_methodHelp.signature = [['string', 'string']] class MethodHelp(object): """Wrapper for formatting doc strings from XMLRPCController methods""" def __init__(self, doc): self.__doc__ = doc def getdoc(method): """Return a formatted doc string, via inspect.getdoc, from the specified XMLRPCController method The method's help attribute is used if it exists, otherwise the method's doc string is used. """ help = getattr(method, 'help', None) if help is None: help = method.__doc__ doc = inspect.getdoc(MethodHelp(help)) if doc is None: return '' return doc getdoc = staticmethod(getdoc) ================================================ FILE: pylons/decorators/__init__.py ================================================ """Pylons Decorators Common decorators intended for use in controllers. Additional decorators for use with controllers are in the :mod:`~pylons.decorators.cache`, :mod:`~pylons.decorators.rest` and :mod:`~pylons.decorators.secure` modules. """ import logging import warnings import formencode import simplejson from decorator import decorator from formencode import api, htmlfill, variabledecode from pylons.decorators.util import get_pylons from pylons.i18n import _ as pylons_gettext __all__ = ['jsonify', 'validate'] log = logging.getLogger(__name__) class JSONEncoder(simplejson.JSONEncoder): def default(self, obj): encoder = getattr(obj, '__json__', None) if encoder is not None: return encoder() return super(JSONEncoder, self).default(obj) @decorator def jsonify(func, *args, **kwargs): """Action decorator that formats output for JSON Given a function that will return content, this decorator will turn the result into JSON, with a content-type of 'application/json' and output it. """ pylons = get_pylons(args) pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8' data = func(*args, **kwargs) if isinstance(data, (list, tuple)): msg = "JSON responses with Array envelopes are susceptible to " \ "cross-site data leak attacks, see " \ "http://wiki.pylonshq.com/display/pylonsfaq/Warnings" warnings.warn(msg, Warning, 2) log.warning(msg) log.debug("Returning JSON wrapped action output") return simplejson.dumps(data, cls=JSONEncoder, encoding='utf-8') def validate(schema=None, validators=None, form=None, variable_decode=False, dict_char='.', list_char='-', post_only=True, state=None, on_get=False, **htmlfill_kwargs): """Validate input either for a FormEncode schema, or individual validators Given a form schema or dict of validators, validate will attempt to validate the schema or validator list. If validation was successful, the valid result dict will be saved as ``self.form_result``. Otherwise, the action will be re-run as if it was a GET, and the output will be filled by FormEncode's htmlfill to fill in the form field errors. ``schema`` Refers to a FormEncode Schema object to use during validation. ``form`` Method used to display the form, which will be used to get the HTML representation of the form for error filling. ``variable_decode`` Boolean to indicate whether FormEncode's variable decode function should be run on the form input before validation. ``dict_char`` Passed through to FormEncode. Toggles the form field naming scheme used to determine what is used to represent a dict. This option is only applicable when used with variable_decode=True. ``list_char`` Passed through to FormEncode. Toggles the form field naming scheme used to determine what is used to represent a list. This option is only applicable when used with variable_decode=True. ``post_only`` Boolean that indicates whether or not GET (query) variables should be included during validation. .. warning:: ``post_only`` applies to *where* the arguments to be validated come from. It does *not* restrict the form to only working with post, merely only checking POST vars. ``state`` Passed through to FormEncode for use in validators that utilize a state object. ``on_get`` Whether to validate on GET requests. By default only POST requests are validated. Example:: class SomeController(BaseController): def create(self, id): return render('/myform.mako') @validate(schema=model.forms.myshema(), form='create') def update(self, id): # Do something with self.form_result pass """ if state is None: state = PylonsFormEncodeState def wrapper(func, self, *args, **kwargs): """Decorator Wrapper function""" request = self._py_object.request errors = {} # Skip the validation if on_get is False and its a GET if not on_get and request.environ['REQUEST_METHOD'] == 'GET': return func(self, *args, **kwargs) # If they want post args only, use just the post args if post_only: params = request.POST else: params = request.params params = params.mixed() if variable_decode: log.debug("Running variable_decode on params") decoded = variabledecode.variable_decode(params, dict_char, list_char) else: decoded = params if schema: log.debug("Validating against a schema") try: self.form_result = schema.to_python(decoded, state) except formencode.Invalid, e: errors = e.unpack_errors(variable_decode, dict_char, list_char) if validators: log.debug("Validating against provided validators") if isinstance(validators, dict): if not hasattr(self, 'form_result'): self.form_result = {} for field, validator in validators.iteritems(): try: self.form_result[field] = \ validator.to_python(decoded.get(field), state) except formencode.Invalid, error: errors[field] = error if errors: log.debug("Errors found in validation, parsing form with htmlfill " "for errors") request.environ['REQUEST_METHOD'] = 'GET' self._py_object.tmpl_context.form_errors = errors # If there's no form supplied, just continue with the current # function call. if not form: return func(self, *args, **kwargs) request.environ['pylons.routes_dict']['action'] = form response = self._dispatch_call() # If the form_content is an exception response, return it if hasattr(response, '_exception'): return response htmlfill_kwargs2 = htmlfill_kwargs.copy() htmlfill_kwargs2.setdefault('encoding', request.charset) return htmlfill.render(response, defaults=params, errors=errors, **htmlfill_kwargs2) return func(self, *args, **kwargs) return decorator(wrapper) def pylons_formencode_gettext(value): """Translates a string ``value`` using pylons gettext first and if that fails, formencode gettext. This allows to "merge" localized error messages from built-in FormEncode's validators with application-specific validators. """ trans = pylons_gettext(value) if trans == value: # translation failed, try formencode trans = api._stdtrans(value) return trans class PylonsFormEncodeState(object): """A ``state`` for FormEncode validate API that includes smart ``_`` hook. The FormEncode library used by validate() decorator has some provision for localizing error messages. In particular, it looks for attribute ``_`` in the application-specific state object that gets passed to every ``.to_python()`` call. If it is found, the ``_`` is assumed to be a gettext-like function and is called to localize error messages. One complication is that FormEncode ships with localized error messages for standard validators so the user may want to re-use them instead of gathering and translating everything from scratch. To allow this, we pass as ``_`` a function which looks up translation both in application and formencode message catalogs. """ _ = staticmethod(pylons_formencode_gettext) ================================================ FILE: pylons/decorators/cache.py ================================================ """Caching decorator""" import inspect import logging import time from decorator import decorator from paste.deploy.converters import asbool from pylons.decorators.util import get_pylons log = logging.getLogger(__name__) def beaker_cache(key="cache_default", expire="never", type=None, query_args=False, cache_headers=('content-type', 'content-length'), invalidate_on_startup=False, cache_response=True, **b_kwargs): """Cache decorator utilizing Beaker. Caches action or other function that returns a pickle-able object as a result. Optional arguments: ``key`` None - No variable key, uses function name as key "cache_default" - Uses all function arguments as the key string - Use kwargs[key] as key list - Use [kwargs[k] for k in list] as key ``expire`` Time in seconds before cache expires, or the string "never". Defaults to "never" ``type`` Type of cache to use: dbm, memory, file, memcached, or None for Beaker's default ``query_args`` Uses the query arguments as the key, defaults to False ``cache_headers`` A tuple of header names indicating response headers that will also be cached. ``invalidate_on_startup`` If True, the cache will be invalidated each time the application starts or is restarted. ``cache_response`` Determines whether the response at the time beaker_cache is used should be cached or not, defaults to True. .. note:: When cache_response is set to False, the cache_headers argument is ignored as none of the response is cached. If cache_enabled is set to False in the .ini file, then cache is disabled globally. """ if invalidate_on_startup: starttime = time.time() else: starttime = None cache_headers = set(cache_headers) def wrapper(func, *args, **kwargs): """Decorator wrapper""" pylons = get_pylons(args) log.debug("Wrapped with key: %s, expire: %s, type: %s, query_args: %s", key, expire, type, query_args) enabled = pylons.config.get("cache_enabled", "True") if not asbool(enabled): log.debug("Caching disabled, skipping cache lookup") return func(*args, **kwargs) if key: key_dict = kwargs.copy() key_dict.update(_make_dict_from_args(func, args)) if query_args: key_dict.update(pylons.request.GET.mixed()) if key != "cache_default": if isinstance(key, list): key_dict = dict((k, key_dict[k]) for k in key) else: key_dict = {key: key_dict[key]} else: key_dict = None self = None if args: self = args[0] namespace, cache_key = create_cache_key(func, key_dict, self) if type: b_kwargs['type'] = type cache_obj = getattr(pylons.app_globals, 'cache', None) if not cache_obj: cache_obj = getattr(pylons, 'cache', None) if not cache_obj: raise Exception('No CacheMiddleware or cache object on ' ' app_globals was found') my_cache = cache_obj.get_cache(namespace, **b_kwargs) if expire == "never": cache_expire = None else: cache_expire = expire def create_func(): log.debug("Creating new cache copy with key: %s, type: %s", cache_key, type) result = func(*args, **kwargs) glob_response = pylons.response headers = glob_response.headerlist status = glob_response.status full_response = dict(headers=headers, status=status, cookies=None, content=result) return full_response response = my_cache.get_value(cache_key, createfunc=create_func, expiretime=cache_expire, starttime=starttime) if cache_response: glob_response = pylons.response glob_response.headerlist = [header for header in response['headers'] if header[0].lower() in cache_headers] glob_response.status = response['status'] return response['content'] return decorator(wrapper) def create_cache_key(func, key_dict=None, self=None): """Get a cache namespace and key used by the beaker_cache decorator. Example:: from pylons import cache from pylons.decorators.cache import create_cache_key namespace, key = create_cache_key(MyController.some_method) cache.get_cache(namespace).remove(key) """ kls = None if hasattr(func, 'im_func'): kls = func.im_class func = func.im_func cache_key = func.__name__ else: cache_key = func.__name__ if key_dict: cache_key += " " + " ".join("%s=%s" % (k, v) for k, v in key_dict.iteritems()) if not kls and self: kls = getattr(self, '__class__', None) if kls: return '%s.%s' % (kls.__module__, kls.__name__), cache_key else: return func.__module__, cache_key def _make_dict_from_args(func, args): """Inspects function for name of args""" args_keys = {} for i, arg in enumerate(inspect.getargspec(func)[0]): if arg != "self": args_keys[arg] = args[i] return args_keys ================================================ FILE: pylons/decorators/rest.py ================================================ """REST decorators""" import logging from decorator import decorator from pylons.controllers.util import abort from pylons.decorators.util import get_pylons __all__ = ['dispatch_on', 'restrict'] log = logging.getLogger(__name__) def restrict(*methods): """Restricts access to the function depending on HTTP method Example: .. code-block:: python from pylons.decorators import rest class SomeController(BaseController): @rest.restrict('GET') def comment(self, id): """ def check_methods(func, *args, **kwargs): """Wrapper for restrict""" if get_pylons(args).request.method not in methods: log.debug("Method not allowed by restrict") abort(405, headers=[('Allow', ','.join(methods))]) return func(*args, **kwargs) return decorator(check_methods) def dispatch_on(**method_map): """Dispatches to alternate controller methods based on HTTP method Multiple keyword arguments should be passed, with the keyword corresponding to the HTTP method to dispatch on (DELETE, POST, GET, etc.) and the value being the function to call. The value should be a string indicating the name of the function to dispatch to. Example: .. code-block:: python from pylons.decorators import rest class SomeController(BaseController): @rest.dispatch_on(POST='create_comment') def comment(self): # Do something with the comment def create_comment(self, id): # Do something if its a post to comment """ def dispatcher(func, self, *args, **kwargs): """Wrapper for dispatch_on""" alt_method = method_map.get(get_pylons(args).request.method) if alt_method: alt_method = getattr(self, alt_method) log.debug("Dispatching to %s instead", alt_method) return self._inspect_call(alt_method, **kwargs) return func(self, *args, **kwargs) return decorator(dispatcher) ================================================ FILE: pylons/decorators/secure.py ================================================ """Security related decorators""" import logging import urlparse from decorator import decorator try: import webhelpers.html.secure_form as secure_form except ImportError: import webhelpers.pylonslib.secure_form as secure_form from pylons.controllers.util import abort, redirect from pylons.decorators.util import get_pylons __all__ = ['authenticate_form', 'https'] log = logging.getLogger(__name__) csrf_detected_message = ( "Cross-site request forgery detected, request denied. See " "http://en.wikipedia.org/wiki/Cross-site_request_forgery for more " "information.") def authenticated_form(params): submitted_token = params.get(secure_form.token_key) return submitted_token is not None and \ submitted_token == secure_form.authentication_token() @decorator def authenticate_form(func, *args, **kwargs): """Decorator for authenticating a form This decorator uses an authorization token stored in the client's session for prevention of certain Cross-site request forgery (CSRF) attacks (See http://en.wikipedia.org/wiki/Cross-site_request_forgery for more information). For use with the ``webhelpers.html.secure_form`` helper functions. """ request = get_pylons(args).request if authenticated_form(request.params): try: del request.POST[secure_form.token_key] except KeyError: del request.GET[secure_form.token_key] return func(*args, **kwargs) else: log.warn('Cross-site request forgery detected, request denied: %r ' 'REMOTE_ADDR: %s' % (request, request.remote_addr)) abort(403, detail=csrf_detected_message) def https(url_or_callable=None): """Decorator to redirect to the SSL version of a page if not currently using HTTPS. Apply this decorator to controller methods (actions). Takes a url argument: either a string url, or a callable returning a string url. The callable will be called with no arguments when the decorated method is called. The url's scheme will be rewritten to https if necessary. Non-HTTPS POST requests are aborted (405 response code) by this decorator. Example: .. code-block:: python # redirect to HTTPS /pylons @https('/pylons') def index(self): do_secure() # redirect to HTTPS /auth/login, delaying the url() call until # later (as the url object may not be functional when the # decorator/method are defined) @https(lambda: url(controller='auth', action='login')) def login(self): do_secure() # redirect to HTTPS version of myself @https() def get(self): do_secure() """ def wrapper(func, *args, **kwargs): """Decorator Wrapper function""" request = get_pylons(args).request if request.scheme.lower() == 'https': return func(*args, **kwargs) if request.method.upper() == 'POST': # don't allow POSTs (raises an exception) abort(405, headers=[('Allow', 'GET')]) if url_or_callable is None: url = request.url elif callable(url_or_callable): url = url_or_callable() else: url = url_or_callable # Ensure an https scheme, which also needs a host parts = urlparse.urlparse(url) url = urlparse.urlunparse(('https', parts[1] or request.host) + parts[2:]) log.debug('Redirecting non-https request: %s to: %s', request.path_info, url) redirect(url) return decorator(wrapper) ================================================ FILE: pylons/decorators/util.py ================================================ """Decorator internal utilities""" import pylons from pylons.controllers import WSGIController def get_pylons(decorator_args): """Return the `pylons` object: either the :mod`~pylons` module or the :attr:`~WSGIController._py_object` equivalent, searching a decorator's *args for the latter :attr:`~WSGIController._py_object` is more efficient as it provides direct access to the Pylons global variables. """ if decorator_args: controller = decorator_args[0] if isinstance(controller, WSGIController): return controller._py_object return pylons ================================================ FILE: pylons/docs/en/.gitignore ================================================ _build _themes ================================================ FILE: pylons/docs/en/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html web htmlhelp latex changes linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " web to make files usable by Sphinx.web" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf _build/* html: mkdir -p _build/html _build/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html @echo @echo "Build finished. The HTML pages are in _build/html." web: mkdir -p _build/web _build/doctrees $(SPHINXBUILD) -b web $(ALLSPHINXOPTS) _build/web @echo @echo "Build finished; now you can run" @echo " python -m sphinx.web _build/web" @echo "to start the server." htmlhelp: mkdir -p _build/htmlhelp _build/doctrees $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in _build/htmlhelp." latex: mkdir -p _build/latex _build/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex @echo @echo "Build finished; the LaTeX files are in _build/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p _build/changes _build/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes @echo @echo "The overview file is in _build/changes." linkcheck: mkdir -p _build/linkcheck _build/doctrees $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in _build/linkcheck/output.txt." ================================================ FILE: pylons/docs/en/_oldstatic/akhet.css ================================================ div.body h2 { font-size: 160%; background: url(akhettransaqua.png) 10px 3px no-repeat; padding-left: 2.66em;} pre { background: #efc url(background.png) 0 0 repeat; } ================================================ FILE: pylons/docs/en/_oldstatic/default.css ================================================ /** * Sphinx Doc Design */ body { font-family: sans-serif; font-size: 100%; background-color: #11303d; color: #000; margin: 0; padding: 0; } /* :::: LAYOUT :::: */ div.document { background-color: #1c4e63; } div.documentwrapper { float: left; width: 100%; } div.bodywrapper { margin: 0 0 0 230px; } div.body { background-color: white; padding: 0 20px 30px 20px; } div.sphinxsidebarwrapper { padding: 10px 5px 0 10px; } div.sphinxsidebar { float: left; width: 230px; margin-left: -100%; font-size: 90%; } div.clearer { clear: both; } div.footer { color: #fff; width: 100%; padding: 9px 0 9px 0; text-align: center; font-size: 75%; } div.footer a { color: #fff; text-decoration: underline; } div.related { background-color: #133f52; color: #fff; width: 100%; height: 30px; line-height: 30px; font-size: 90%; } div.related h3 { display: none; } div.related ul { margin: 0; padding: 0 0 0 10px; list-style: none; } div.related li { display: inline; } div.related li.right { float: right; margin-right: 5px; } div.related a { color: white; } /* ::: TOC :::: */ div.sphinxsidebar h3 { font-family: 'Trebuchet MS', sans-serif; color: white; font-size: 1.4em; font-weight: normal; margin: 0; padding: 0; } div.sphinxsidebar h4 { font-family: 'Trebuchet MS', sans-serif; color: white; font-size: 1.3em; font-weight: normal; margin: 5px 0 0 0; padding: 0; } div.sphinxsidebar p { color: white; } div.sphinxsidebar p.topless { margin: 5px 10px 10px 10px; } div.sphinxsidebar ul { margin: 10px; padding: 0; list-style: none; color: white; } div.sphinxsidebar ul ul, div.sphinxsidebar ul.want-points { margin-left: 20px; list-style: square; } div.sphinxsidebar ul ul { margin-top: 0; margin-bottom: 0; } div.sphinxsidebar a { color: #98dbcc; } div.sphinxsidebar form { margin-top: 10px; } div.sphinxsidebar input { border: 1px solid #98dbcc; font-family: sans-serif; font-size: 1em; } /* :::: MODULE CLOUD :::: */ div.modulecloud { margin: -5px 10px 5px 10px; padding: 10px; line-height: 160%; border: 1px solid #cbe7e5; background-color: #f2fbfd; } div.modulecloud a { padding: 0 5px 0 5px; } /* :::: SEARCH :::: */ ul.search { margin: 10px 0 0 20px; padding: 0; } ul.search li { padding: 5px 0 5px 20px; background-image: url(file.png); background-repeat: no-repeat; background-position: 0 7px; } ul.search li a { font-weight: bold; } ul.search li div.context { color: #888; margin: 2px 0 0 30px; text-align: left; } ul.keywordmatches li.goodmatch a { font-weight: bold; } /* :::: COMMON FORM STYLES :::: */ div.actions { padding: 5px 10px 5px 10px; border-top: 1px solid #cbe7e5; border-bottom: 1px solid #cbe7e5; background-color: #e0f6f4; } form dl { color: #333; } form dt { clear: both; float: left; min-width: 110px; margin-right: 10px; padding-top: 2px; } input#homepage { display: none; } div.error { margin: 5px 20px 0 0; padding: 5px; border: 1px solid #d00; font-weight: bold; } /* :::: INLINE COMMENTS :::: */ div.inlinecomments { position: absolute; right: 20px; } div.inlinecomments a.bubble { display: block; float: right; background-image: url(style/comment.png); background-repeat: no-repeat; width: 25px; height: 25px; text-align: center; padding-top: 3px; font-size: 0.9em; line-height: 14px; font-weight: bold; color: black; } div.inlinecomments a.bubble span { display: none; } div.inlinecomments a.emptybubble { background-image: url(style/nocomment.png); } div.inlinecomments a.bubble:hover { background-image: url(style/hovercomment.png); text-decoration: none; color: #3ca0a4; } div.inlinecomments div.comments { float: right; margin: 25px 5px 0 0; max-width: 50em; min-width: 30em; border: 1px solid #2eabb0; background-color: #f2fbfd; z-index: 150; } div#comments { border: 1px solid #2eabb0; margin-top: 20px; } div#comments div.nocomments { padding: 10px; font-weight: bold; } div.inlinecomments div.comments h3, div#comments h3 { margin: 0; padding: 0; background-color: #2eabb0; color: white; border: none; padding: 3px; } div.inlinecomments div.comments div.actions { padding: 4px; margin: 0; border-top: none; } div#comments div.comment { margin: 10px; border: 1px solid #2eabb0; } div.inlinecomments div.comment h4, div.commentwindow div.comment h4, div#comments div.comment h4 { margin: 10px 0 0 0; background-color: #2eabb0; color: white; border: none; padding: 1px 4px 1px 4px; } div#comments div.comment h4 { margin: 0; } div#comments div.comment h4 a { color: #d5f4f4; } div.inlinecomments div.comment div.text, div.commentwindow div.comment div.text, div#comments div.comment div.text { margin: -5px 0 -5px 0; padding: 0 10px 0 10px; } div.inlinecomments div.comment div.meta, div.commentwindow div.comment div.meta, div#comments div.comment div.meta { text-align: right; padding: 2px 10px 2px 0; font-size: 95%; color: #538893; border-top: 1px solid #cbe7e5; background-color: #e0f6f4; } div.commentwindow { position: absolute; width: 500px; border: 1px solid #cbe7e5; background-color: #f2fbfd; display: none; z-index: 130; } div.commentwindow h3 { margin: 0; background-color: #2eabb0; color: white; border: none; padding: 5px; font-size: 1.5em; cursor: pointer; } div.commentwindow div.actions { margin: 10px -10px 0 -10px; padding: 4px 10px 4px 10px; color: #538893; } div.commentwindow div.actions input { border: 1px solid #2eabb0; background-color: white; color: #135355; cursor: pointer; } div.commentwindow div.form { padding: 0 10px 0 10px; } div.commentwindow div.form input, div.commentwindow div.form textarea { border: 1px solid #3c9ea2; background-color: white; color: black; } div.commentwindow div.error { margin: 10px 5px 10px 5px; background-color: #fbe5dc; display: none; } div.commentwindow div.form textarea { width: 99%; } div.commentwindow div.preview { margin: 10px 0 10px 0; background-color: #70d0d4; padding: 0 1px 1px 25px; } div.commentwindow div.preview h4 { margin: 0 0 -5px -20px; padding: 4px 0 0 4px; color: white; font-size: 1.3em; } div.commentwindow div.preview div.comment { background-color: #f2fbfd; } div.commentwindow div.preview div.comment h4 { margin: 10px 0 0 0!important; padding: 1px 4px 1px 4px!important; font-size: 1.2em; } /* :::: SUGGEST CHANGES :::: */ div#suggest-changes-box input, div#suggest-changes-box textarea { border: 1px solid #ccc; background-color: white; color: black; } div#suggest-changes-box textarea { width: 99%; height: 400px; } /* :::: PREVIEW :::: */ div.preview { background-image: url(style/preview.png); padding: 0 20px 20px 20px; margin-bottom: 30px; } /* :::: INDEX PAGE :::: */ table.contentstable { width: 90%; } table.contentstable p.biglink { line-height: 150%; } a.biglink { font-size: 1.3em; } span.linkdescr { font-style: italic; padding-top: 5px; font-size: 90%; } /* :::: INDEX STYLES :::: */ table.indextable td { text-align: left; vertical-align: top; } table.indextable dl, table.indextable dd { margin-top: 0; margin-bottom: 0; } table.indextable tr.pcap { height: 10px; } table.indextable tr.cap { margin-top: 10px; background-color: #f2f2f2; } img.toggler { margin-right: 3px; margin-top: 3px; cursor: pointer; } form.pfform { margin: 10px 0 20px 0; } /* :::: GLOBAL STYLES :::: */ .docwarning { background-color: #ffe4e4; padding: 10px; margin: 0 -20px 0 -20px; border-bottom: 1px solid #f66; } p.subhead { font-weight: bold; margin-top: 20px; } a { color: #355f7c; text-decoration: none; } a:hover { text-decoration: underline; } div.body h1, div.body h2, div.body h3, div.body h4, div.body h5, div.body h6 { font-family: 'Trebuchet MS', sans-serif; background-color: #f2f2f2; font-weight: normal; color: #20435c; border-bottom: 1px solid #ccc; margin: 20px -20px 10px -20px; padding: 3px 0 3px 10px; } div.body h1 { margin-top: 0; font-size: 200%; } div.body h2 { font-size: 160%; } div.body h3 { font-size: 140%; } div.body h4 { font-size: 120%; } div.body h5 { font-size: 110%; } div.body h6 { font-size: 100%; } a.headerlink { color: #c60f0f; font-size: 0.8em; padding: 0 4px 0 4px; text-decoration: none; visibility: hidden; } h1:hover > a.headerlink, h2:hover > a.headerlink, h3:hover > a.headerlink, h4:hover > a.headerlink, h5:hover > a.headerlink, h6:hover > a.headerlink, dt:hover > a.headerlink { visibility: visible; } a.headerlink:hover { background-color: #c60f0f; color: white; } div.body p, div.body dd, div.body li { text-align: justify; line-height: 130%; } div.body p.caption { text-align: inherit; } div.body td { text-align: left; } ul.fakelist { list-style: none; margin: 10px 0 10px 20px; padding: 0; } .field-list ul { padding-left: 1em; } .first { margin-top: 0 !important; } /* "Footnotes" heading */ p.rubric { margin-top: 30px; font-weight: bold; } /* "Topics" */ div.topic { background-color: #eee; border: 1px solid #ccc; padding: 0 7px 0 7px; margin: 10px 0 10px 0; } p.topic-title { font-size: 1.1em; font-weight: bold; margin-top: 10px; } /* Admonitions */ div.admonition { margin-top: 10px; margin-bottom: 10px; padding: 7px; } div.admonition dt { font-weight: bold; } div.admonition dl { margin-bottom: 0; } div.admonition p { display: inline; } div.seealso { background-color: #ffc; border: 1px solid #ff6; } div.warning { background-color: #ffe4e4; border: 1px solid #f66; } div.note { background-color: #eee; border: 1px solid #ccc; } p.admonition-title { margin: 0px 10px 5px 0px; font-weight: bold; display: inline; } p.admonition-title:after { content: ":"; } div.body p.centered { text-align: center; margin-top: 25px; } table.docutils { border: 0; } table.docutils td, table.docutils th { padding: 1px 8px 1px 0; border-top: 0; border-left: 0; border-right: 0; border-bottom: 1px solid #aaa; } table.field-list td, table.field-list th { border: 0 !important; } table.footnote td, table.footnote th { border: 0 !important; } .field-list ul { margin: 0; padding-left: 1em; } .field-list p { margin: 0; } dl { margin-bottom: 15px; clear: both; } dd p { margin-top: 0px; } dd ul, dd table { margin-bottom: 10px; } dd { margin-top: 3px; margin-bottom: 10px; margin-left: 30px; } .refcount { color: #060; } dt:target, .highlight { background-color: #fbe54e; } dl.glossary dt { font-weight: bold; font-size: 1.1em; } th { text-align: left; padding-right: 5px; } pre { padding: 5px; background-color: #efc; color: #333; border: 1px solid #ac9; border-left: none; border-right: none; overflow: auto; } td.linenos pre { padding: 5px 0px; border: 0; background-color: transparent; color: #aaa; } table.highlighttable { margin-left: 0.5em; } table.highlighttable td { padding: 0 0.5em 0 0.5em; } tt { background-color: #ecf0f3; padding: 0 1px 0 1px; font-size: 0.95em; } tt.descname { background-color: transparent; font-weight: bold; font-size: 1.2em; } tt.descclassname { background-color: transparent; } tt.xref, a tt { background-color: transparent; font-weight: bold; } .footnote:target { background-color: #ffa } h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt { background-color: transparent; } .optional { font-size: 1.3em; } .versionmodified { font-style: italic; } form.comment { margin: 0; padding: 10px 30px 10px 30px; background-color: #eee; } form.comment h3 { background-color: #326591; color: white; margin: -10px -30px 10px -30px; padding: 5px; font-size: 1.4em; } form.comment input, form.comment textarea { border: 1px solid #ccc; padding: 2px; font-family: sans-serif; font-size: 100%; } form.comment input[type="text"] { width: 240px; } form.comment textarea { width: 100%; height: 200px; margin-bottom: 10px; } .system-message { background-color: #fda; padding: 5px; border: 3px solid red; } /* :::: PRINT :::: */ @media print { div.document, div.documentwrapper, div.bodywrapper { margin: 0; width : 100%; } div.sphinxsidebar, div.related, div.footer, div#comments div.new-comment-box, #top-link { display: none; } } ================================================ FILE: pylons/docs/en/_oldstatic/pygments.css ================================================ .c { color: #60a0b0; font-style: italic } /* Comment */ .err { border: 1px solid #FF0000 } /* Error */ .k { color: #007020; font-weight: bold } /* Keyword */ .o { color: #666666 } /* Operator */ .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */ .cp { color: #007020 } /* Comment.Preproc */ .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */ .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */ .gd { color: #A00000 } /* Generic.Deleted */ .ge { font-style: italic } /* Generic.Emph */ .gr { color: #FF0000 } /* Generic.Error */ .gh { color: #000080; font-weight: bold } /* Generic.Heading */ .gi { color: #00A000 } /* Generic.Inserted */ .go { color: #808080 } /* Generic.Output */ .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */ .gs { font-weight: bold } /* Generic.Strong */ .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ .gt { color: #0040D0 } /* Generic.Traceback */ .kc { color: #007020; font-weight: bold } /* Keyword.Constant */ .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */ .kp { color: #007020 } /* Keyword.Pseudo */ .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */ .kt { color: #902000 } /* Keyword.Type */ .m { color: #40a070 } /* Literal.Number */ .s { color: #4070a0 } /* Literal.String */ .na { color: #4070a0 } /* Name.Attribute */ .nb { color: #007020 } /* Name.Builtin */ .nc { color: #0e84b5; font-weight: bold } /* Name.Class */ .no { color: #60add5 } /* Name.Constant */ .nd { color: #555555; font-weight: bold } /* Name.Decorator */ .ni { color: #d55537; font-weight: bold } /* Name.Entity */ .ne { color: #007020 } /* Name.Exception */ .nf { color: #06287e } /* Name.Function */ .nl { color: #002070; font-weight: bold } /* Name.Label */ .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */ .nt { color: #062873; font-weight: bold } /* Name.Tag */ .nv { color: #bb60d5 } /* Name.Variable */ .ow { color: #007020; font-weight: bold } /* Operator.Word */ .w { color: #bbbbbb } /* Text.Whitespace */ .mf { color: #40a070 } /* Literal.Number.Float */ .mh { color: #40a070 } /* Literal.Number.Hex */ .mi { color: #40a070 } /* Literal.Number.Integer */ .mo { color: #40a070 } /* Literal.Number.Oct */ .sb { color: #4070a0 } /* Literal.String.Backtick */ .sc { color: #4070a0 } /* Literal.String.Char */ .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */ .s2 { color: #4070a0 } /* Literal.String.Double */ .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */ .sh { color: #4070a0 } /* Literal.String.Heredoc */ .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */ .sx { color: #c65d09 } /* Literal.String.Other */ .sr { color: #235388 } /* Literal.String.Regex */ .s1 { color: #4070a0 } /* Literal.String.Single */ .ss { color: #517918 } /* Literal.String.Symbol */ .bp { color: #007020 } /* Name.Builtin.Pseudo */ .vc { color: #bb60d5 } /* Name.Variable.Class */ .vg { color: #bb60d5 } /* Name.Variable.Global */ .vi { color: #bb60d5 } /* Name.Variable.Instance */ .il { color: #40a070 } /* Literal.Number.Integer.Long */ ================================================ FILE: pylons/docs/en/_oldstatic/pylons_as_onion.ai ================================================ %PDF-1.5 % 1 0 obj <>/OCGs[15 0 R 24 0 R 28 0 R 32 0 R 36 0 R 40 0 R 44 0 R 48 0 R 52 0 R 56 0 R 60 0 R 64 0 R 68 0 R 126 0 R 152 0 R 161 0 R 165 0 R 169 0 R 173 0 R 177 0 R 181 0 R 185 0 R 189 0 R 193 0 R 197 0 R 201 0 R 205 0 R 263 0 R 289 0 R 298 0 R 302 0 R 306 0 R 310 0 R 314 0 R 318 0 R 322 0 R 326 0 R 330 0 R 334 0 R 338 0 R 342 0 R 400 0 R 425 0 R 429 0 R 433 0 R 437 0 R 441 0 R 445 0 R 449 0 R 453 0 R 457 0 R 461 0 R 465 0 R 469 0 R 580 0 R 611 0 R 615 0 R 619 0 R 623 0 R 627 0 R 631 0 R 635 0 R 639 0 R 643 0 R 647 0 R 651 0 R 655 0 R 766 0 R]>>/Type/Catalog>> endobj 788 0 obj <>stream application/pdf Web Adobe Illustrator CS3 2008-06-01T12:30:09-07:00 2008-07-18T18:27:25-07:00 2008-07-18T18:27:25-07:00 256 180 JPEG /9j/4AAQSkZJRgABAgEASABIAAD/7QAsUGhvdG9zaG9wIDMuMAA4QklNA+0AAAAAABAASAAAAAEA AQBIAAAAAQAB/+4ADkFkb2JlAGTAAAAAAf/bAIQABgQEBAUEBgUFBgkGBQYJCwgGBggLDAoKCwoK DBAMDAwMDAwQDA4PEA8ODBMTFBQTExwbGxscHx8fHx8fHx8fHwEHBwcNDA0YEBAYGhURFRofHx8f Hx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8fHx8f/8AAEQgAtAEAAwER AAIRAQMRAf/EAaIAAAAHAQEBAQEAAAAAAAAAAAQFAwIGAQAHCAkKCwEAAgIDAQEBAQEAAAAAAAAA AQACAwQFBgcICQoLEAACAQMDAgQCBgcDBAIGAnMBAgMRBAAFIRIxQVEGE2EicYEUMpGhBxWxQiPB UtHhMxZi8CRygvElQzRTkqKyY3PCNUQnk6OzNhdUZHTD0uIIJoMJChgZhJRFRqS0VtNVKBry4/PE 1OT0ZXWFlaW1xdXl9WZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo+Ck5SVlpeYmZ qbnJ2en5KjpKWmp6ipqqusra6voRAAICAQIDBQUEBQYECAMDbQEAAhEDBCESMUEFURNhIgZxgZEy obHwFMHR4SNCFVJicvEzJDRDghaSUyWiY7LCB3PSNeJEgxdUkwgJChgZJjZFGidkdFU38qOzwygp 0+PzhJSktMTU5PRldYWVpbXF1eX1RlZmdoaWprbG1ub2R1dnd4eXp7fH1+f3OEhYaHiImKi4yNjo +DlJWWl5iZmpucnZ6fkqOkpaanqKmqq6ytrq+v/aAAwDAQACEQMRAD8A9U4q7FXYq7FXYq7FXYq7 FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FWK+avzR8jeU7+Kw8wag9jdToJIFNtdSK6kkfC8UToTtuAajFUnf8A5yA/KVBV 9bZR4tZXw/XBiqKsfzv/ACmvZRFB5nslc0NJmaACtOpmVAOvfFUt1f8AMbzqlxNeeWNF0zzdoMZP GbStTElyqjassYjfevaLmcVQnl7/AJyH8pXl0bHX7a48u30bCO4+tDlbxOTQLLJRJYfnPFGPfFXq cUsU0SSxOskUih45EIZWVhUEEbEEYquxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Ku xV2KuxV2KuxV2KsL8/8Am/zr5akiudJ8rf4h0lo/9Ilt7h1uIZeX7VukE7tGV/aQMa9QBvirz2T/ AJyauI+aS6DZ28oDUWXVGDKwH7SNaRtt3xVBy/nn501kta6dpOnXaSD4rKO2utTYigO6wvHUV/yM VYpffmV5p0qeSSOOz0eZH4yRWWn2MLROvQTRSrcXMVCdi6jFUm1XV/zP88QmGa0v9at2qxKtcS21 SCfsWEPp916j9WKpHL5a13y+iSXlg1kUYMJL+G9t2jpUj0J7niRuNqMMVRE3nXUr6S10/X1/SCSh k06e/driRSSNrfVE/wBJiZuyNJLH4oemKs3/ACV/MfWPKWqR6LrIdPKt5ci1jWZlIs5piPSniKfC IZHfhKtFVW+MKnxLir6jxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2Kux V2KtOxVGYKXIBIRaVNOwqQPvOKvPtV84fmy5ki0byC8QJKxXd/qFgaU/aa3hnNQfaYHFUok/Ln8z vNRB85+ZxY2D/wB5pOkAhCrblCxCLt4SrMPfFWW+WPyr8ieWxE2n6VFJdRcSl7dD6xOCvdHk5el8 owq+2KssxVgvnv8ALjWPMFydR0bzZquhagqKsdtDcSfUDx7tbxtE3I/zB/mrdMVeDeZ7K9sb6TSP PGlx6X5hUGfT/NWnwBoboo1ed5bW6Il3HVgGliiWePb4O5VSCGSHWdGYOvBbmN4pU2biwqjUI2NG Gx+nFX1j+Xmuza95F0HV5253V5YwSXbD/f8A6YE3/JQNirIcVdirsVdirsVdirsVdirsVdirsVdi rsVdirsVdirsVdirsVYb5r/NHSvLWsXGm3OmajeLYaemr6ne2ccMkNrZvJLH6soeaOZuJgcsI42o N8VZEPMWgfpC2006larqV7F9YtLFpo1uJYqV9RISRIy0HUDFVFfN3lNtRk0xdasDqUTtHLZC6hM6 uiNKytHy5grHGzEU6AnoMVRVtrejXT2yW1/bTveW3120WOVHM1r8I9eMKTzi/eJ8Y+H4hvviqB/x t5M/Rcmrfp7Tv0VFL9Xl1D63B9XWb/fbS8+Af/JrXFVDTvzB8m6j5muvLNlq1tNrVpFFO9qsqFmS ZWcenv8AvCqJyfj9kFa9RirIcVdirsVdirA/zu8uW+sfl3qtzxH6Q0SGTVdNmH2lltEMjID4Sxho 29mxV87o6uiuv2WAI+R3xV7z/wA49Xvrfl6bP/q26jfW/Wu0k5ul+5bgYq9LxV2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV5F+Z35Z6/5k8za9qtlHLt5ctrbSQt16Vvc39td3Nx9Vur f1FSaJ1dFYTqUo3jWiqgfy+81T/mJLqd9HqP6MvtT0/WontJNKEFu9pbxR/V7ozKb34GiZKW7lGR uxLVVSGz8r65pmt/l15W1XR0tJ7O41u0bzCJIJFvWuNMuyZowhM49SvOQSqp5fzdcVRdl+Xv5ial plvpFzpn6FNh5HuvK0eovdQSpLelrVUdVhd5FilSBjyK8hvUA05KrR+XPmb9HTamdK15vMqy2zae 5uPL8f1aa1tZrdJUigEVrJAyT+nJ6i+oVAoopirOPKGhea9N89Xmp6xp8cq6zo2kxXWo2jxCCC9s EnW4i9JmWbi7Tj0yikUG5GKvQcVdirsVdirAfzy12LTPy51O0DgXmuIdJs03qxu1KSsKf77g9R6+ 2KvnoAKAAKAbADoBir2//nHVG/wlq8vDjHLrExjPZgltbxMR/s42GKvVMVdirsVdirsVdirsVdir sVdirsVdirsVdirsVdirsVdirsVdirsVUJ7CxuLi2uZ7eKW4s2aS0mdFZ4XdDGzRsRVCyOykjsSM VV8VdirsVdirsVUL7ULCwt2ub65itLdftTTusaD5sxAxVhevfnT5E0xHW0vP0zdD7EOnATISR3uK i3FO/wAdfY4q8M80+Z9a82a4NV1UqGiVotPsIiWhtomILBCQC7vxHOQgVoKBQAMVSsgg0IofA4q+ j/yas7a1/K3yz6H2rmwivLkk1rc3Y+sXBr/xmkanh0xVmeKuxVhfmL81NL0PV9U0+bSdSu4dDtYb 7WtRtY4Ht7W3uBIVkkVpknYBYHZvTiagGKsii8z+W5dTi0mPVbRtVmiFxFp3rx/WWiK8hIIOXqce O9eOKoYeePJRhknHmDTTDDMbaaUXkHFJ1RpDEzc6BwkbtxO9FJ7YqqP5w8pR6KmuvrdgmiSHimqN dQi1Y1IoJy3pk1FOuKtX/nLyhp8ME9/rmn2kF1GJraWe6giSWMkKHjZ2AZSXAqNtxiqJuvMGgWl/ a6ddala2+oX29jZyzxpNOP8AiqNmDP8A7EYqusNd0TUbm7tdP1C2vLmwf0r6C3mjlkgk3+CVUJKN sdmxVG4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXiX/OVHmTzN5f8r6Ne6Fqlxpkkl80M7W0jRs6tCzA EqRsOGKQy78htd1LXfyo0LU9TuXu7+UXK3FxKxeRjFdSxqWY714qMUFn+Kqdzc21rbyXNzKkFvCp eaaVgiIqipZmagAHicVeZeZfz88vWbNb+XrZ9buAafWam3sgQaGkzKzyeIMcbKf5hirEINY/Obz0 5+qXMtpYMQG/R6/UbdSux/0klrgtvuFl/wBjiqY6Z+SGjvKb3WtQk1O7FXmezX1DyG7Fr65qpI78 qHFWTWPkTyZbxg2ujR3dKn6zPNLcdNzz48bTYdR6oH04qnFgq21W0+G0sIT8Pq2kUdtH8nkSK9h6 0p+8HyxVD+bvLUPmbTLzTryCMa/bRGWwukA5MUoePLaqtyCkmg3qBVSAqlP/ADj5rS3PkybQ5DS6 0C7mtypPxG3nY3Nu3yCymP8A2BxV6firsVePeffyt17X9c87anbLOktzpmmjQI0uzHa3l1Zm5kkt 7u2EixTRsxjT/SE40Y0/axVLrT8t/OEnma6fV7bUprC51s6/CbSXRltV5EOlvPJIg1ENGn+jsI3Z Sg+FgDiqTahoHmfQ7fytYXOhzTaHaeZ9OHlzQ9QmsJL3jBp1+ZImnty0DIhCGD1X5bfEw64qyK08 k+cLTzOnnX/D/qWz6neXn+DY57T1YRc2NvaJdK7SLaGcvbOzgS9JDRia4q7y3+VfmC01KKbUNOgN rJouvxpah4pIbK41XUY7m3s4+VDRIWdS6jj9oVoRVV2m+R/PFjN5Qk0+wubHWItJ0jTfMuoyT2U9 iYLSEidGjZ3ufrMLu/pSQ/CT1YjFUf8Ak35A8xeXb6F9cS/W50zS/wBEpLI+mGwmUTLJztxaJHdt 8SFg118Q5NWpJOKvWsVdirsVdirsVdirsVdirsVdirsVdirxn/nLHTWu/wAqfrCio07ULa4Y+AYP b/rnGKQs/wCcTNUW7/K57Tl8enahPDx7hZFSYH6TIcVLKvzD/N3SvK8j6Xp8a6p5hCgtaK/GK3DC qtcyANxqNxGBzbbop5YoeLalqnm/zxqkMOoTSancu9bXT4VKW0bAkgxwAlQVqfjcswH7VMVeo+U/ yh0zTDFPrqjU9VYB005P95469DKx+1077dQA2Ks6nMXp+nJ6cyRkRCIAi0iI2EaxLvO4pslOo24Y qo+neXstET1XjanOXgRGynoBxeGIqRQcVlehoxXFXGwspJSLieTUbmNqGCCrcGU13kdnMLct9pEB p0xVEyxWEdjcXlvai2urL45gVX1TwAlaN3Uty9SM7nketeuKoVw1s9nPv/oTyW8i9WZYeQi5DrX6 s8jinUkeOKvMtRcflr+bMWsE8fLPmMeheSA0ijSWTlHIei/6PM/0RyE4q9xxV2KuxV2KqFzYWN09 u91bxTvaSie1aVFcxTBWQSRlgeL8HZeQ3oSO+KoVNdsJNfl0KNud9b2yXdwBSkccrlIg29auUYj2 HyxVMcVdirsVdirsVdirsVdirsVdirsVdirsVdirsVYt+afl1vMf5d+YNHRec1xZyNbJ4zw/voR/ yMjXFXyP+Sv5l635WtNd0bSeK3esrC1tdyUZbVoeYklWM7O5R6KDtWhNQCCpLLvLPlnU9b1NdP09 XuLu4dpbm5lYsSWNZJ55DUkkmrMdyffFD6H8peTNM8r2v1LTQJtTlUfXdRdfiAO/TsP5U+k4qmsk kaxmKAkxOSXkqA87gUcl/wBlAPtP26L2xVSSFSEkmJMbD07eGIcXkH++4l/3XF471OxZgopiqOSw eVFW64rAoCpYxbRADYBiAC+3b7P+TtXFUaiJGioihEQBVVRQADYAAYqgwqrq0yOBwubdCq9QxiZh JUfKVB/tYqlsEDgNZl+Lv+5Ejb8bq1o0EhH7TSRBXI6fDTFUi84eWIPNXk+80cwg3MCevp8bbkbM jW5O3Qh4j7cTiqB/Ivzjcaz5bk0PUpGk1ny8UtpZJDV5rVgfqs7E9WKoY3PUuhPfFXpWKuxV2KsY /MX8wNE8i+Wp9b1RgzKOFlZhgJLicj4Y0r97HsN8VeZf84wXWseYT5v88aw3qXmt30UAalAq20Zb hHXcIonVR/q4pL3TFDsVdirsVdirsVdirsVdirsVdirsVdirsVdiqXeY9esPL+hX+tX7EWmnwvPK FpybiNkQHqzmiqO5OKvgDWZLvSPNzaiYEtjNKbxbaH+6RZiS8KE/spUoPbFk+1/IGn6DoXk6wutM nhnk1eKO4bUW2R/UUMGYkgiOMGgXbfbZmxYsknaONGtIyzqCBdOCPVmldQREDt8TChZtgq+A3VVS jVSPVmAkQsI1RP8Ad0grSOMGn7qP8aFjsKlVMI0W3Rrq7cGYgeo4qQo7Ig60r9JOKrfQnu97qsdu elqCPjH/ABcf+NBt48gcVa09VgnurNQEjjZZII16LFIvbw/eJJt/CmKt6h+7ltLnoIplSQj7RSYG IL8vUZGI9q9QMVQWp/uLi4m+yFijvEp1LWr/AL/5F4mVK9xt0xV0/wDouoSSDZYnSfwAiuf3Uy+A VHQTO33064q8k1eM+R/z00/Uovg03XXFpdAdPT1Bwq16AcL1EPshPjir3TFXYq89/M787/J3kK3k huJhqGucf3Oj27AyVIqDM24hXfq29OgOK0+NfPfn/wA0eftf/SWsSmWUn07KyhB9KFGbaOJNzuep NWbviyfb/wCVHk7/AAf+X+j6E4AuoYfVviN63ExMkoqOvFm4j2AxYstxV2KuxV2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV5F/zkXq7JpOiaAjEfpO8a6uVHRrfT1D8T/0cywN9GKvJT+Ut7568r6xfaca6 poyq1hAKVndqtJDXsSi/D/lU7VxSHnen/mlr+n/l3qXkeQyenPLCbS45FZLaOOYzTQqOoEkgVvvr Wuyl9P8A5M/mVa+e/LdurzLDrtino6lAGo6j7U98OlPXLcR/Kxam1cWJen6fEsnG6KBE48LOICgj h2pt2L0r7Cg7YqtWWOdjfTNxtISfq1ejdjL716J7b78hRVs310JYZHhEVpK4jq/94OQPByBsoLUU L1+LelKYquuv3eo2Uo6ymS3YexQyg/QYqD5nFXav/vLH/wAxNr/1ER4qtukRtTthKoaGSC4hIYVV mcxtwIPisbfdiqChQXFrpLXJMiXdo1tLUnkWmiWQsT8oW+k4q82/PrS59S8labqatw1CEtbvIo4s kzpWtR0Mc0P0HFUL5r/5yXOgaNpl+3lLUJBqlpDc293O0cNm5miElI5ozcV413VgrDuBimniPnL/ AJyY/M3zEklvbXSaHYvUelpwKSlT05XDFpK/6hX5Ypp5VJI8jtJIxeRyWd2NSSdyST3xV7f/AM4v fldJ5g8zDzXqUP8AuG0SQNa8x8M16N0A9odnP+Vx98UF9hYodirsVdirsVdirsVdirsVdirsVdir sVdirsVdirwD/nIG4kfz1o1s393Bpc8kfX7U1wgf2/3SuKoeD8l9N80/lomvWty2l+ZYvrM1pqUB ZS8cTFfRn40JXlCeJBqta+IxS+d/PHkDzf5P1R7TzFZvDKxLJdg+pDMCT8aSjY8qHrv4jFKB8rea tc8r6vHquj3BguVBSRescsTfbilX9pG7j+O+Kvsv8tfzv8vfmHYxafEw03zFJ8N5p7tQ8ApaSS3c 05ggUFPiWtSKCpUU9EtkW5cTAAWkJ4WkYHwnjt6ny7J7b9xRQrajA89jNFHT1ipMJOwEi/FG30OA cVQ95PHPYWl3FUB5baSJjswEsiKfvRyD88VVNW/3lj/5iLX/AKiI8VdcfHqlnEfsok048eacIx9H GZsVSo6hYWGk6BcX1zFawKELTTusaAfU5OrMQMVeVfmn+cX5ZDyfqOixavFqOqNctLbR2INwm9z6 3ITJ+5/u2I+3imnisv8AzkD5li8hw+TNNs7WKxiE0cl3dRrdSPDJO8iRiOUNCoRHCbq3SoIxWnlr K9AxBAatDSgNOtMUs0/Kr8rdb/MHzCmn2YaDTYCr6nqRWqQRHsK7NI1KIv09ATir7t8ueXtJ8uaJ Z6JpEAt9PsYxHBGOviWY92ZiWY9ycWKY4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXgP8A zkFAyeedEnP2Z9MuIwfeG4jJ/wCTwxVnv5bTRv8AlIqKatDDfJIN9mMkrgf8C4OKsi8wWlpeW+t2 93DHcW76cnOGVQ6GhuCKqwIOKvLtU/5xo/LXzDd3Rtop9FnQysWsnHpsWuZY0/dSiRVVVh6Jxr+t TbANR/5xN8wWd+X8veZIZJ4LhI7VriOS1kEvATcg8RmpwU8uWx2NBWlVbevfl7F+eOgLBpPmq1sN e02ICOPUrO5EdzGi7DkkiRLKKDavE+JOKsu/MnU7nS/y98yahayPDd22mXb200Zo6SiFvTdT4q1D ih8TRfnZ+asccUa+ZLv04eHpo3BlHpkFNmUjYqMWVL7n88vzZuTWbzNdHdWovpotUIZTRVA2Irit ICX8x/zQ1aYIfMWr3MtCBHFcz1ozAkcY2G3IL+GKusvy8/M3zBLC8Oh6ndmeiQXE0Uqxt8PIASzc U+yK/a6YqyuL/nGz8w4tJv8AVNX+q6XBpwrPDJKJpiSqsAoh5x9HHV8UWzn/AJx+/J/yPq+oa0df tTqtzpLWrW6SsVg4XCOatEp+I8om2Yke2K2r+Zvym1n80vzRuns4V0XyJoQTS7W8SIRo6WzH1ktI gFVv37SDkPgHv0Kr6E8peUNA8p6JBo2h2q2tnCKmm7yPQBpJW/adqbn+GKE5xV2KuxV2KuxV2Kux V2KuxV2KuxV2KuxV2KuxV2KuxV41/wA5H6Y/1by3ri1KWl1NYTU/ZS/jDqx9vVtY1+bDFUb+SF2l 95X1jRC3CRJGYP1olzHw+zt9loyevfFWeStJewzMiUl1TTAYYwQaMgaqljx73K0+npirtAmie9ui rVEih0qCOSvNNOCK9f3dxGT4chXfFV//AEtf+3j/AN27FU5xV51Bput6b+Ynmlr3Xbq9sdfsLebQ 9Km4/V4JEf6rMqLQ09N5ITVeNRJ8YZgGxVjNovn9tI82xXWgaRq3mfy7dznQrFIUWNraSKJrRSSA ZE9KaUqKhmZOLMDviqYXnm+x0nRPK3mQeUI72TWjp1ldRaakIW3l1VElYlnUcgv7tUrRSWoXGKp2 35heWB5u1vyfbPLBrcFj60TJE0dukQb0lHqiiqwuJjVjRdx8VcVR+kfmH5E13T7DWNL1e1/QcM0s Qu3b6vGs8ZW2WAiUR8S31kFQRvtTriqzz9IF8k+Z5RukjBVbsaLDE1Pk6kfMYqwD/nHP/lI/No7f U9INPf1dQ/pir3MAAAAUA2AGKuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KpB5+8r J5q8napoJYRy3kP+izHpHcxES28hp/JMit9GKvB/yZ80vpnmi1N4pthectP1GB9jDcB+BRqnYxzp xb2rir36X/RJ3Y7LaytdA9C1vPX16t34SMZCKdl74qg7P/cfriwttHVoVHYK/ARkHrQKkEQr1bl4 Yqj7yGVb9zCP3jiO6t1FBykhPCYVOwLwsqCvzxVFfprSBs17Cjd0eRUcHwZWIZSO4IxVjPmyX6vq vl/zPIjRWmm3n1OXkh9VodTX6rXh9sD6y1vtSux26Yq7lcaV53W5dKz+ZbFo/RJ+FbjT5OUMfIVG 8N1IXP8AxWT4DFUv0/SXbydrnkqNwb+1ubqzsZSAvD6zS+tp1A2C263C07ckoN8VU5L2xuofKHmy 2t1ijtktodTZv7xbfUQsSW8h3r6N00czhvs8K964qld5+XHkfU9V1TyhrOnR/wCF9RkGq6HbxsYE hubQ+nqCQ+nw9Ic5A9Afi5P+yuyqWef9T83aL+Xi6NeW8F5FfTILbUTOIp5YWf61+9t1iCpIh+By h4nqAvLiFUt/5x9PmaOTzPqdppMVyk09rYsWuhFxa1haagPpty/3sxVlPnfUP+cgLfXY77yjpVrd 6d9VgR9Kubi29EXLSzeu5Yi3nbjGI6ESqB/I25VVINM86f8AOTOrypcW3li2tdOd4kla6tltZk9I 8rgpb3F+sjct40JNHHFh6e+KprqOtf8AOQt9p80OmafY2+qWlxY28rIixITc6ZJJesfrUkyvHa3V xBwMLHmY2FTVlCren+Y/+ckRHf8A1ryvp5W3jMen83g9e4k9CRllk9O/9JVE0aIy/DyD8hx4lcVQ 2j+d/wDnIvVryEReU7GxsfrrWt/PfRPDJbxhYC0kcT3iG5RGklCyIQJAteKHbFXtGKuxV2KuxV2K uxV2KuxV2KuxV2KuxV2KuxV2Kvm/85vKreXfPB1a3ThpPmY+ryAosepRr+9TYbevGokHiyvir1b8 vPNh8zeX4XLg67pQCXCsSDKvGgLE9pQN/BxWmwxVM721R4YhFULHVbc0IYJHWsZpv6kPxBaVPDkV q2+Ko2PU7e4tkF3J9WuISGW5anph6bEsDxHJW3BI5A/CStGxVGJPqrIrRxW0yMAVmWd1Dg9GCiJ6 V605H5nFUk8xWQ17Q9Q05LgXd7cwSRWrW4Ho28xU+nKzEsvON6NVjXb4VxVJ9X1eG68q+X/Ostfr tvNaakLfvDDMvoXsSrXb0be5lLk/tL8VABRVMJWm0zz/ABTOqvceYtPeGOMUCJNp8gdE5dfiiunZ 2pv6f+quKoTT9MjksPM/kmVv3lxcTssqgB/quqJ673Pt6c0s0cY/yFHToq3HaXPmjypocMMqWWt2 Lq9xM0ZmjgntC9nexOivEzpKwmg+2pIJYGq4q8d/N3zP5g1HWpIZ9VsZ7LRldBNBZTRxltjM3Frq UmhXjWvbFXqP5WeTvPWgeSrGAajp1pc3nK/vYJtNnklSa6PqNG7rfRAmJSI/s9F79cVXebvKf5tX ev2WqaBr0Ft6FkYbuP15ra3mnBl9MpZvDqEMY/eq0kj+o7cFVeAqWVSuw8sf85HW9nZRTeaNNuLj 6xHNqU78d0EkjzRQr+j9kkWRVFd19P4T8dEVTW68s/m1FeTvpWrQQxzXN9LNNLeyytLHLIWslWC5 s7uG19CJvSpDty4yN6lDGyqSv5B/PebyjHplx5ttf0pa3VnNaXcclzG3oW9k0c0U08aRSTepdFX+ MEEbsCP3eKrLfyV/zkZLf6Lc6h5ysg1qCmqNa0RJI5JauUtzZ+k8qxCivJtXoq0ZnVT3yx5e/OuG /wBFm8z+YbS9itJnfVUsykMc0TR3Cqoi+pKzMryQmvrKpC/ZDDk6r0nFXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYqkXnbyhp3m3y3d6HfExrOA9vdIKyQXEZ5RTJ0+JHANO4qDsTir5x0TVvM3kjzU8F 3GLbW9NIS9tgSYbiFukiH9qGYCqnqDsaMpxV9C6DruleatM/SGlSKJiFF5ZSNRlcbqH4/EjDj8Ei +HemyqyRJIbkAh0mNeKGqynqxC+l8TDufT5r3aOtTiq6O3SSsxt4Z0ZiZbpraG8q56gG3MMh/wBl FWvXFUcL25YBZLmaNB0NvYTo3yrKJlp/scVY15f0lp4PNHlSW0EFu884eW5bnM1nqiGfohYMolln jX94KcenbFUFLqkn+CNL1x63OvaBPDca1NUsI5LMvZ6mXY9AkT3HFBv3ApU4qnd67aX5606aN/rF xrVrLp92K0X6xBW7tKip9NFi+tUHU+5qSqwf8ydYXyy99ZaBr95Bq2qSm4v7e3WyMELSIFkYGS3l lRnpUKJNiS3U7qsG/K/8tL/zdrTXNxqd0mhaVIr3E4S1Jlu0KyRwIHgZGCGjy8lIpRaHkaKvoH/D Otf9TZqv/IrSv+yHFXnnnXyjaf41k1C589T6Pcro8dveXZtfRkROV0ttNcanbG0hgRp5qiBuCytE uzEbKoLRdF8v3cWuWL/mqdRdZ9Njike6aUWbw3cNwjILy6uoZpbh0ROfEqJOSqvWPFWO2ekafpeq Jp7fnPLZ2+lRPaDSpI7uztkNoj2E3qubyIDlLZOaK6UNWi4lg2Kqn5aaH5U8v3uhyw/m691oNgJC dKYy6fY3E3JbpgztcBA6/pGDlG3JjurA0ZVVe3/8rB8hUnP+JdKpbCtz/ptv+7HNYvj+P4f3kipv +0QOpxVObK9s760ivLKeO6tLhRJBcwuskbo24ZHUlWB8RiqtirsVdirsVdirsVdirsVdirsVdirs VdiqQ+c9K806lo/o+WNc/QOqxuJEuTbw3McigEGKRJleitX7S7incVBVeWeZfzD/ADe8hWcP+Ir3 yxqtwxUR2MLXa6jMrNTmqJGkQVdyWKKvvWlVXmP5k/mZrXnRbUy6fY2V5p7CWC4tg5vOFfjiEztw 9OQfstGRyAPbFUz0hfN+i2kHmjRne90sg8da0tWkVOO7xXtr+8lt3Sn7xHDIP5zir0Xy7+fdhfWq w6/Yx3Vu+xu7MrJG1KU5ROSOoqSG+jFWZWOveQtWb1tO1tLe4oPhkkMUh8F/fcZuA6FY3AxVPFsN dRQYdQE8TCqAFYwo/wBeRLt3r7t9/ZVjt9Y6naed9NubkLJHrVtLpkrPcNxaa25XdsrRxRQK6iI3 R4t18RiqWDVvLOkXnmby7r2qww6dI63KWNqoRWgv4mSeLgpmkB9eCZmCkU579RirzLWvzdsdR8o2 GgKsME2n8VvNRlkEc7XVsDE88YUqYy5Vm5EkkN2xVJvJvkLS/Nd4Li91SLS9F51ub+e843M/crbJ JJX4v9/OOP8ALz3oq930/QPyi06zisrG8tbe1gHGKGPVJQoFan/d/Uk1J7nFUR9Q/K//AKuUH/cV l/6r4qxvzrP+TUd0NJ8zC5i03UdOt4v0it7dpZXcJvCkVtyt7gSXDxzSlmPBljV/idQ1Cqkkb/8A OM0sdhZehLcwAeramddWm4CCGzlUN63KTglvHAdxwREIbiAwxVD6/ef8432yab5mbTrm5Pma5u9S stRs5L23mlvYLqGGdqNNbTJIJJ+ajiAFV2FP2lUt+vf84vvFZ2UWgXl3DbrPrVlEyX3pkpbPLcsj XEyI4RdMEbCpjL0C1/ecVVaKX/nD+Gy9KFoxaXlpJF6Kfpgq9rFKkkr8BX/dlqvKWlW4bsaHFXqf 5e+avyyka48l+TbqMP5bDQzaYiToYVSVkajTqPUHqVqwZuta7iqrNcVdirsVdirsVdirsVdirsVd irsVdirsVWXEXrQSQ82j9RWT1IzxdeQpVT2I7HFXhnnb8pPKXlmJb610HXfOOqX7MCguZmX1BT97 eTwBbj9rY/FXvTrirAP+VGfmneW9zrVrolpp9zNJF/uNDwwyvEpHwxpzZEVUr8U0pkLfaqTyxVjl rrPmryTr+p/Ubx9J1SwdYb4WksdxFK6oCY5o6PBIYyxQ1qVbkAwocVZz5t1y9sb+CL8yfINrDql+ rTQ6xpN0thcyhCquX9CS5SZ0Lr8MsnHfYYq1oXlvyb5mufqvlvzZJY6jJ/caL5htUW4dv5Y7i3eK GTxpGJGHfFU5m/Jv80bJK262Nwpr8Freyq23iJYYV3r/ADYqkfmj8t/zTj0ibUbuwb09NH16V2vI 5CEt/jkoqs7MTEGXYV3xVG3X5K+brO+0/UNQnt4bCcPb3EumQXeryoHT1Y3aCGKBuPKPhyUkAvir NPLfkb8rNJmS7vdP1fXL9aMJtQ0bUnjVgOqW62ixbHdSysw/mxVnH+IvKH/Vqvv+4FqX/ZJirv8A EXlD/q1X3/cC1L/skxV3+IvKH/Vqvv8AuBal/wBkmKsF87/mX5D0TzZBDq3lGO8tRp8Fwmqy2yQ3 SVmnljtxFew26pxe1Mio06uzfYjY4qkw/NT8iY9Rht08kxcPVs4YLhNP02qXc6V9MqZFZDbCNVlb pGw4mm1VXaz+cf5aWWo6LpP+B4rnSp47SbT5hbW5WC31a1+sTcIVjdFkAkiV41f4+da7bqpUfzo/ J2yvrFo/y9s1iuP0r9ZuLeHTJZY4LRJULRpB6nqi4jUqxDCLcqJHo1FUe/50/wDOP9xYpFdeSpFt FRIY4LjSLEL6R9a4hVI2fdGkWbjxFFfkW41JxV6h+XGreTdcGs6noGgjRry2v5tN1ZpLW3t55Lm3 4vJyeBpBKoaT7XI71+eKszxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KuxV2KvPPPn5G+RvNWnmK Czh0TUVd5E1GwgijdjJX1EnVQvrRuWqwJrXcEHFUHe/kD5d1pLF/NGq6jrV3p+lxaXayNIkMcTQq FF3CiqXE5NWLSSOKncGgoqwO3/5xa1NNRuLO71KyvNCuecn18RPBqMM/H93IiqJI5BzUB43k4EVK qj74qmPl/wA4/m3+XepjQfOGkX/mbQlbha6xZRS3c4Qn4WWRQ5lG28UxEq9mcUqq91UwXdoCyFoL iPeOVGQlHH2XjcKy7HdWFfHFWKeXrjzbo+i2mkTaFNenTY/qcd6t1bD14rcmKKYh3DBpI1Vmr3OK ph+nvM3/AFLM/wD0lWn/AFUxV36e8zf9SzP/ANJVp/1UxV36e8zf9SzP/wBJVp/1UxV36e8zf9Sz P/0lWn/VTFWKebfPn5kaJfXV9b+XrefQrWzsgthK7x31xqWoXb2sUFvcKZLVgremXFOXxDxpiqSw fnV+ZEen2klz+Wuoz3LQTNd+it5GFmhiDKgjezdh6z/ZozBQR8TNyVVUdr/5vectH80w6efJtzca Xfi3isLmk6M1yLRry8SqQz8uCvFGnJI15LJyeiPwVZt+X3mm781+TtM8w3WmvpEuoxtKLCRmdkTm yxsGeOFmWRAHU8BsfDfFWQ4q7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7FXYq7F XYq7FXYq7FXYq7FWmVWFGAIqDQ77g1B+g4q3irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVd irsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVdirsVf//Z uuid:D1901A446631DD11B693883DCB3DE232 uuid:8b042820-52bd-2b40-8c3d-932332e9814c uuid:CE901A446631DD11B693883DCB3DE232 uuid:CD901A446631DD11B693883DCB3DE232 Document Web 1 False False 14400.000000 14400.000000 Pixels MyriadPro-Regular Myriad Pro Regular Open Type Version 2.007;PS 002.000;Core 1.0.38;makeotf.lib1.7.9032 False MyriadPro-Regular.otf Cyan Magenta Yellow Black Default Swatch Group 0 White RGB PROCESS 255 255 255 Black RGB PROCESS 0 0 0 RGB Red RGB PROCESS 255 0 0 RGB Yellow RGB PROCESS 255 255 0 RGB Green RGB PROCESS 0 255 0 RGB Cyan RGB PROCESS 0 255 255 RGB Blue RGB PROCESS 0 0 255 RGB Magenta RGB PROCESS 255 0 255 R=193 G=39 B=45 RGB PROCESS 193 39 45 R=237 G=28 B=36 RGB PROCESS 237 28 36 R=241 G=90 B=36 RGB PROCESS 241 90 36 R=247 G=147 B=30 RGB PROCESS 247 147 30 R=251 G=176 B=59 RGB PROCESS 251 176 59 R=252 G=238 B=33 RGB PROCESS 252 238 33 R=217 G=224 B=33 RGB PROCESS 217 224 33 R=140 G=198 B=63 RGB PROCESS 140 198 63 R=57 G=181 B=74 RGB PROCESS 57 181 74 R=0 G=146 B=69 RGB PROCESS 0 146 69 R=0 G=104 B=55 RGB PROCESS 0 104 55 R=34 G=181 B=115 RGB PROCESS 34 181 115 R=0 G=169 B=157 RGB PROCESS 0 169 157 R=41 G=171 B=226 RGB PROCESS 41 171 226 R=0 G=113 B=188 RGB PROCESS 0 113 188 R=46 G=49 B=146 RGB PROCESS 46 49 146 R=27 G=20 B=100 RGB PROCESS 27 20 100 R=102 G=45 B=145 RGB PROCESS 102 45 145 R=147 G=39 B=143 RGB PROCESS 147 39 143 R=158 G=0 B=93 RGB PROCESS 158 0 93 R=212 G=20 B=90 RGB PROCESS 212 20 90 R=237 G=30 B=121 RGB PROCESS 237 30 121 R=199 G=178 B=153 RGB PROCESS 199 178 153 R=153 G=134 B=117 RGB PROCESS 153 134 117 R=115 G=99 B=87 RGB PROCESS 115 99 87 R=83 G=71 B=65 RGB PROCESS 83 71 65 R=198 G=156 B=109 RGB PROCESS 198 156 109 R=166 G=124 B=82 RGB PROCESS 166 124 82 R=140 G=98 B=57 RGB PROCESS 140 98 57 R=117 G=76 B=36 RGB PROCESS 117 76 36 R=96 G=56 B=19 RGB PROCESS 96 56 19 R=66 G=33 B=11 RGB PROCESS 66 33 11 R=236 G=28 B=36 RGB PROCESS 236 28 36 R=0 G=169 B=157 RGB PROCESS 0 169 157 R=102 G=45 B=145 RGB PROCESS 102 45 145 R=139 G=146 B=152 1 RGB PROCESS 139 146 152 K=100 GRAY PROCESS 255 K=90 GRAY PROCESS 229 K=80 GRAY PROCESS 204 K=70 GRAY PROCESS 178 K=60 GRAY PROCESS 153 K=50 GRAY PROCESS 127 K=40 GRAY PROCESS 101 K=30 GRAY PROCESS 76 K=20 GRAY PROCESS 50 K=10 GRAY PROCESS 25 K=5 GRAY PROCESS 12 endstream endobj 2 0 obj <> endobj 15 0 obj <> endobj 24 0 obj <> endobj 28 0 obj <> endobj 32 0 obj <> endobj 36 0 obj <> endobj 40 0 obj <> endobj 44 0 obj <> endobj 48 0 obj <> endobj 52 0 obj <> endobj 56 0 obj <> endobj 60 0 obj <> endobj 64 0 obj <> endobj 68 0 obj <> endobj 126 0 obj <> endobj 152 0 obj <> endobj 161 0 obj <> endobj 165 0 obj <> endobj 169 0 obj <> endobj 173 0 obj <> endobj 177 0 obj <> endobj 181 0 obj <> endobj 185 0 obj <> endobj 189 0 obj <> endobj 193 0 obj <> endobj 197 0 obj <> endobj 201 0 obj <> endobj 205 0 obj <> endobj 263 0 obj <> endobj 289 0 obj <> endobj 298 0 obj <> endobj 302 0 obj <> endobj 306 0 obj <> endobj 310 0 obj <> endobj 314 0 obj <> endobj 318 0 obj <> endobj 322 0 obj <> endobj 326 0 obj <> endobj 330 0 obj <> endobj 334 0 obj <> endobj 338 0 obj <> endobj 342 0 obj <> endobj 400 0 obj <> endobj 425 0 obj <> endobj 429 0 obj <> endobj 433 0 obj <> endobj 437 0 obj <> endobj 441 0 obj <> endobj 445 0 obj <> endobj 449 0 obj <> endobj 453 0 obj <> endobj 457 0 obj <> endobj 461 0 obj <> endobj 465 0 obj <> endobj 469 0 obj <> endobj 580 0 obj <> endobj 611 0 obj <> endobj 615 0 obj <> endobj 619 0 obj <> endobj 623 0 obj <> endobj 627 0 obj <> endobj 631 0 obj <> endobj 635 0 obj <> endobj 639 0 obj <> endobj 643 0 obj <> endobj 647 0 obj <> endobj 651 0 obj <> endobj 655 0 obj <> endobj 766 0 obj <> endobj 767 0 obj [/View/Design] endobj 768 0 obj <>>> endobj 656 0 obj [/View/Design] endobj 657 0 obj <>>> endobj 652 0 obj [/View/Design] endobj 653 0 obj <>>> endobj 648 0 obj [/View/Design] endobj 649 0 obj <>>> endobj 644 0 obj [/View/Design] endobj 645 0 obj <>>> endobj 640 0 obj [/View/Design] endobj 641 0 obj <>>> endobj 636 0 obj [/View/Design] endobj 637 0 obj <>>> endobj 632 0 obj [/View/Design] endobj 633 0 obj <>>> endobj 628 0 obj [/View/Design] endobj 629 0 obj <>>> endobj 624 0 obj [/View/Design] endobj 625 0 obj <>>> endobj 620 0 obj [/View/Design] endobj 621 0 obj <>>> endobj 616 0 obj [/View/Design] endobj 617 0 obj <>>> endobj 612 0 obj [/View/Design] endobj 613 0 obj <>>> endobj 581 0 obj [/View/Design] endobj 582 0 obj <>>> endobj 470 0 obj [/View/Design] endobj 471 0 obj <>>> endobj 466 0 obj [/View/Design] endobj 467 0 obj <>>> endobj 462 0 obj [/View/Design] endobj 463 0 obj <>>> endobj 458 0 obj [/View/Design] endobj 459 0 obj <>>> endobj 454 0 obj [/View/Design] endobj 455 0 obj <>>> endobj 450 0 obj [/View/Design] endobj 451 0 obj <>>> endobj 446 0 obj [/View/Design] endobj 447 0 obj <>>> endobj 442 0 obj [/View/Design] endobj 443 0 obj <>>> endobj 438 0 obj [/View/Design] endobj 439 0 obj <>>> endobj 434 0 obj [/View/Design] endobj 435 0 obj <>>> endobj 430 0 obj [/View/Design] endobj 431 0 obj <>>> endobj 426 0 obj [/View/Design] endobj 427 0 obj <>>> endobj 401 0 obj [/View/Design] endobj 402 0 obj <>>> endobj 343 0 obj [/View/Design] endobj 344 0 obj <>>> endobj 339 0 obj [/View/Design] endobj 340 0 obj <>>> endobj 335 0 obj [/View/Design] endobj 336 0 obj <>>> endobj 331 0 obj [/View/Design] endobj 332 0 obj <>>> endobj 327 0 obj [/View/Design] endobj 328 0 obj <>>> endobj 323 0 obj [/View/Design] endobj 324 0 obj <>>> endobj 319 0 obj [/View/Design] endobj 320 0 obj <>>> endobj 315 0 obj [/View/Design] endobj 316 0 obj <>>> endobj 311 0 obj [/View/Design] endobj 312 0 obj <>>> endobj 307 0 obj [/View/Design] endobj 308 0 obj <>>> endobj 303 0 obj [/View/Design] endobj 304 0 obj <>>> endobj 299 0 obj [/View/Design] endobj 300 0 obj <>>> endobj 290 0 obj [/View/Design] endobj 291 0 obj <>>> endobj 264 0 obj [/View/Design] endobj 265 0 obj <>>> endobj 206 0 obj [/View/Design] endobj 207 0 obj <>>> endobj 202 0 obj [/View/Design] endobj 203 0 obj <>>> endobj 198 0 obj [/View/Design] endobj 199 0 obj <>>> endobj 194 0 obj [/View/Design] endobj 195 0 obj <>>> endobj 190 0 obj [/View/Design] endobj 191 0 obj <>>> endobj 186 0 obj [/View/Design] endobj 187 0 obj <>>> endobj 182 0 obj [/View/Design] endobj 183 0 obj <>>> endobj 178 0 obj [/View/Design] endobj 179 0 obj <>>> endobj 174 0 obj [/View/Design] endobj 175 0 obj <>>> endobj 170 0 obj [/View/Design] endobj 171 0 obj <>>> endobj 166 0 obj [/View/Design] endobj 167 0 obj <>>> endobj 162 0 obj [/View/Design] endobj 163 0 obj <>>> endobj 153 0 obj [/View/Design] endobj 154 0 obj <>>> endobj 127 0 obj [/View/Design] endobj 128 0 obj <>>> endobj 69 0 obj [/View/Design] endobj 70 0 obj <>>> endobj 65 0 obj [/View/Design] endobj 66 0 obj <>>> endobj 61 0 obj [/View/Design] endobj 62 0 obj <>>> endobj 57 0 obj [/View/Design] endobj 58 0 obj <>>> endobj 53 0 obj [/View/Design] endobj 54 0 obj <>>> endobj 49 0 obj [/View/Design] endobj 50 0 obj <>>> endobj 45 0 obj [/View/Design] endobj 46 0 obj <>>> endobj 41 0 obj [/View/Design] endobj 42 0 obj <>>> endobj 37 0 obj [/View/Design] endobj 38 0 obj <>>> endobj 33 0 obj [/View/Design] endobj 34 0 obj <>>> endobj 29 0 obj [/View/Design] endobj 30 0 obj <>>> endobj 25 0 obj [/View/Design] endobj 26 0 obj <>>> endobj 16 0 obj [/View/Design] endobj 17 0 obj <>>> endobj 610 0 obj [766 0 R 655 0 R 651 0 R 647 0 R 643 0 R 639 0 R 635 0 R 631 0 R 627 0 R 623 0 R 619 0 R 615 0 R 611 0 R] endobj 5 0 obj <>/ArtBox[6794.06 6962.0 7506.0 7463.48]/MediaBox[0.0 0.0 14400.0 14400.0]/Thumb 787 0 R/TrimBox[0.0 0.0 14400.0 14400.0]/Resources<>/Font<>/ProcSet[/PDF/Text]/Properties<>/ExtGState<>>>/Type/Page/LastModified(D:20080718182723-07'00')>> endobj 782 0 obj <>stream HlWˎ$7WZ(Ji` |49AUy]CIۯۧz}}m?q~jo~]ݳ^F_c#o~feG܏UGqԺZznxpJk˽Ǿ|zqW/޵4UYJ4U'dWMbw~%Ԯ5ly&tCPQƺLV%˞R%E|=~fq;!P`H-,BxG,SPq,nOB{N!"\!qP%,Ң( :CS[Ui[dڲth2Y ,YK(jƂ,:K6U`iJ|gש^S"'7g+Ԉ+n\$ XP!Hmo4"jؓ UEhce(SzZ{Vhm6r5piR`[^hZU#VcEBEVX@ p| h(lC(8/yA3ȶ .)*baA*dH uaM .<"6kd h\oƆlʑGm%vzd ݘ1N.FVԃ) $gW&/&3OWOmf/P-,|uh.{i8WUܰ] cr (!dяE= KܲPٖ&Q@e rBUL B`Fjw|bc(|l9) Tc˦Qss6}NS]t(bSWqO_758p<٥ٔP~r5h-(9DaK7lp+''#Pֳ1t)- &v|?>c>;FN2nX}\!4yʴďNU dAOif{"=/Nç=BͩiKIጰ1SCD "i6L]NѬ7kNBDn, := $.)M;piiGsI?.r  ǢM$9f838379qҶJ>6D!X)ΤQic#^8S_ ]3扮1tx 1Dꑃ"KBfQXԢ=¨"r8ܪ1 #9KZdkxFg}1t J5ՆR"G sHE]Ŝ^ц@xXX}/2#^A%P{kA1nAm|μN H%X&";?@K:VgWhZ07w_IFŀ|qdhM!C/.5MyUr 4nXjMږ2b;B|ǛA1y՝(0LŸ>M 6ñ"4;а:UՈi{NMj*Ho{5_eBsMb /jƗA Жp\|yFdEk8 475!J0ZP)a|$%Rx%C-ڦobTv\'ڻnQrbg8 ǑǰaNUk1z%Ri[ 0PIQ"Z&tԨXNTDIBގlX14#Aг3Gt! 6ަu)n&:n]AlTL'⇠-X3[+(^Í!ZѢWaY.Wlƕ ]u2a7P۶؛;^NvfWD\l ;]5 l1h^TLLE11:2ms1Kkq{~ +Ks.%xVK2YZ JF~MtHKZ=)u P׌qLuw!-]]=Fg..(}$tL>ƃ JI `*KhqM{[j8a:tW!)OxJ$v"ae0&[ WhrXp)uDmֽ)HkR*kmD95.viI*cװ!-lJՖb|Z| _ +\Y*A7q]ta+ݾM@}e2V]`yC2 xXKfqbo ء &Amz3uKYх!.{֗gbA7FfYw,uF~b #J8Ń!I~ .>DN'nwX\G@b]!9>6,:uhN69iI4z U 6Cؓt(K'VcG@cӀM`sU&.mrUAC>&jyI8=QE8䴼1VAǂM_$l5&bAs2 !gnsxi@w̜BEf{䱨U̇ dj`\ubҴԀl7U5-Mlp˯& -5:Dž)vl7E9s3 :HjuվHT엎f$R}?H> /f M6)jW|}aU@֟Ϗ=yǷrkyHϏ}|{}IO_Ϻ']]lRշfE'6 ݃$Qؒ+'H;q c{ޢN4L~/UUA# fkT`c/"8W 2G3 % :[r 9IvdMP0O Wu}z]߫?WObX;e3Y=hM,UMVk?&w*9m,쟳)Jy\, PAOkFmЌ $A %N~{DU?1~[ sD/ʏ9ٲVH"qkZ ק#qR6ߍ3*r$GfX,[E_87rxI^ WQƀޔRT+,6pZ-W&;dDv`~q;bʁy1 { H@˂OU.R gU /Fbzj5Y5e=*VHofk"BIĹ֕'/RzM[ad'irA $upV;| )i1e%kQΰ|ʛ+>*}HK?MY}.L*l'ۿKJvL]Ran66e'+o{=G6@O9d1yT/e;G8ĦzslfiL z|l9Zy1, UhSpJYxu d#sO0Զ$مjv(-_9gϡ]Krz("\k`;3|>*:@{\p@z@twwaU&;n?_;H'>3.HvrF]-m\h(јu!bpu^Mº5G+΀yPHU@@5!/'y(y^FϳU\ͱ2;|m<υ*z 7죭 ˑ"*G]\Ŗ;ؐ@K\DWDw\!݅Ir=wЀPp*R(7rfςsͭ oBWgl.7#ΥdTk.qd}ș>=(%~]G8pZG ;\jAh )B!#3[ A4 Sd5ZXr'XoLWj"hHG2>*lń<$rw)# Mɐ F9U4cg endstream endobj 787 0 obj <>stream 8;Z]Z0bFC>$j/6U$%FM"'+1;C<$Li+])Vg2!!#5UC8"O>17_8/#QOi)!8u620E=qg,^f~> endstream endobj 614 0 obj <> endobj 780 0 obj <> endobj 781 0 obj <> endobj 784 0 obj <>stream H|yPgƻ٥N[g;M/QAQ9`C$ ܈@tpTD#J,\mtk{jO8fk8>~vPȜ)˶bʨ8ʺ,Ώ'at;_C.OMc68^U;7!Q$s6mw}|x̎LWʂj "I)͎ZeJRb] eIy.ޮOhL!S)b52RRD*M VlMTʼeʍ a_ad s09M07alE62 c1,&. ~X p/op"WZdB Ÿ("z-^?t.v9#sHG4 ~!d=亏c9_d#dQԵ) ]=4Vh&Ob"~$]MTt2_76;c'E(]߁ViM紑?I֮d[s\6eS X‹EJ (_ w Ƅ78B 5>x򟛐~}"?|4z2jQ/Ԋ)(l}d8艹.LJЇ;Nإ n G܏ѧGw>':pw|>3"vO.1ZvEǚ :0;pPydmEsfrҺJv?)8/SPZ_Ͼj*_zf~w*G s ޜJ~BS2pz{]qͫ*Wo J.9*WYߝ?~LAтFyn]2+`\JR˴&N[΂[ϳ7_A9N^~MrV&JJSRE_Ϝ6ܳ(_Uk]+#m{l jݾaQ m(Rb$!g&V^tj#SYFؤ@8ʠFtrHlvtByr|zo4\>" ^D/s"嬄,v{_dfdI˚GgaA\6/C+Fi6 D|80y{%Ap}h2<=KI3ڈX֗u>qymLӏAUFxFڏ92C|aG7Y$==БgR H&GHdwpZvHE&i"b[ΒsM\ScU'$0Х̐ ۛ*VqYy2$Ԯ)&LdEdkYWH\f8sYV[U}4;R+Н)#ָ֠#',tY/-( 0Kh÷gZHڰ!1̏ P;Ƞ?2H DS0 aq̛osë7U槀uYiPgL(1C\Ss5 '0^Q>Q(!aesS__(:ʯɾk$ N5ꊊ%/Cu6]V(IJ yP9֊hRF@sg \o>IlegI4сa),c9gfI(*XhNd^bA:pgCqηzsI]ыҪ[DY→Pw kq\PBi-BQ$iMOD/.EuR`qs}~}sZzbvxw|rx3{{~d{bbP;o3>aιx<IwSx&xzМR[U{0x4F{R~XZ}y@A<{V{WZ~}K7v w[xGRw^misxqun}uAǿ:.+hfn!6^/tJLo`E h endstream endobj 778 0 obj [/ICCBased 779 0 R] endobj 779 0 obj <>stream HyTSwoɞc [5laQIBHADED2mtFOE.c}08׎8GNg9w߽'0 ֠Jb  2y.-;!KZ ^i"L0- @8(r;q7Ly&Qq4j|9 V)gB0iW8#8wթ8_٥ʨQQj@&A)/g>'Kt;\ ӥ$պFZUn(4T%)뫔0C&Zi8bxEB;Pӓ̹A om?W= x-[0}y)7ta>jT7@tܛ`q2ʀ&6ZLĄ?_yxg)˔zçLU*uSkSeO4?׸c. R ߁-25 S>ӣVd`rn~Y&+`;A4 A9=-tl`;~p Gp| [`L`< "A YA+Cb(R,*T2B- ꇆnQt}MA0alSx k&^>0|>_',G!"F$H:R!zFQd?r 9\A&G rQ hE]a4zBgE#H *B=0HIpp0MxJ$D1D, VĭKĻYdE"EI2EBGt4MzNr!YK ?%_&#(0J:EAiQ(()ӔWT6U@P+!~mD eԴ!hӦh/']B/ҏӿ?a0nhF!X8܌kc&5S6lIa2cKMA!E#ƒdV(kel }}Cq9 N')].uJr  wG xR^[oƜchg`>b$*~ :Eb~,m,-ݖ,Y¬*6X[ݱF=3뭷Y~dó ti zf6~`{v.Ng#{}}jc1X6fm;'_9 r:8q:˜O:ϸ8uJqnv=MmR 4 n3ܣkGݯz=[==<=GTB(/S,]6*-W:#7*e^YDY}UjAyT`#D="b{ų+ʯ:!kJ4Gmt}uC%K7YVfFY .=b?SƕƩȺy چ k5%4m7lqlioZlG+Zz͹mzy]?uuw|"űNwW&e֥ﺱ*|j5kyݭǯg^ykEklD_p߶7Dmo꿻1ml{Mś nLl<9O[$h՛BdҞ@iءG&vVǥ8nRĩ7u\ЭD-u`ֲK³8%yhYѹJº;.! zpg_XQKFAǿ=ȼ:ɹ8ʷ6˶5̵5͵6ζ7ϸ9к<Ѿ?DINU\dlvۀ܊ݖޢ)߯6DScs 2F[p(@Xr4Pm8Ww)Km endstream endobj 785 0 obj [/Indexed/DeviceRGB 255 786 0 R] endobj 786 0 obj <>stream 8;X]O>EqN@%''O_@%e@?J;%+8(9e>X=MR6S?i^YgA3=].HDXF.R$lIL@"pJ+EP(%0 b]6ajmNZn*!='OQZeQ^Y*,=]?C.B+\Ulg9dhD*"iC[;*=3`oP1[!S^)?1)IZ4dup` E1r!/,*0[*9.aFIR2&b-C#soRZ7Dl%MLY\.?d>Mn 6%Q2oYfNRF$$+ON<+]RUJmC0InDZ4OTs0S!saG>GGKUlQ*Q?45:CI&4J'_2j$XKrcYp0n+Xl_nU*O( l[$6Nn+Z_Nq0]s7hs]`XX1nZ8&94a\~> endstream endobj 603 0 obj <> endobj 604 0 obj <> endobj 605 0 obj <>stream %!PS-Adobe-3.0 %%Creator: Adobe Illustrator(R) 13.0 %%AI8_CreatorVersion: 13.0.0 %%For: (Kelvin Wong) () %%Title: (pylons_as_onion.ai) %%CreationDate: 7/18/08 6:27 PM %%BoundingBox: -7 62 706 564 %%HiResBoundingBox: -6.44434 62.5 705.5 563.9785 %%DocumentProcessColors: Cyan Magenta Yellow Black %AI5_FileFormat 9.0 %AI12_BuildNumber: 406 %AI3_ColorUsage: Color %AI7_ImageSettings: 0 %%RGBProcessColor: 0 0 0 ([Registration]) %AI3_TemplateBox: 400 300 400 300 %AI3_TileBox: 111.5 -55.5 687.5 678.5 %AI3_DocumentPreview: None %AI5_ArtSize: 14400 14400 %AI5_RulerUnits: 6 %AI9_ColorModel: 1 %AI5_ArtFlags: 0 0 0 1 0 0 1 0 0 %AI5_TargetResolution: 800 %AI5_NumLayers: 13 %AI9_OpenToView: -94.5 634.5 1 992 699 2 0 0 50 75 0 0 1 1 1 0 1 %AI5_OpenViewLayers: 3323333333333 %%PageOrigin:-66.3003 -266.2988 %AI7_GridSettings: 72 8 72 8 1 0 0.8 0.8 0.8 0.9 0.9 0.9 %AI9_Flatten: 1 %AI12_CMSettings: 00.MS %%EndComments endstream endobj 606 0 obj <>stream %%BoundingBox: -7 62 706 564 %%HiResBoundingBox: -6.44434 62.5 705.5 563.9785 %AI7_Thumbnail: 128 92 8 %%BeginData: 8930 Hex Bytes %0000330000660000990000CC0033000033330033660033990033CC0033FF %0066000066330066660066990066CC0066FF009900009933009966009999 %0099CC0099FF00CC0000CC3300CC6600CC9900CCCC00CCFF00FF3300FF66 %00FF9900FFCC3300003300333300663300993300CC3300FF333300333333 %3333663333993333CC3333FF3366003366333366663366993366CC3366FF %3399003399333399663399993399CC3399FF33CC0033CC3333CC6633CC99 %33CCCC33CCFF33FF0033FF3333FF6633FF9933FFCC33FFFF660000660033 %6600666600996600CC6600FF6633006633336633666633996633CC6633FF %6666006666336666666666996666CC6666FF669900669933669966669999 %6699CC6699FF66CC0066CC3366CC6666CC9966CCCC66CCFF66FF0066FF33 %66FF6666FF9966FFCC66FFFF9900009900339900669900999900CC9900FF %9933009933339933669933999933CC9933FF996600996633996666996699 %9966CC9966FF9999009999339999669999999999CC9999FF99CC0099CC33 %99CC6699CC9999CCCC99CCFF99FF0099FF3399FF6699FF9999FFCC99FFFF %CC0000CC0033CC0066CC0099CC00CCCC00FFCC3300CC3333CC3366CC3399 %CC33CCCC33FFCC6600CC6633CC6666CC6699CC66CCCC66FFCC9900CC9933 %CC9966CC9999CC99CCCC99FFCCCC00CCCC33CCCC66CCCC99CCCCCCCCCCFF %CCFF00CCFF33CCFF66CCFF99CCFFCCCCFFFFFF0033FF0066FF0099FF00CC %FF3300FF3333FF3366FF3399FF33CCFF33FFFF6600FF6633FF6666FF6699 %FF66CCFF66FFFF9900FF9933FF9966FF9999FF99CCFF99FFFFCC00FFCC33 %FFCC66FFCC99FFCCCCFFCCFFFFFF33FFFF66FFFF99FFFFCC110000001100 %000011111111220000002200000022222222440000004400000044444444 %550000005500000055555555770000007700000077777777880000008800 %000088888888AA000000AA000000AAAAAAAABB000000BB000000BBBBBBBB %DD000000DD000000DDDDDDDDEE000000EE000000EEEEEEEE0000000000FF %00FF0000FFFFFF0000FF00FFFFFF00FFFFFF %524C45FDFCFFFDFCFFFDC4FFA8A8A8FD7EFF522727527DFD6FFF7D272727 %5227272752527D7DFFA87D2727277DA8FD6EFFA8A87DA8A8A87D52F82727 %527DA87D7D2752A8FD6AFFFD04A87D7D527D52522752FD0527527D7D2727 %A8FD62FF7D275252527DFD04522752275252FD0627522727275252272752 %FD63FF7D52272727522752527D525227272752527DFD04522727F8522752 %277DFD65FFA8A8A8FFA8A87D52277D7DA8A8FFFFFFA87D7D7D5227527D52 %7D27FD68FF7D5227527DA8FD07FF7D272752A87D27FD04527DFD63FFA852 %52275252A8FD0CFF7D2727A87D525252277DFD63FF7D7D7DA8A8FD0FFFA8 %52F85227FD0452FD79FF522752527D5252FD79FFA827FD045227FD50FFA8 %A8FD28FF5252527D5252A8FD4FFF7D2752A8FFA8FFFFA8A8FFA8FFA8FFA8 %FFFFA8A8527DFD14FF7D27525252277DFD4FFF527D52A87D7DA87D52527D %7DA87DA8527D7D52A8277DFD14FF7D52527D52527DFD4FFF7D2752A82752 %5252FF527D7DA8FD0652A852A8FD14FF7D27525252277DFD4FFF52A8527D %527DA8527D527D52527D7D527DFF277D52A8FD14FF27FD05527DFD4FFFFD %05A852FFA87D27FF7D7DA8FF52A87D7DA8A87DFD13FF7D27FD0452277DFD %58FF52A8FD1EFF52527DFD0452A8FD77FF7D27FD055227FD77FF7D277D52 %52527D277DFD74FFA85252275227FD0652FD0CFF52277DA8FFA8FD05FFA8 %FD05FFA8FFFFFFA8FD4EFF7D7D52522752527D52A8A8A87D7D527DFD0CFF %52FF52A87D7DA852A87D5252A8A8527DA85252A87D7DA87D52FD34FF524B %52FD10FFA87D5227272752527D7DA8A8FFA8FFFFFF7D5227FD0CFF5227FD %0652A8527DA82727FF525252A8527D52A8275252FD33FF52F827FD0EFF7D %522752527D7DA8A8FFFFFFA8FFFD04A8FFFF7D27A8FD0BFF52FF5252277D %A8A827527DA85252A87D527D7D7DFF5227527DA8FD33FF52F8F8FD0BFFA8 %7D2752275252A8A8FFFD09A8FFA8A8FFA8277DFD0BFF7DFFA87D7D52A852 %A8525252FFA8527DA87DA87DA852A87D7DA8FD33FF7DF827A8FD09FF7DFD %04527DA8FFFFFFA8FFA8FFA8FFA8A8A8FFA8FFA8FFFF7D27FD14FF7D7DFD %42FF77F8F8A8FD06FFA87D27FD0452A8FFFFA8FFA8FFFD07A8FFA8A8A8FF %A8FF7D277DFD13FFA8FD43FFA8F8217DFD05FF7D52277D52527DFFFFFFA8 %FFA8FFA8FFA8A8A8FD05FFA8FFA8A8FFFF5252FD57FF7D21F87DFD04FF52 %2752522752A8FFA8A8FFFFA8FFA8A8A8FFFFFFA8A8A8FFFFFFA8FFA8A8FF %A8F87DFD15FFA8A8FD40FF27F852FFFFFF27FD04527DFFFFA8FFFFFFA8FF %A8FFA8FFFFFFA8FFFFFFA8FFA8FFA8FFA8FFFF7D27FD15FF52A8FD40FF27 %F827FFFF27FD04527DFFFFA8FFFFFFA8FFA8FFFFFFA8FFA8FD04FFA8A8FF %A8FFA8A8A8FF7D277DFD13FF7D21A8FD40FF52F827A827FD0452A8FFFFA8 %FFFFFFA8FFA8FFFFFFA8FD07FFA8FFFFFFA8FFA8FFFFFF5227A8FD12FF27 %27FD41FF7DF82727525252277DFFFFA8FFFFFFA8FFA8FFFFFFA8FD09FFA8 %FFA8FFA8FFA8FFFFA8277DFD11FF52F852FD41FFA827F827527D52A8FFFF %A8FD05FFA8FFFFFFA8FD0DFFA8FFFFFFA8FFFF7D27FD10FF7DF8277DFD42 %FF27F8F852527DFFFFA8FFFFA8A8FFA8FFFFFFA8FFFFFFA8A8A8FD05FFA8 %FFFFFFA8FFA8FFA8FF7D2752FD0EFF7DF8F8F8FD43FF52F827277DFFFFA8 %FD05FFA8FD07FFA8FFFFFFA8FD07FFA8FFA8FFA8FFFFFF5252FD0DFF52F8 %27F852FD42FFA82727F827A8FFA8FFFFFFA8FFA8A8FFFFA8FFFFFFA8FFFF %FFA8A8FD04FFA8FFFFFFA8FFFFA8A8FFFF7D27A8FD0AFFA827F820F8F87D %FD42FF52522727F87DFFFFA8FFA8FFA8FD07FFA8FD05FFA8FD05FFA8FFFF %FFA8FFA8FFA8FFA85252FD09FF5227F827F82727FD42FFA8275252F820F8 %A8A852A8FFA8FFA8FFA8FFFFFFA8FD07FFA8FFFFFFA8FFFFFFA8FFA8FFA8 %FFFFFF5252FD06FFA87D27F8F827F827F87DFD42FF7D277D5252F82727A8 %F87DFFFFA8FFA8FFFFFFA8FD0DFFA8FD0AFF7D27FFFFFFA8A85227F827F8 %27F827F84BFD43FF5252527DFF52F82727F8F8A8A8FFA8FFFFFFA8FFFFFF %A8FFA8FFFFFFA8FFFFFFA8A8FFFFA8FFA8FFA8FFA8FF7D27527D2727F8F8 %F820F827F821F8F87DFD43FFFD0452FFA827F827F82727FD09FFA8FFFFFF %A8FD07FFA8FFFFFFA8FD06FFA8277D7D27F8272027F827F827F82052FD44 %FF2752525227522727F827F8F827FD05FFA8FFA8FD04FFA8FFFFA8FFFFFF %A8A8FFFFA8FFA8FFA8FFA8FFA85252FFFF7DF827F827F827F8F827FD44FF %7D525252A87DF8F8F827F827F82027FFFFFFA8FFA8FD05FFA8FFFFFFA8FF %FFFFA8FD0BFF5252A87D2727F827F827F82727FD45FF7D275252FFFFA827 %F8F827F821F8F827A8A8FFA8A8FD06FFA8FFA8FFFFFFA8A8FFFFA8FFA8FF %A8FFA8FFA852F827F820F820F827F8F8F8A8FD45FF7D52527DFD04FF5227 %F827F8272020F87DFD0CFFA8FFFFFFA8FD07FFA87D4B27F827F827F82727 %272027F8A8FD46FF7D275252FD05FFA87D27FD06F852A8FD07FF7DCAA8A8 %FFFD05A87D7D52522027F8F8F827F8F8F8277D7DF820F87DFD47FF7D5252 %7DFFFFA8FFA8FFFFFF7D522727F8F8F8277DFD05FF7D21FD0627F827F8F8 %F827F827F827F8F8F82727A8FFFF52F820A8FD48FFA8275252FFFFA8A8FF %FFFFA8FFFFFFA87D5252272727A8FFFFFF77F820F8F8F820F8F8F827FD07 %F82752522752FFFFA8F827A8FD49FFA852527DA8FFA8FFA8FFA8FFA8FD09 %FFA8FFFFFF52F8F827F827F827F827F827274B52527DA8A8FF7D5227FFFF %7D52FD4CFF275252A8FFA8A8FFFFFFA8A8FFFFA8FFA8FD06FFA8FFA87DA8 %527D767D527D7DFD04A8FFA8FD04FF7D5252A8FFA8FD4DFF5252527DFFA8 %FFA8FFA8A8A8FFFFFFA8FD06FFA8F8A8FF7D7DFFA8FD07FFA8FD05FFA8FF %7D5227FD50FF5252527DFFFFA8FFA8FFA8A8A8FFA8A8A8FD06FF52A8FF27 %52A82752FF527DFFA8FD04FFA8FFA8A8FF7D5227A8FD4FFF7D277D52FD07 %FFA8FFFFFFA8FD07FFA8A8FFFF52FF7D7DFF5227FF277DA852A8FFFFFFA8 %FF7D5227FD50FFA8275252A8FFA8FFA8FFA8A8A8FFA8FFA8FD07FF52FFFF %7DA8FF52FFA87DA87D52FFF87D52F8A8FFFF7D5227A8FD50FF525252A8FD %06FFA8FFFFFFA8FFA8FD06FF7DFFFF7DFFFF7DA8FF7DA8FF7DFFFF7DFF52 %7DFFFF525227FFFFFF7D7DA8FFA8A87DFFFFA87DA8A8FFA8FFA8FFA8FD3B %FF7D275252FFA8FFA8FFA8A8A8FFFFFFA8A8FD06FF7DA8A8A87DFF7DA8FF %7D7DFF7DA8FF7DA8FF7D7D7D2727277D7DA852F8F82727F827525252F8F8 %F85227F827F852FD3CFF525252A8FFA8FFA8FFA8A8FD08FFA8FFFFFF7DFF %FF7DFFFF7DFFA8A8FFA87DFF7DA8FFFFFF7D275252FFA8FFA8A87D527DA8 %A8A87DFF7DA87DFF7D5252A8A8FD3CFF5227527DFFFFA8FFA8FFA8A8FFFF %A8FFA8FFA8A8FFFFA87DFFFF52FFFF7DA8FF7DA8FF7DA8FF7DA8FFFF5252 %277DFD0BFFA8FD47FF275252A8FD04FFA8FFA8FFFFFFA8FD05FFA8FF7DFF %A8A8A8FF52FFFF7DA8FF7DA8FFA852FF7D5252277DFFA8FF7D527D527DA8 %7DA87DA87D527D7D277D7D52FD047DA8FD39FF7D275252FFFFFFA8FFA8A8 %A8FFFFFFFD05A8FFFF7D7DFFA87DFFA87DFF7D7DFFA87DFFA87D7D522727 %27A87D7DA8A82752F852F8527D7D275227527D27F852F852275227A8FD39 %FFA85252527DFD05FFA8FFA8FD09FFA87DFFFF7DFFFF7DFFFF7DFFFF7DA8 %FFFFFF527D27A8FD55FF52275252A8FFA8FFA8FFFD05A8FFA8FFA8FFFFFF %7DA8A87DA8FF52FFFF7DA8FF7DA8FFFFFD0452FD05FFA8A8FFA8FFA8A8A8 %FFFFFF7DFFA8FD44FF27525252A8FFFFFFA8FFA8FFA8FFA8FFA8FFFFFFA8 %7DA8FF7DA8FFA8A8FF7DA8FFA852A85252272752A87DA87DA85227275227 %A82752272752F82727A8FD43FFA8F8525252A8FFA8A8A8FFA8A8A8FFA8FF %A8FFA8FF7D7DA8A87DFFA87DA8FF7DFFA8FF7D52275252FFFD06A87DA87D %7DA8FD057D527D52FD45FF7D277D527DFD04FFA8FFA8FFA8FFA8FFA8FFA8 %A87DFFFFA8FFFF7DFFFF7DA8FFA87D527D52FD08FFA8FFFFA8FFFFFFA8FF %FFFFA8FD47FF7D2752527DA8FFFFFFA8A8A8FFA8FFA8FD04FF7DA8FF52A8 %FF7D7DFF7DA87D522752277DFD07A87D7D272727525252F8F827F852FD04 %27522727FD39FFA87DA8FD05FF7D275252527DFD05FFA8A8A8FFA8FFFFFF %7DA8FFA87DFF7D7DFFFF52522752277DA8A87DA87DA87DA8A8A852FD047D %A852FD057D527D527D527DFD39FF7D27F82727525252FD052752527DA8FF %FFFFA8FFFD06A852FF7D7DFFFF7DA8FD045227A8FD59FFA8A82752275227 %7D7D522752527D7DA8A8FD08FF7DFFFF7DFFA8FD0652FD0BFF7D7DFD07A8 %7D52A8FD047DFD04A87DFD3AFFA87DFD052752277DFFA8522727FD04527D %7DA8A8FFA8FF52A8A8522752FD0427527D7DA87D7D7DA8FD047DA87DFD04 %27F852F8A8F827F827F852F827F82727F87DFD38FFA8522752527D275227 %522752A8FFA87D525227FD06527D5252277DFD05527DA8FFFFFFA8FFFFFF %A8FFFFFFA8FFFFFFA8FFA8A8A8FFFFFFA8FFA8FFA8FFFFFFA8FFA8FD3AFF %A8FFA8A82727F82727522727277DA8A8A87D525227522752275227272752 %277D7DFD10FFA8A8FD07FFA8FFA8FFA8FD43FF7DF8527DA8527D27522752 %2727277DA8FFFFA8A8A8FD047D52A8A87D7DFFFD0FA8FF5227527D525252 %A852275252522727FD05527DFD3CFFA8A8A8FFFFFF7D272727FFFFF8FD04 %27A8FD07FFA8A8FFA8A87DA8A8A87DA8A8A87DA8A8A87DFD05A8FD06527D %7D2752527D275227522752527DFD42FFA8A852F87D52FD042752F852A8FD %05FFA87DFD6DFFA85252A8A8FF2727277DF8277DFD05FF52FFA8FFA8FFA8 %FFA8FFA8FFA8FFA8FFA8FFA8FFFF7D52A87DA8A8A8FF7D7DA8A8FD54FFA8 %A8FFA8272752FD04FFA87DA87DA87DA87DA87DA87DA87DA87DA87DA87DA8 %527DFD0527A8FD0427FD59FF7D7DA8FD19FFA87DA8A87DA8A8A8527D52FD %FCFFFDFCFFFDFCFFFD2AFFFF %%EndData endstream endobj 607 0 obj <>stream %AI12_CompressedDataxis%Ǒ ?`?hCf,%DiZ1TR5]9~xGª8Eͷ_n^ݗusvn?{+^O>G/~U OC_MɃWD<޽˫W_\ wzn?|-|^uo香_v7o}zW_WCw56U"yۻuu'zؿ{ۏ߼Çݻ۷Wr{/w_u9{O_ݾۻ>}˻*}w|Vǫmn뗿OwGFݯۻa\~Gx+FXw7?զMs?.Om}pi:Sy-ͻw<߾_06b?巟^߽WB4͙w/^[6wa߽p72~unkwsi{50W 5WcW:v׎ ؑ߿_~9 װ՗ĻO_̛6vWGܿYa!>~{+kȲo_޽gv- w?g |ӏᏡoӻwwWpۿ]ty==fׯoo/>}Gػp{-?rKܾxw~sR/;d޹G_qWW[>ܼgܳw޽pQO^x{\|xs^* yG ? Зwon ^wǗwl7_x}@?}~~͛OKpX=߽tËO}t'_׷oGW/n_Sé~v=ֽ{.?x%gw7ތ |;ܼCyts"S Ie8_+D[4[;zN~`‚=^"^sw6@w>CуyØqxi~_yt=+~k~kןk~}7?ĝՍ4oě߁c/@n?y}#C"-|.@5~.|:}_^}K+o֏ݬ?wh]c;rƯڱO|ɝ@\z(p3Kh"n_|707tO/L?"7w?^vN7@P]xs#|et{ JFd8wW]l׀}ӛ?{ x56 qx߼>߿ `ۏƿ^'ի1 ػ8x >,ӷ^}o_ jӇ?_ݻ6O6Lx7_^IX ӧ{}w ]ݯ^^P{.~%Wdltox}Or e#_^ pe]u%:y߽Eo| 㟏۷@ n]"zV$Y '}_~y&v?!]DZ'x; u0j;4f=Cۀյ=&yFhsiv=vMv]wKЍݦvn|cMw?$S縉۸xHMjSSJcҜ6ifh~Caa3l]GL 'Si`*Iڷj81Vyv詡 j qynd0xę~Á'ShmQ&S`[36'S ,PRb`8ʶ_[m4c{l~9̇0C:z@6?~>w;amwݼvnإ]lv~nvئmNU9nfn6y3mͰIa7۴f>·y??~mn g@813N#5>i!X#Œ@X,pD@?@ G؇uP"@%@-@7&! hI׷䞨(ԍHQzq*jT P/{h!mQw%w*;3DC4V|cVr0sFA@k pnܛ {ر# H x& px>쎀D->N a 8q<}hwzSy(؛w!Z.}xrVm9y3xgbQ;Ij|KX!b%ʗpa)-".y f>.`!zb + %WqZ2 [" DDfZ$^ cU⯎?z%؂mk^̺$a]x6%W-R0`%mK:[ [`2VbK+̳mpFY7XFWB98\u`ܡUiDhFZeDB|`m/mGn0ljmmpwӍ*Ote^t8zg*_{t_jsz5f[XpAz8L1-TDcW;Yd-NaI(BW v/cH|Kq߂#w]GpGql"h`$ TBS򱆙y8xS VtaxTǺC2\r-厹%L ޔVc&s-1sF+?0*ߛgS&\geo]BO c1VrOch!LFv~cj86 Mx5io?|=n?v꓍ܘy*zIu*ױ<'w!N=8,ۑ Mӥ#v'qa ½{^{ylO0԰+XJԫPAV숭dk,>UuPJ׬7VYT`6ymCԷ zag[عo]L ~O ?JA/yb'Loiwrۄ1\"obx]g{G|ո_d/.<fx3ir"y7 M%IToW!7}RN$ؾ";)L*&.DlIX!һoLR*kɴc<[A̶WQk[ȿ3/k6 ];؋E )5Rd=<<šJf邱Hc@%xuw {Wr,ְQ6gT%i9%J&P*Tb٭́&gs/g@Q#ϖ<Ŗ')iF+ E#k=D%b&uARHm#Z«^0kCqٌ]{'Q/r 0zuGŬFuV 2nmHEn t2i?L-eƖ6BaQ@]F qm qʩOZYcuiA_9KXhҢ EiӲ5Hn޲mNmق|#17#?Xv~q6q 4s .JڢZg߅fҖvֶm\qmtmp--ύ|+YlmvU}mOMق|#sI}S~RYVi@N&"$B2d터JUbDU#a+HXEJqKdm4 OiJOteVaCWDsE:|[fsǪ7~N/Xeۃ4Z6 T_~?|ՅSvgH3V XC:!d`eHP D]5E! ?R8=dWHgo]uD[ծPq^;X/Hs.߼?J^7'9/<;8ko. ?nNJ3oūOጫʼnS4g;| 0o sgVϫA)Q> -u{Ђ&8;XC\kwuК%YJZq0F8 y 5A*c?^?DԡH2tQh7n| oH&pY5$8ѢZ~􋞿S`*%ZL[cU$7EbՋ֩lj*Q soᒇVW8Nje Kc[g5 [7| >n~0 Xm\tAA~khs^h ui òo6, #+8R2ՑKkT$j LcSI֕g\Slzl njtU[\uA*eZtƾdy*X(%ÌMU1a UaEO-Z8"k`'";l9,s>L\-炥s><η ؖt`1Np qrvщdayJI[;ӶB?|wolO!v<שmaݣ1hC[1\mŮ dt WrP|^=|x׳n:x}}}}}}}}}}?K#}׳5ﶯz}WH}RbSSr~]\YLĥaQ]/]6b%j$Uɰ6S Evo8e# 1,Z2Y ןk.9frLlQCjx:Ξ:x"-hl)[1q9NcMa+& 82 ̀BlhjQ`"{Ș-:INpK Y4#n{uWwo^q^bf Oi|=ȩDjSv5sB'tHc:B>aJUlM7NcQn)uLPW%z=;*kWui8m{(bw̜iC(XM:KB ۔Tm P)kDɜ|7JGw ސxp=uo.3;]Qi:bQHTK?gd"eh_Vb0ޕԬA~'P8T\w.d(:F 6 7w@ P[c!/.Ň8{D5$_NX@e <`cvmD4Q zn[+/εihAxj ;Ґ9%z3Y$f% hnْu҈j|TvI|ȏ{;nŋ{0.:ȮE\-XPtiAlE7T{ZY:]-- |撐Vǚ+&!sIGə%"{goSnYCeUЃD/ɜ{( EDQKwAA; pte=9? O]9l2&A]ENXkfm4J՜ƅY0Bay;{I:H㼆yOy_z{wm^pM߈u&DJבܢx%$xv$|ZPT5jq=|.ع(y~:;{pU/>b 'ǟ%㳨YukQU)e+u43.)EPM:e1Ҫ * ":>Cf7ˁy` +Eۃ mP=gjGsw!n{צŢaۣ0hu:hٯ٦7qE\/e,sedΖ2@ՎX^ʒ,2kgH`np墮Oq\^bI tMtCіnCXY׊ցYT;Owt߈l tG2JW9DAډOl2L(=:z0ڑ.yԑ9ֵ`tJX+<믌 &'$]d]P!WSu øH28w#GVGoƒ4^x^l9Ԝ53[ꙛ(sc.f:hP̑Q4ZH}((A7ჸvl1B 3^sC5~r'+,3Fѝ1J->!<\*&!'(G5q1=5#<5Oc1-<oFTkBytmfz!jkf:HDZ}xWS\ qvhJpA–X*n1yW超70T2xxym.td%1Ano٠r?ȱnf93fNjVUq&(H jV*ԲmIeʪJc9ida yyuW05&Bo*̻}pŝFI ˜*gj˳X >y.fj"K Ls;c#wFrbe CRg|]Zt]P ]9P͜JZ~QNLӶEy |a@NNya;zs2-3o/_X2&&$#ME"ً3YMNv؅_Y/^OnSOms}8OU"!d*6чWpIx=V%OSvY7S.3[]!iuΫ, /#ָ.^h4"ӣ!`䎝%%j,æIŒ/p^e]iZk^|ak.sM3pY<_&59;c#OˑkRLj4ܩf6Fagx . 9K$oE(7=!"aR͈cjp /LKs 8 b.UҐ.RC W'6ʹc˜4Ϧ\Z%rFMvVVYN>]t܎\&7 y\ YҬR[+'C~2Ia3YLYG+*v"<;i<:YVi,&tZi`mYȑ<2x}6&霂*["Bvpr^,V UGZq: *. Z6*7lh.[V8_Ҧ-2U8ԯA%V=u8W9#:lAA؜Y{~Jn{  zP]½BάD(.Z:qa0}6{J">{,WMai%/J%פG_7}?} }2S3a_]xC2a?硫yJO [!| (9X{5^P  ~٪]E*$&qK,K-vr.ЌT)S fɆAږ*&py'z3 ,3"Qr$ԱDc׹mB]pY  vcm\ʕgISYVIqNdֵ ul=3}:qgtδ:Ö =,i;"^N~REN &\D9%/r[, 16`"/Yr[یݗrbO&%eCk"'m3$ˀl+:? 5(N>T[Y1Vq-@ӨB˲ z.Q4VP5(@ddd Y3T=P5ig띲oI{ Ekh*hZ QȎQK?=jXP .z )jN'mO+e.kVM8֜nB,AV_ˋh[Ui 륉eňâ䰻JV, ODMc,MY\gISV=ήT- 9H `>>EQe NU6iQc}%yuw5שUG\ 'ꐴ6kXn,'k#)'{]E2i{V21=IӸ.}e* a>dOޕsh,ggY"zK9 ФsxRNJTeIЃz4ds '4O NK _zs4P Ӡ~}bIWt|5&~7i:Յ7n+!`-oa}F_7Z-.Vm0\x`r]ZE-FݣYhK\ %!S*sd`*F9 [ zrêř6RSA Y*aQpR O1{*N's꜕F5=ߓo¤vֶPz\QQ OfjnX6YlWfmecF-y,_d g}S|o&W5qۇl3Q(fľkOMYʎVF3J?v&UQt n؉wwg[ b_ g.CmCPEӈ[W@V(Y6X|@RSi$;03}yIJr(Y( e&nc[sfJi?wr/^\`&O6AK)@K|+^[GNz-dԓ$- ^+CG')֡ h^9N}3_wխֲ#|h7' Nr`2嵗a{:LVײM .CYmsr+7/>1Vf3[Caksf8[ 593+RrKyWmϹDPpD) fAsJjs֕zQ -R|rtu%i--~K/2][iFXVYՄKU)' 0ןi.H秳k?ť[_lV3S3^'2u?EgWYsvtEd,r%{, +5KzE0Ro -+.|Oq}ibg<{3CV&Hz̓/.nV)[@ Dxrq=_n,ݫu`\ENq$X#m~*RhzɈj@E.Vc 8V?W'vlVFy,ry9x\(Gz=2Ť &.ifi7ę 9WL[$)>RK'Y״V"jLjYe#LHE.$?,Px%ˢ@׷x%n\$JD{K.SٙvDz|| ar7ou+,q5$ZvpډfXy%*o{*wUyɻ ߘUId9Sl*K\KO+\$g&p]UNbYm[)Q=%5òe> +)`.s|].# y',Iq8Ն.fi( ɜj(,ueiX^5]0`%rS"՘_V&w6dIJ(JEXiX 4;G}ZT7P/I}'xEB<I): zҗ9)(i]ũ3Ҽ"CY4dyъ r&]NpkA:/YSq7ʵIb._ z <ߒquܷ]WgQ\(c2RsDw67;Nf[\eK*n)\r-t)3ƊrI#ʂH{Wt6&{®#3$sf`eEN -g Ҧ.ZCj RűIXYm:.z rZ'sǑbGu*.K=QD\5_Ӥim;ZpT8͏wl%\"o2~my\jX}x|C:U6z H@AD MZpW^xӣb DR32.*sPz;v .cc.BQd0m,ఆUTWV̂rKFBm9Yvڗ^ '} )$qArXx(w.a;/3oT!iZٝIIi*fqؗ\k\>k֬WD'5L;Mnɒdb[̓Y;W^50F]Q~JV¼[R ,Ut7b/:pq+ .Ny =EgjSUMEj:j@e*>"x%R"PLzA5k]|=C68䋒#E_pu)%ܣZVb}tmXqѦU2>mmhc=Vryo ɭA:]ϘKU|/>Ukwm}_ wK$}~ce l?)oEP)DicF ߊAzh}b1{Zc<$POcf'c,5\Ͽ[r0~>BtFH?49V>>H@Һf{[I$0nbG, >JilQ}ڭX#Y8G}Js6bj\,t0 m' Y.l"jWl=kEi:;verm' ѳ`,]uA-N$ZX@Z4r;J^j T^D`"["{pk"5)"%GEL,j(jIQHˈ- JOߑQf>LzD-akm +O[YK)V0q&-P*|*Jۜl۲FLRi7}ٻloLW_K-kڄ94ޙEkUXXE#_p&詐2ˇ~)D-ba8Ř?,$:q?c f9Uk]իZg 4>QKXWJuJj_QUbMU5V%E;V,ڶ\~Q`۞L*l*x$#u\`f=|q ";*rˆwBe]8!6uOBU ZUիf@֮~5C 8Qj%Eh*@Ma0two"۬l!>/Ez[^Ktp>wX}wnw[i|-jl_Kpb87[J{|kx>xwg:u<ٹ,i:v$10otv3U ǔ(Pބ=nhAV!`õ;mh͒Ah%-TuD8N@#`[7| њm\tAA~&Cb 9ڋ3ל?},OlBW x?/m*iCa \O.F s2O(D(Փ]YPcQWuvE}^BG.{@Y:%Bm.$ Rb]A0A.Y{'vt=A Jz4ר۱(YߋzqzV0Y& P22*MܖA|.h/n}\jek-Ԁ`.h%BӃ4]hj eX^^Ғ\zuoJU^ep˥z Zj/K;е+Yi0Ta̙EZ̳z̬ +Ml̥j4MiL1u,5b Y`*śCV 2JaOxWFVqeU NO.46d]y5ŶǶ 薭&Ng^Ÿ5X@"^F|{Iк5S t%dcPsasᏚ_o5 "ݎ=}[v;6r|.uN8-%<|GZp CwbGt"G9W܍G  ~SO!v<שmaݣ1h1ڄ+`N,0 3.St(ּ C{xLd )GTOL⺚ Y5$f,YJ!sf UGH,U"rhI_mDTۙ#k u&'wDZP)&Q)JESEV1X_VljűT+dlt;:.]A310{g>#G\cښ b!22xtSS)}; <[2Rdt4 G$0S'S/b"q~BQ_,UqmxY&Du6*o=2fVC{b6emPQU{dqlo[|T+٪wx1`|}f'hbtO{ c&jԕZ$~]"DN .z.1rmaW+\Nzk^0<.V^Sy"h>AU!5f.?OKR .q[xՙs2*KuahC{}w/Px]^32gԗ} .}՝]Wnu%E&(+g@xFUO[vVkzZCR.P+prݎ~wr;L|bw˽Y9e_)U; Y:q9/y+{Zj!FF_UL RQӇEM%U´\^4ݱ=K`岗M&[*=E{zXͅȏʋ a Y>IqVC!'^,%OR 9 "w+1CH<8$š\ˊ2,EqCQ'X8'"I >vr~Kq%=xP8 IMX";FQ6&!qV$0D5_m"d#Yxe_$ϗ5aNa>Uf8ԕLi"z )$Q*P/QRMx+q+Mu/)1L˃Nd[igvbl[(HDzӒ JJǎ4MwlѴD'UvҔ4zדH \FK\Z«ҋ~ān-H"f9'3&Qmhj 0XIa;"û nN)^&=2G>Ә{bZmS.·Ò5v3 @25KZ{,FG5[akrkħcЯ>K!s.BE5q캱g&_[q̽&xM2 'yLeŷmʲAEmgtik#YX0ى׷+r^%5,VuIՂ!}2*;lBg@y Rywb;J1`mF"+61S|gjMeꦖzp~7m&5TrpE] QbK NZCkfD{VIA4juāE $xzcXc$8BٰGu;2[% W0VvEqM;/ : ʪD)͟ҲF;q$֦҂|{Vz@Tc0lrpO`RTKG]%-CۊK\KNqW_;6#_s\q?ڊB`/LUq=ۖe:쬐;{ݧIYQˌ3{oJO1{z j8_EڔUl $ J@=bJY1 "*%[GA~b;kͳVg] eI#{$o{SmЦABOEkkݢ9#ioXRm?UG7ߝP֦tgݟvW޳sjyA45U}ٕr6,rˊ;dac' ML "E5G0<[/Ho۲r/ck>*r9QDcs ,`='ؼ>[ɎᜟT}b;(>7_0l]+P c9FKuFIڻ/0lMQY洴JGMg,.IӋUr95;Inz ^D3h{_VE|%'$kr,&vERR/ta pQtpCf`#؆h )3cm$xTg: /.۫#w[a8]$<D{豖Jl<ٓ{#Տ-f, *J$UWq_遍$0eGeR~ZߞC/uv).JdJQ֬XeJ 9%l_#U>:6QQ\;W+h0dE`H1s|itUDŰPRj(C-88@.麤Z Y$j,OPSs;:; ^\|f> FHΟ1hU5w}W2KG&?A_:\urJ}ɏj6d=[3+v b[tc: Η(* <{ jɺ" HկQ tUd'Or6Zm.sQmI|DQq^O_TG4x'\T:a)ΗP ;\/r`,\P9[&=͋z(IU^q$pzX|O[PCF2I 3<46c\JnvXp8^c O2L ]&b]*f!%pa |A&OG)G&爋*̘}m`ջG\gp,G~<Yªrݔ(iz`t_ yWHq-`gyP|-N^ ؊Y%_&,K?BN{ <15~`aBSL?U?}Á"/EY}ß_?/Bsk =I]KB:/Qj bF¾*ac˛l &}K-,j?2hQbJ:Ϝ3SVp3M|Xnvv`0a9<iO 'Wlܿzջ%~%^ͷavyӛ߾x~q“ /?O2_Ѣml-03iL> i&4ξ.Ձ-on58`P(1H{X 1`x24h`Az<d87D\_(C&z}PZ ע@+?^TOpbNŮ?`{` _SكZvێ``aap׶b0AKUS0(<,N_ 3LA0 Q65Fц7q8įzam'X+*ua#DXa1D uR I1FǀyYthUGuN^FM+qJ ƽ5 OeX}!@ #+61 F['oQ]Gyt-Ò#6=$y Mq FeW2 ECK~ tHJ`@ep2fIM:f̽ ꠿^[A= NK5:)7c?Z;}I?OM(),tx?'C  ^h`36)P NdĢ1|<`'S vyVD&7zul? 쬰6rp$<0K*OAאA=v>RM:pXaR4tt괒{*=@At im+]DӴ$HHcwz]luG}c+vc([[5z]ہ_.8iJPٲFt> 4ƨ0$NN3+;2v&0CCR`7v +=bE(MF`ǾS;4FM{#q:Ea<fȤ|vN;&b29MQ AGփH1s PN:bxņ/;x1l3o vfG>8y81i{#NȥȢ@2w`ۚT0F #&~mpSCwAqܳ AC5I;K6zfAǟG=hK h _3PS5 1!pl9RGO@0#\Eg c*gYXZS+hW@ut,q6IiKӷd!A er9E<+޶;eFZvxŢNLf0ĴEM4ujeE? *I2\ۀ&hqRLUz6@ EiޱP4yZmeJɓ"m'/]uS'"8u1o*(H:'9H$(PaݦzS&K+k1UA:tt}!=(+?E>B$%b`6 ~ Ct ]G<*;MQ&[$wЛ8CG/{:.V! ):(;0ķ嚕 ˳cP[Qvu }y4:8B6gƒϢIJaI֝weP`.!6<!" ɮ+6.UׁB/c , "cԗg'.$I l&}R?93h98klea0aTtF$Ӵl0{;r®0p Ӌ`UNn: |EU_0$ x,g؁wwYG`u$ -bd)&Nxxыޯܷ?D&L8S`B;Kx:+1!*"N t.PsdU1JKH2FIMonGх@i0dcYFeBlt ^ؤ M1E N61GM%81?ƓXPD!3_X'dE&]n$ҏաTk6g|hDXcN"]IUd'uoAh8f])! plP}Fv2+ af)&6[Vـ4Nz2rQP dZp]X-04@JuFt:Ez#Se2f˦4vFy(XSmto@lbdȚikZV2,f:!69|`2B+UUJD݆,ypյ gW=~:6 @*G8vss3b,:ԋ)kM`<0b0vQxR8W;zLېq4hYTi5N>J;znC?9n^֧4@cN25I wLȘg`S%;zBXoS>}_jfX8U۝^=:Tقd8B -"]$c{`If1#tʮ(gGjEe^F3IP4C_=A3":sw@/UaH~`NW(OϱqN5bD=SP~g(dlɤ` NCT^Wr vJ+^*u B ˴.<$Lb:! w0V-PгQO}#"4謈лz?sDKyv}DˀE:cdAeѮ<A \,aO*ޑB`Lٮ!wN݅&Pal<0rX;~IyhGeli;)&=z}Ǝ7';DyvKÌv.ڭ[ݓў,AfWF|ѝ1uyR3ȳiPg*מ^:d<0&--+İQw"MC+3|/E촷 4MB*禍,v `8dOa\gEO@`]gv*2+:HPز?mbDM,o'l.I];8*1t 8*[S Fa>x&foC7cb]EU0@q0tu3SzTH }/ѯE&$F.C̨!R<9ϊD]Z`67E< Gri$ 6xrceY^2p= 9¤gTDZQ9QT&-*EbRVb@77IhTR;UGhUSVO"9hgT(Lc0pcII:F736SoM*;xhO<*^HMEΝv7{T;nK'b,%p,>?I燹5sq|;.JNZ E |%`0blԈĠhݒ1^FNuJ:e`3)E!Aj# J&GQUh6_m3ԡ'bBëV/Z(vSzFm;ME#P\(ZCzVE`hu1F#u>P~)QBJb yK2$Lʐ#@`uH=ʂ}Ȭ/CZg%L [FySgg4vjAyDg \>%Tqh/!nyᎁ$~f; R;: "F2'E{ebIQ%I`s2XWBvR3kHD2J)f s^APԢw"82pL0vJTfUYMA : pغΪoEݭ !$#@`^ yz0r-:djϬ$#gQF9jI|r"p48 x˜bЙWGj M\Hţifvx@iQs*%<;Y`<:UzIg; 0} 28Dgl#Cd@fm2oK*~`qGi=Q%-4Yn5zGSH1L(%@7z(ȑu;8{u!3vF~Tw0:AqR+ܲ:- *Lfvb=F1j &Q? 3z r D(3cÑ8bYz(yPG[VN \Gu(lɋd[x}Gh/6E%A#sgq/ 𿄄@0[ YI 1>0!?;$|I 6qleTթ[{gmxtUSUgSr> K;XE 0_X h p5_o]@p[:4kx@)1)LVĊ4n;4GAؽYaq82 S ~LxB{ {nA =a3`g܉$@by!{iF?Щv҅m g !eh=@)R2mU ܫr'LJU8&ѫHlIw|?cwt<z v@g(nҘCR"gcOJb N<@ZB-͹*  XA {ht;"bEr4lOr <aw9&vE杊 Y<]TxzRdbr -˟d`1@`@k6A⻰agGY5"?:w9%IQ;`[ޔT'uդ&v&N@KTф+@`&6<@ք`JuLɅ!6l1MEҵĸ5Z ǥ|°XAqEXz#x* "9pH)ksxqp>34QXg|03PgqF4.H11[M0*OeTy : 5; @Man?'wಢk :Ty=aG*it0LfӋ($'H4겂6t;@mjɀ)BT 3VCS#(K/é0E&bhHf@C-7 k8E@ύ =ML\-d.M @AW8Qg&*1zBa6Jq3#9XuZ~yz,("k UϷg\1UV0&ngM dgСqK8D}H\~Pzq`N(sD;d< 8`uV(%HP"cDj"D=U4kaS.˺ 8RvuV6<$]M:@xT *X:PRA(Dap(+craR7' "JX_U"NXs c̀dqj Yxku4˨<Ulk6ΌxA^2aE+5{ΣFp9Mӻ9+ˈ1(g 9+8@!<\f|墡K)2K54䷾hxmM4ċ~П(qj OeUb<O4 W\qoWRp O0u3]2_)\s{n#h<%xº4'1m[Ʌ;Y-STiW@PZxw7ߨ5uikw$e m)*!d Z$uQ+~RmG-39T:0\˂XKL9 i\^ #|m gPiZxֱ @]UFnMzp9\ Ad`( Q<0;qp&B<.2A[ľe\{,B_ !=k!=G< SC &:&򸋁B2I}L[c@#OQ7].(u0X6-T2H=Ӳqʼn^3tXj8\|P/=9' SgnPE|@-nb,x% iĆÊ*[l$"8҃j. 8]9XL߱u0mW icH87Htf'xa'LOrA] &Qǣ ⺸Gp֟a\jDȑs8m@qnpȀ =4sVq<$ Kq 3+ns/H@5uYFHɨHioBA\7 9[3!&5 l"/0?qdXaNb ̂Ov%Xh4Đ,x@!?Rdo/S/u`T]{vgsd>񫲣 >:͔|x;ӹG.8r 9_bp[YSWf~ .Tri$h\)/2MV|%wB*QP]9,BBn_a: -/I>\qr6z4-I'itB!7PG6I~k˲xc]ԍz2W4m﷍gsG/g d}>H,bhW1 a%bMPo'JƹӮҧk#=I\_>@۲o־lw!:50[4^h"WZ̝MtłXtѸ;}58ܨ3|K}8ihʖfr?3*dC,T)5j_#Iaoavq;Y ;&2w7cvTqm)?ځm_ʏƽ7AVCpD|l.+;$ }HAKxkH(k稹Sѳ RN1k؆r!Xh0 kԆ"mXôՒ_9x=Z 1L2]ir .EjۂK+y?}? ebK-QZv{I-2tZ,-&i-@=i1p@Ѳ`eh[4#:A$/K!pC6OXڢ}!2uI"D,!G>fDwL3!POR~'B'B)DqaBE։dr%뾐:`=+=:D]Ph1v/_L 4\@gu"V ]EH D>T>@xJS}OxJS"N<G sTCrsVrH99d${ύҧv],KU6a⤱IM|2+.+#]aBv~Pfm$6A+LX0 Zahx|2l`ĘÔ+I[Y,DH@G$FV۹'6 H@*$D$0?axAMәr:wzE3,dR:L2}p8WK>|a-geL)xS^S`%w31Ӫ$CSyDMEW/bB )qD0 h%MJD&)BKeTBYJq#Ia8~Tˤ\!ֵ6vjQ 5H]9Dq 9D*=:tzu)GJ?yt~$7:$ 4C$7fϔLfG3-0T56 g0id-)}5kޠij3OiJ^sݦXd-):qP,cHk0iTwӵ]syܸ'm{>+Gm$&rjd1KgT0d˧vĉ@ʧb5[{^ٶa@^!1,zmD v6C/LbKmIfmĵ+[ǝ@rtERHgL:j91mWKnq똄sBp2 yrՌh&Wwp;y@9_-ֲz{W=Z{^NlHlΰ)Kb$(hmj,ee%3w`c7!nqQu=Wi2)O9W;_Դ}Z/UU@]uY IжUcs+UVΉ&oj}X xt/ %=e":HI'/Rh0KS깊vi؆#`3e"XK5S:U@#e:ǭYᮁ1Bv=wDƕbz$XhBP'ZZdobRWc?Y65EbFK^ELEٖD"L.0Aȗnt/b9y%b<ϖ"YP {L’_'C8 4rH %0: ˅?í#X'Ѡ _gkx:>qژ .y(wDeSRc nں'6U"tqcQ-5`Vlb^Ҩuր1=^{˫Y-&u=j*U) j8WKW -ԖtN65Q%`x =Mβlj8!+ VLp:5X'bv Q Lյ@4"]\:T?S&Ɠ\X *(+%zރ- $E4.A`S)Xs1f9&\lHD!{ tUΨC<]`&)Wza* @x &{Iqkdfc|b|mWc.,U3 vKoB~T&""3IJ(U%P?J ن=LA#==3H@ŠP(DNҕ{oGN%6@h#f(]NP gE;npP/+wz[WȽݧ}3ÿulB f}⟾>|~[G6|<_<7[J6ځz~k{}s{k^r{}ᑿ7Bn27om$Mg}{Ms[s`k Ds; *]ִݕӧO)@}5(XG&9z6/mF (,~\egfPo7ʖwRV LMb"صeHTh4 F-)9C3s+i%B>nQ V짫I$+(cXiNsv1Yb Jq>9mRI>'$PvSsi$(E~ngMjoXŧX5%XEXgɰ>KYTb 6dEf-2Bo]!>q`Ŋ{{N5sE:ͺ@k.n\{\|/7;wfjr]tY]Wߴ}ecؑn7tcŹ\|ڭG M{ɻow έU\w##[^}Φk/YUcŹ/wo?׾׿O~}ށ/[j{W?}?}g?{?~߿اjkC=>ՇcO=O>٧<}ݱa}eV+V^pɍ}ooNM> ?~ï6]ɧ}zѳigGX79{U[=Ɠ<_ݜvy'?|*Ŋ~?~,O?}w]X ow~|x%'ս^/>_ubH=zNEי/}?t8W ]⼋op3}?},~뎘]ܸc⭏|G>~?r37诿s{깺 {{{﯏^LpІɇ+,C.,zYԑ?'kO~Oq9Ceʎ#ygúb^sdrJ]xU>o3A} >. TɚRCLT>ezñ}j W?g\0V%kU*?طyKWU+KB׾#ܗװBtu b*ȃy˟|ojRto{_F)EvZYu#cvuuhHλ+^DlMe_u!Nb'?Sc+@pJB@&/#ԔaM6B_"۳jSjѻOnJhNT=߂4g{̙{'#<_~򌬅<&?Ug~?n?OW)}}~XW鋍 ?^lևh/>sسN9Ku>d5A^x>F_ɷKh?R}ϯ~/g[ހ}%v_=! O۟E'^:1q~#v}{uһ̜:%9.ó'\YQngC{Or2g1?3d7 vN?ϓ iI<. s?G,Y9'(4\XS9͟)rSUm7R>}~9cojMmdgeu[>ǟz?3?}KxCLCwz׻G6ec\ S %tX%<=dVd eX ¦f⁚-oPuxuW3Ɇ|c&k,Xo qq6w^ W^u[}]\z饗\ W7hW_˯]}W\vɚ W_'~ .\sW_sp]{՗_ [YpE]_}7fE7^Ov]hoR֭@>)7bp~æ=yӆu!MCΕ_ 7lٶw=6tt5\)={_tM6n޶=\җھu3iz._#=]zuk7lKiA>׻4]{՗^(!Fem)U7@&X)T2(b /<{^U7m"L\DZL]ݺiM]ypaȞ}А4sAoxe]/Ѫ5W\ vIϱ 2'h^ru7߼ xN%w>Oo&!ICҔ j}6DEK[iKK'ze1 ij}WUv¶ `q7㯼6VWY-+]ue-Rz6RZhoH6#|9R20ǥ7۰y+z6oXw3y+[}1iJc=[ɧg9ҋC,k.#Mܽ~Fٰfp0Ӧ^H[|B6>8ŗ\"nkrDF]ED*]s\s5iGS=FEEk.˯.ZP ;ȄXH:tJUL6ʘqnD3l7c̸=8 aKL%Fpnje6U=Oר+TS=heԪqF:e V\5Cm .)ñШ1`{r^꺬0N2[`+ZVB}Ă5?֯V#c?cXj,{٨\QeJF޻_ݗ]{9WkusWq{]S'Cwԝmkz+w؟;=kط7koyϟ3 _?ͯ@h?O~Kկ~勏~#t-І'_?o_§Oa u7_} _#7Ͽg+gW>xoW^{;?̣~wmrľhÎN|_~zoٱ~ Nfkߑ_7moSݼ)Xs.;S ƜY 41+d, ,%MYz4"*4 sکE`rN;Ui4i'LOI>' &U61Тn+U[L#L&h,%2h, 21Xldf!34ఀ,gaV 3+'"!tsW_rUs%Ԁ%l厃#s[6_s4}YʍN?>5ub]n0Ї{3kz{_}'Ϟy<{֜[x~gub~?=t_OUshy4 ?ⱇ_%Ԝ5Y;<Կ;ԼIcgy5=M߷nmj}7iL?CW2\νrǞjznZM+ژ/ ٯm|奷>gFb?FkF?x_>Շnt9+Vo~'EL e7\X$>1\KumDk=Xkwޙ_oo;^Л;w|+L^G_[_ iΗ4CdTS CYͰP4lQ5KxUHpi?t7E}\S=U಄iMm fYdk ,^ nF3Qm]ǡ.\%4 ..SR n,ݐ0a`-eh\{&c)&`;K4Ѽ&`yW#[ak2uvXF2(C3.zLp>wfb*Ã4L]PdT=4Fi$ΖugJ.](dzp3e],\/Nc@Why!Cn+А [X9 h>q-6d0)۲*"뢋LB-0Z0Fq4a!33ϭsQiL?Vw e"Ķ]֖+%x )@5`LVu$V6GxRшYu:[BϳFVSse ZFU[HC~X{le-䥦Rid^D1m|-ϓeᶰվi hIl<ÖZp  !И %8Ex4#@5B5#OD>cS$Kɣ:Ft*a֎XIn/Eb@TaVЙC cS~&v]K5 ]ŰF- 6Zl!4l˴,:!'A PՓ(YF]02ɂ30 1 R=#D6?Jo|HX^&QLSP3/l-L׳,n.1;u =1ON6@qlbƦjS d Fi|T`H|G3NLuYkXxۘ KPt8Όb:sT\ [Pvip-,D'\b;*y}Jٵ5SkS[V4ԋD<7{4yb Mѫw嘑暭er̵tQ*fnf'A:feߕ= !)ˇO51?lrr}AY[ٜ_\axA*&b@@u"עaFxn] 6qanLׂcd`[xuFj0& glQe5_eGȂRh5-oH| !\;^G#lOJ/q:0߸웑\<8)w +)e:_;}9ĮfxD )2SN)M(dմ}A!vl:M?aȱNmpj=8.f i+:DM?^T6|ԁ4"'ۣE5VtiX85&"^cۢQ*v#$(nxBRܦhkr5?ÇPJ⩚KVxba$[쇊jD]:U\䅤z,R] >Bvbр )6ǵ@ȐZP6/0c6ZEDmֆ4WN M>xY#]?[8|•!f )}0 )*4>6pl7XХwGp"O &*JlQܔ]}|=}xH& ,򇲥\¦NG՜m!D8@{, RTQc!2J4xL4tL);WIJABd2B؉l*/s(  ۂmɅ"aBOEV8M J܀t:iNærgu2(@AAY ܧGvAw7/hz"p㒥YYJ.#Tq2Ϝw0wsv2gEѸ#ҳO؉IdMKdMc<IN%͝jn_wo:]R_t[2+Btȟ,>~:*σE+Ӄ~tё2{C~tw:S.!ui:Fv}p@:͏=I\ʖgr$=/LnNj&:-tzg< CŒ2:/u'င˜ttBAQY<*ay:U({dJ m.6)̤3 lܸ Te{o]vcHZaob+ jB8q=âGQ$Bda#/$r~-DOm|y`hB,`mDQc\ TPCEY"#kB[A8lY}蛖֫c1kR TI&?oSxm#o;ˤ{=!+f|rw8ӐJzCkVJ* ,\!ܡ ]_BwB1_47|o^ărD/GamQgW/3Np莧45U4z%dY k-P^5N n$?-=uv2In*&7MYvL>i^Zv¬vK5MH$ YI`4kzaŠtW@|;(̇:0RkGd:siÅ`!>bKyTlkbӥA<'M8[䨫&r4=娙O0QnCIzC7E rhùfX)Z\K,J_"(zpb䗡NT\wUéo:ʔj[$J&JQl2zMJϺ.U12u袈j[LNhe.Ʌ$vAr8h TjdaTHHf`-89HW7,]mG9-H oFjm-|NoKFO.P f,n}kUcO1}J8 2Y=\ILN?fkUXmyrcAumP;1%16MoŪ[fG.EƋVc8V _ǖ^9G"d{ o,VC}!O_+? ':#x!6}d,>=˯=N]L=-Yd;:!F&3V-hsĶZYby3KQ5%O>cC]l%X l<^YcgъH'5>fsjt67UUc#|s).zQUxtgbt= iC B̜@⍼4f— klp H#?HT% ^N[q Á[,׊Wƀ v]:ˆ#H]%v3x%,Aoaj<%FlXYgs20Uن`L|-m ÷K\l$y@=*gUɟ D2am\l)U,/f ɭ3&Sa,s.X2Pġ*Q SnrqnTS`#ߤkO9H/5bod&㘌(#]q,!KV_BD3ŅUʥ$496D0 ɸ&b.^ {Axp`#r$r@S* XV.A*#8~ 7$lSAXBQخ 8(D5 :pKG(fr@0WO&hQu|!Aǵ'Η*$չj/gA@PW쿜,1y%U<Ώr@{3-0)':BqRp>HB0dCl+F/GڲJX"o.d)V, /6BJBy!vtWŖ50=@B]UN`<݀uԝHH<3j>73|x8|Kh~i#LmV`Jaڴ^8c14<*]=wHR:ſ Com6o¿q@Ȁٞ m3- D~`srksyFOMr09p\o_JW}䮞1:"YeN4goΤإHafn_LLm7_BWtƣ}%=dzi_**X;j"oP)fj' (L8拇7O<֏0KW64MlK}e ҆x1`A:)+)tcy*eeX̰`HX;9NEdj+h #kx=%_S)JY\ &'5689̉2ЦLŘd@fe94)*=,tB=\0}.j"ԶKZf5^-C3MyC@@xUe:3SLac6Mn'}װE{dƕSP?(qg>!d=RL&Ɉ[ڈm渁 V;`&!VO6khu+z9NJlw L.s16|ْ6lNurrX!֘?&C(O˩\hK)ʨR*#k6Al(C"z8פMNPFT+MaYYXSr3Uo H#Nf2&S +MS&ݛ3pqHu:$vm%3Ӓ35RLj#Fָѻlr\of-[ՆX/~uO+;?G&؀>UI!恥r5 5xX]8,e.؃}=4NӗHrecg6oY c5T¬-E*-NյRv2/%#A֎_iCKƲt!MYbmXF'!B2PT MZP{2xpTHu[Q7DYA0̥gޮ@կIL `m C9s׺f`葡GT0K;xb5@x:_1N2뒃貼 7MzWź.Ca7Vw7r|P<6,Xs4eV=H=!;X:!z=H!Ewv8Hr##^Ov1A9 57qgr 2yb,@ÅYq&qى,ʒl3T'q?dW͖q:RG1WZc;$lXKӧbSU+0WSƬպ닓'$,Y0faf%HhL*q>uFKOW2`zf&jSp8'0iq<+Ktxn ABJsm|bvz?M6?JխfzHnی?* {J崠Rx2=Ogsl^1,=#òPrY4Kk4{bzfF2`6-b!5 ;q 'Fֳ(+hVLs'spw</?ݗ3䗞u0ݑ&o[n?wmܳ9տ;gmk0[:RDԡWŸwŸ^q=K}jHsf5|{p-왩]cu_wln-B};GߖN6:/5E%ή&vܶf:kwVaw+wܻ~z?w|殍r@88} ٫m;X}ÅJԶ]Ğ~ػcnojDˌoR) ^E^ߗ^7~Cwf/W<ڲȺ-y{o+q؞#=W=mݛv[iˤ _mwo^ȎۇtoзuotnEhuOwKrwo5ZzT-q%'`r09眡Mi9k}%9ۘ{|CcSJUJ(Q[i֙Š}J|[+'BT5JQcV^N?)-H; k;^e$R*2g7)cJʴQfj2|uV-4JdPִ>|٥lݖ}Q);sZ9(n˫ImV 弻(HIMʍ)AZW<92?DYM*RQ\e9a^UN{yT@A`o!U;*3*TaWU䪪R}Ts Rʱj#/yjP`QGZ늮Ԇ4<9ZINO$T>~>թJ5لյAݥn-ڠS{zZՋV((0Gr>iU5oi]i`GV`|D{&49gܢ):n͇tf`k4ӀY+frhS/aif2< U,/XvU_ab؀5Sws?k3԰kUΧ.n3k^@qf=iZsXYδS|~/}_c^,N{XtsV]ŝK6 untYuKOϤۖcyz{>;ә`W_RQkhK?/KџEAX|lIS-"d5df BcE:>KLF6z1sm9fakwScogӁqݸo2&o76s)59kꔻGTuL?etV}xY?V'̎LbYv}sY_'79_gڼm:_sc`-ʡc).w{LecXY.ߟՔ藭[(b[߱K!uZl嬿_ ,3ovےV VadjCQ455NuYv,/)0b ʞe{kugaw8:#K:Q5Z`zZ4N0q&ݓMC&)}\NW`.9irC^3\]Z4'ɩ9MxUl2[e1Q>y2 [h=_iƁϲbێ86qgP_(Ʉ V|bǯ~ǢX'^/d׽z Q)̊fjX7)wAW2 RSNA:ѹ./u_fWzL՗&CoQE?Va[h[cU R505}hY r2~խDh}6bֱܴݣ]^E61$.։/[Otדt&})LfaTNlmPۗ'cYd`)Il9rh*a [FikzGG=*?[[8{_ݕibp/}3_SM~6Fc3VkL/Z}fy6;g_ʹ'{kWOϼv߰ppL,~3wZV;r9gЎ7rW5۶k\[R珧i L۟w>kv*ݜ~P]십lcv;rd :__6<3{]!VHh}1w&eA|OUGӺ8ƬqlfOlT 8! ^睙TBky=v{tR'ү)6&h' !OgG@|w*O~Lv++zu 0U[91a޻7vK,ȄY\Z">&!U]'k; Q6Z`*s$:Wɣ[ d3\["E&]-ÅKszvyjVҌh-pɍ8Oݘ=u\d+z~Niia7ήNR@%WB.8 NmՒUOY(z]r=4V.QblJ]9v|y3iGه].5R?o3GVc,dޟbæbHT8'sR,Z4p+e꾯*{`%Gߣ=e1t~Q`++8cN)Q1OC+\\vA ~}M7ڔ^Vv6ךI`=*XTg9x=bʩ<0mMrkqGNCJNm6n8y`MażHzk[bKCNwuě܍LpC6E_Zwne"5F'R.ϧ+LRK[Nug֪rq$\}5W9t8ǽÞfNErta~a]jH>ةQ;A ,= W+PDllJ#>* g }&kOwggUY8_QIީ&c:ÿ{CrL18!A# vDm!$9hMS 5b:J3eۋy&9؃l0=R3kü9(00E5` 0T=N"e-/|U3tUx Oer}Z{k:cV\[H^eSY9O1{ϷƤ `y;n槙U`!اͻ#`/栥@ ;%+?~F{i?2Z^OD8|kzdдsTQZu#d -ܿ`)}|4_W`՘"a=%O9\L9K&vSoz ]]٨wjà%d)?Ebd.!D?P(&џFQOI]Y2^%uE*_0pQx\1zA M M S$ 'ӄ0f"8eAџ$ 7A-#ʠxNwNIbټ6(0Fb`e ؞r*> E 7j2Q6 i]m9(¸+eW#>Rq" bdrf-l=UG+y@m`=h]PbjY?5lZCU_ub(.j&'?[pž}kD&^|'ga#ҌmPI`MH"o!J PwF&P+S/i[Grx"@ 9ރZ TAy8Ys*~g\5QfQLM8Oe{%pܑu`JÀ4S˖) t&0"O?!F!FШΤ.5U*$XKo[!K+v&eSbiq2ls%(Hv<(kh;h-&6e0kKg02Jla%JD;'0G&, uֺ)ڞXSq®ؗk)h=r,:]&@QFSyJY,YYA2W\^j?fk~ Yh`(0of;f^60dGwx̵h^D ~JjL0EzC5D^> lŽx %)\R֏eF*QedheحSn0FS 牜\6&%h'jޑV#@q +Q8??Niѳdi, )y4/en t3lP&|wL!2<97q=_`>žZBG]rwTYV1{ۿ&PtQMz Sfj]wW37կlvYA6['ǎ%^ o vc2ou)2he?#r؂MV%\L~cM#w0 h \$~ T{o^WE_BOK 2ɅgoįO:ц$W&U4ckPp d'fd;J[RxC~*QK:ƏJXQo Ko;ήbDB0oՠΤjFۻp,cħE  Ž u2BAp6 RycMܥOl,dD;QJ8 ˦ 0.s1[UlISS$>:{HMA}^6Rfv?A:v'VNYj#.c4= ra&<{Dԑ UEcl/3bdD>7f%-:r5az:ɫ[)cYJun⻫BMW(Qh/EuIOx,??X*?S>AĹQwEQ+ `_%! 4c9;sn qrlZ TFKf2rl!9O6) |N7}RW{s838Go'7025y/NßiܥNeOg6 ;T0<0dO\@E/@| lk[?prsBԮyD lW)< 1%&UWz~p-߭ӣEh> 'VC]`3/Cܥ<wl0:]b9bg}ia>pCq R =z!;-4c88 l©ầføPsLp֓ܒ&;]|dC)>;91oL]Х.5v3g?}P͑l!T?֩?_TYLCdA\ɑǣ`X""IiohD kz(+epɌGPGrXK[9\U3`v.Sg. n_]=YWtmnjGSN7y{̆Le#0JN NA9|&O݋Og48saŦVلʧ_i^Ҫ4] M)-xف==ޭLꮓTKm58>e cc$t kRO:(42c}?fP0 wW51(<73܇JUS A5_*"kuXUdJi+3ƨ7,A{_kɛ葃,zޠFi&M6J?cxv Nݨ4,в4'" x#8AEp2(ࠍq4(:he+$Om]'`UI}i|>A%NtV֖E*04jh:ࠝ(4(p8~ x‡զAxJ u4(/kLڮؠ|\? ~ FAæ,rN/NU,iKFp\>:1~hA7~#Ohrōu"wP]PA-̠`R@ʁړNWpFi3x#z -S0.QkQز[%4sz\G 䠁\+iChPgU*A(Z{֯A+WZU\σAWL3$42* /SeLg}zȘXBO$Lnf4vlT՞ReBO0=w?w>OZeIE|A}~Zć d2/"=J,rщ<-.ko"WSaX {i~ѶtDiHN:1Sc9"ΙIOs  N?q$ulβY5BVcCc_mJ ޞC]Jer^a <q:7ǷGB*i~sZvͼB۳wٕG% ϤR[?w0F njl+t^eVm[g`ݱqQЅk*8vB_La}.@|PUfd$x  xbxrO.XZc3v?g,?sA^!m(,k!KX[ rg/u\bvF bx14g'i+==_={z\ѣn(mOOYS٢42=%dg]ptQHlsAtiNMXki\-Fzj-CbqY82hѪhϼ窉 0Aʑ04r_ԬZަHvp@hɏ!fi:M4f%$%}A`#3ǎwSU 魯Q4C$洙+0Y̜*턖wC?DAlژvQڰv0o^9ħ HsB/`s-n%e\ѓXMQLlF-γ\RK .[O¼&?SJ# vsJ~d#äbS/wEM:RȎ/:IL:wUguӴ\`yq01Uq%J1FFHBs$= qAVIo3sEq~8s`r;'j^++[V=';^;e4z'Q ߏn i[^a'[ #%rb&)7R-) g"|!b{j" ,)JtǮ|rHqsrH&tHmS jCAkR X,/Qԙd38<;V`+@XOǗvUO}*n`,EKr?=[dLgC9f/\dxݷK>EHb NHF~85($^nxJ~a%TR7|qtHLB 1:ō(S?"F-hX`G,^ådD4LMƹtoJsd endstream endobj 608 0 obj <>stream qGc|ϤnY\5bw)@RޚNEƫ 0~!hL: $c˓vMBn:w̆9Gp@eٳd=p{,_.%9Dw)w:a@{I7JJKs]kz jB |֬{b2eTՕNq`9 W-2Ĕ[x۷e1~vt&g ; PhX,mNS'}u6?7pbrЮ'rG#Si˰NإrɈ#^voCG`ݫp RKNyvcxYvdVU,Q1$YCUr(KI@N@WD C&7lAcx:岫tNqcRr?Ua'nUƉNW HLy.(NbJ;‰qj>L^T=Vn 3n{Q"2Lv2#2Ղã 콺H}>H"V,J%\yr}5і]:c@ADኯ ꠥ er52$9~e.,.U ު/Q0C(7_Oq·DPq%UlJFVxi;tF7|E=s#,t=#_'z2TeP}Q=Osz8AEgCQw{q=[^PvJߋ|1cIyW/& 9+mK99WY~z(dfBބ#,9̆gk (~pm֒ IޗdC[*hK,ׇD{: !/E‰ʱA{fFw‰kKដ[.ϯ|wR8Kc23?Onc] Ψ݃2"'lى{} j&G"uV]أ(nMI&EꁌՉqL~Aw_zSv#f;'J(<7SK&lNJ(4RQSML)[o꒙ 3w"ap/+(vDٵX`$Rnv_qJ"d'V:vȠ5l=+vQaw j jbvb[ZMӃ`_ ;ANZE3PMdP؏sURbT#>hdLJUؠۢ9N2]Pnvc&=gG M|b e@4v4:{.!xI:ʹr˷`ZQJFҞ0Vg~I_%~s%>|R<{}Y'9>rwo2suoNfHJɹKqew|tWy nEzﯦ{4s_5D^UM'TK'|?3tB1A~yN;E3㡅_[j:!P7oNȩ辶:aU NF^PM'g j:X;㽾N➫V qsG_QM'NhJ?vy*Nhut+tנ]j{cU yXW @/xy5iUtBt|j:1+jj:!dpeNF% j$=F#ryžo2 Ĕ< W$[Zū MIҺx_u!O7;rNw61t!Ý'2^27+SdSJwUVNG=^Q2S )%uIb=^WuIrMFWu v;$vޫکj|V!v d8BsE7%tGZJtnZúRsW}G﹧E OǵX}/2Gb7˘*}uܻO'c++L-i|SnSZp]!Qj[$0LZ-<%2 Þa.qMtW75YO.Zv-{bC`ߣ2%|Ć߸y [vRyoK SgW{q[fl*3[$Ǩ#JQoO>3f3}sf!~% [q6~#K>lo_NⴾDz&%N=)U+sXHcdnxID3D]hPFacWHYrĎĊ]ݹRPO-M;^&v"<1Pp9+u㮴/Uupbڿ]):X] J}U]QJ?+2/i^eid5bMrY}؃,.CxT%I1n{I2NvRi&v9^3Nnn 񒤃)+F ʱVSUHRMOAuBYR$ 7O>^v+C̃EO;q>ۓ'e֔)/.?K œJwDVun,W\YwW=3_p%nw5*)lhGȺI):IMpo'eM|)''Ur D&Um* `_Trnq/lz VtTH꡾~׏"7c}˹x1<Տ}tw͓TW?['"H3J=O:*`_?8-ɯy q}+\W]d ﭾ~҆!}d_?XF#?O=P_?)#a^O \_?NIVOZ9ܨOZ9<^Kbʄ^+76BBSe<92pOoo!/$ L* "X Ŧ+[%zt:},X@qtZmT[h_Re<eR4 XebXX-&|ז=&8l m?,L*(!dXTK$}&/' 2ޚ`h6^Jwvv6L~̖H;r1][x3p4|̓Â:VHLiѩJ$OzSʌ]2\>oO!\Omv9ۺcq`:^3",@awg"lW3.`,LOtk|CptŒ>95$36rK#w=Q37،~3YZ9~^p&T hlI53zvdYXp*]a֞O{6hulot-,ĢaV/O©xW$~}MmVnliӌ: 7h|㷁~(6IITa^{mBqDM} Z'rU\Op᳡T`$vW:ŋ΄!/2PK srah vv>vS_U7@%.bvG0_i>X=b{+E1=X ݮ*l}~2Uٞ5–pH`6eU0D>&6[|j3Dcv&  .4r/k% yH "s C"@ Hfl"+g-)`$:zMm{VR m璓M|,}|%0)7>;qƧg[z*,M LOpD [ݘn/Ԇq uVX8%"VJD+˿,GrQ2uk)S *~a,a(PnhUm&jT=SFupWi$0?:gLm wʃJ,`԰Mv,KԀѠ !pK m): .l&"G bΖ&pL 3@ zr"DHIa B aq4?}0? 0WJ޵ cedH ި#B d`̆g0@_%,Bn-s x_zVz4Z)'ʌ$AeRJ:$MM;5ƻ`~!R54&*ۍAc6듛c!2H?ɮ6;Se69(0Y8xa{S##g8>1tX" 4/:#>y繸L7T`v+ ʁ[`40A3f,f2Ncow3Q#M&=Fbfٰ!Ӄvt~ꣃqx1 7'CuCRi?vJ#gYoEnF࿴#kA5UM(@xS#`O94Wg =JgB݀`$}imZ)v+C@"&NTˍ1tu==t$bKSw$,π "ApRy\gV G&$-^yԃ~0Bz @ fo#gfټ`Dwsm) br$쌀 A| mW6af0Alr[ˈdD `^Āq%e?]+J~xx\gQ LB| D"xl&[4SyXI^No"~j*=u kVEH3@ق YJyԷb}6rDyثԉ>!l /wy[4E!^ q oF^nus>.c^cVxnowb\sju.|N-v?Kө/Np>#UH-MNx/3/g. ?لʧ_i^Ҫ4] M)-xe`z=g2'>7qO.`D!J!V"\c>]q{љ.$_XqW?]=JwJe+JKY֎oDi/Z[+,ṡf; ty 莑̴P?$whf5PI]U苤Zr9+C34ҳT\PF^gԃ%l(EG+2QJ,cQ#gcF ;aDJK6R@e8.&Rd[ks5iBB+d%ԓ.=??px3v _aX hh{U+9 aw.FerT>Ehk!3ܔއM{b gqe+H[$fCD8"->᫻|4yM;+Y*߳+҃4U P@:uhh)|n [21=}0CVry?4Mٞ@ E RB\.=sQ29r4G?2G{;"M¡)dCҊn7 Yul% ( TLHUf%Z־^Õrp-[H jlHv )Eb(J>ž2PP[*4ͣ0;&hɞK©"gW'DTGZyh+K\5W1GjʍTgaҏQf\p׆+,bo`Ƅ (qޘs'MH711kiǤl"xыv_&CR)#pzmN0) !A*xvdLYBy :b>cϒnٶM0 ϓF(=n7F)b0^cU?<@I%vj;ـ#egB/cO<^t2U)`i/1X|f\fHCƉfQ!0oо=kaLۿ>ĮL;,a^d \,|^qjv%NJUXeuS]]&-^4 d`3ZZ}6 ? t 4aSt`^zف7`4FZgcOԞk*(m+~ľQuB>C-ԦQ"{RO:en`:|Dw\̲,%+}r~;==>Y#*u6vr(aθֽmz"X΄v` F"Lsb֊CB//nݟbR]6XRt)H-tjgMױ\*0!E}5dM=q O!%(On58 џmPA)Հ#^6 O9&Q5V'8z, D~9fFSƜ`{pqh\9ü2wٖ5d=FenW0䫛hvAJäAʸgPo4RP `iirjvm+Nӡ*~ u#H;2WX ~pqa#q22f*wL>TD(vtV%Sa^?U}*,$#v]9:H -H$|Kx>a|^ i w?VAJ9Rt(lgCJ1RCw<=Q.ĸv~n33cw/cuxZ@*:H鎲r/O9 S (ٻQU]-NO?Ej(a+0y\(wƄ\ިEWoscM;=l0ǹ7Ә0Kxɫ[<IXo#]\Q\dȤE}*'9~~P9;A䰫Fޓ N e_V,n9?a%d922ًA&3&Lbfy;c+D]/41d̂*'T;nDjIt'P"ɠP$[hD }] [r# /ȡ.0yṮO8ϫsJvp<":hqͮbθҢUaOUi;\8+f$`'* ܕH.D>:+W:9J(MN(ǶZZ(qǞhCɖTVG7GtGV^euu QBg+0Y? U״|idωᗜC/n[#} S m 3k=jtUʢ,L2(+wQCW }Eg]ػ(9ʊ¢=(2my{P/==ȋ1 㫺>(*!.@VsfCEa1յ I='BqrF.#u_N Q(edNE: i2B|i2)NA&x ,.M[_/ iEFJ/ iEū (=xenkaqY\Aʇ,Jbe1t;ɿⒶrՔ%Ǟ-Ҷ,5 e]6}xHY,`ݻ(+"=o,nӁ7&^CY]Yl"e1tE6bs)+=Uʺz+J2E~ ]k\7QQCWCY ]ў7PIWjՙ2k I,=bb .b芎\9,}^HY|+DORّI)M@ch,/1O2Z38Zi{xy~ze1tԉ^z1t#k26*e ,*-.x(3ߘAzyψ04k3"dxp?!#)6##¼?x^.;A 邷N~(O$/ƥxJN~ؕ\bgQE(+U%xHF~ e1tŜDQFl2EY2NV&6O/,{(+{=eOyGY ]!]&b!@Y);8BY ])HY ]7辅7B\}ۍ28ߋ)+PCWs(+>b8~e 1e`@> ӷr4f1£4f{-*+->,V'Ch{ ycx=Mhʔ7uuq#4ꮛ׉aOam" u66NaF a$XTa.U[*|e1tE;(+]Еc%@0es{GĽa;"lFi]W]qGYB'uczyκ\uy݈ޟuHY7}Y7RY7(b`7wt403"K/FA*W e|x卿NF+[g_HY ]WCW_)*J!ʲĝgCb2#!3'Gc'$?$ѠIbߓOazXeCCmB!%9&91W%UgB06D$Oc{q$\ |۲h ARrPc;`Y9\Wo t{o{ [IPUoU0aumӍ!7LE=?1{bϗIsPVk'sȗ|od14K=p3uٸ99|q=wG%u]uӸUj'Lƪɠ5֗ޒR_;i?-M(~_V{}[yDEvϼ{yW/wܫϻzgs {y^}7,׋c ׿Vg7yP?d@{{y>g&Rno?Yq,OYq,<=/dzXoN49?:0c' +'|BMs;P]9ioNs}ۓ?aΨI?s[7' 9Ã9IO3#v_Yxp|+VM9mڬ^ɷLʽgЁ3Rl Z*&<6t96x&Dr7Vލ79o=۽w ,< (ۙ9̺[HUԀv0eWv#sNOxdW*"FPmcc˗/qc)~{``dl>Vt;_oVJˋ-k`Om#D^6/c ^{7Vf{5ɡo_[E~I yp2e<'Q쥌S(NrGuOb%\u~)ˬʙ6wVpѾ_iDQ3*,;G6;Hl:Rj\ukO͠hueg4>Xg,݈,Qskt%?]F=6D?ڣG'h/oHҖptZjGލH)²ݴ+Ul-[-j/-a~Z!@N۞2'mT&*V5I4p{k<-mVQACE!ṳi%s WHҬSVZFiߣAt׺Eݚ#YHv=USܑ4.xmAM#h܄H{4AR߄~M:` h=\/Qvz ([Bd k|η^@^B5 "UD= h>} ^:=wԧpU4ӾZ4o[JuG6Jz5EKuc22eYnEn%6P"FJYe`XMVX Ȁ8~wD~{}Л Gx!.b|/~Ћ%Ǹ 1~B/~͏z\_}[-qCS_I*I*I 2bQ6"5V8 "9x`)ي @#יaaE-p.Q>a*S,RHHjZx/D\|OP$P=Aͱ%*7^s![8QKţfdXKI^cvh{\/5Z^K$d n-ѢθOWNO0;5؁R*F PU D(2ϸˋWhs \w0ʒN-604*,s,H*QD\ۮaL "2s,'ъ`9$d$ R1䏀R]> %f6e-rx#!%YALh}̭ $n& ۰+&2@ŏW2uT D0RhˀaM 2̚lTʠ2maR(.00u*F`5G ܱy^XN1C;iz05 .p=@ckjD}H6 t&4:`b̋J0@ppIh Bw ɀ4 5K%ƴXY=W:B; 7;Pny0QAkf_ 0_4TH4U᳴c\\(JD, <<3.& wn'?@ğM&qBQ &%Ĉ].QpIO_*FS`9T-YAR\sa,,O_hB"Hz̓(}W,lK5j%0&Sh)bG+קfkP)ڇy* (Q&qO<蟛I0MP+b&Rwo[n*x@61mGv {bq$i ht^IĵJ,C2 SD'+yϬ_R1>!q@PZ (Xڂ04ǔ (̘`k>i"b)X#ڈsȂ9$?-2Tb $c!\AP=`jɚ:d*kUT(6Иo,phZ[)f$6SXtDqR7i.AE h0_4=Iznbx 8bvF]IQ5:r&1 D`K$4!tjP&]8M&D#Kw 7_}xѾo ;YF|pBd 0JiDCf層2PYO8v*\jD X7F0CR,%) .*i#7Ͱze[6Kh7} 5HGBȟ#r7y $Er>ݣ226r|^.!&hd \Cf|b1P3c(ǵU<q5`z5&&á`Mm8$YL% _8h&e  fJxm 3%A`bh8}_%\ ᄈ׾фQ5h'^Qq,#.>XQ\bfDhH# X0f V * fm0r o-5:0%2猝5a"@!- Q@$qAؒ@J2\`pq<3s&֞Z7i # QÊƘHYA}Z2 ΊD): s}81. &tc)s#y.ilv[ebv*R I8=H]N%, H6l A4c` %lij,Kdg\HS%+ &X\Pϳ ai0#Ycl5AװZXf$Me  ABږ2xZA73tl@TBL@Iׂ2i}B#yq' =(# s("HD=";}?%Yϰ " (@}d\ F#Gj2vp)</067,[`ô&,p(I|+j S9=AhNY"e8*!16=el#x@!&`1\k,K00+u|c>)<OkHkdgk7;bT{j^/G;cG!AK%Tҥ%U'O g[R$~-q?AjY]!D"y?:tu5-?^4k/nvF,AA r[.t : ?޼ݎNjw/OMÛ 7;$}w/;:%ͯo?wL2e&<_jRvX:[=|H~?+ߋ+>7VYeW &|ЯTo1vO_wh0~gԖX4Ww2P.rbn@(t`#! lc⊘ uPrmxϠmB4x9uy lZYW( V {bʼ&xrgߖMhUl5i`GBECI 3z3#ǖ!vcxxeAxAYLa1!E3(ʅ'Yb]R}X˩kN f#)ze.nK0bI}fQEaach(7[iibl9hMA*@F0uf ^NTj" \4pk`Fp ׁ>|9ļ9ol&g1h$m۔ +0u>2|BcP8D9~RQq,¸-\^uEs !A‚EV/!1mɎ]ֶ6`|8fӖ%G'gb̄@.!&Z|&b+IW{ĺ Ĉ@AOQq{56Sk ^3g+hXjI<@V<(%vNi6hƞ !)!YM6+w1Rhq|ZBﱻS!}0_hp-ћ1"Q+YdQ}]{Υs 5,bqo)CׄKZ>h|lD}67S ~F-@B`N"܁7cYB0ẊHPF& 4A_3Y-xĞz KO|6R> 32po;7u J0gA k/SK[ ؈3")>*B 2eъxNɕBQB]r-ce2qb/pE;FsQC@Y-0hͶl*%:JXzCzƃ%X`;f)XC$ٍܗwlAWGL %@4ݢteַ2P>+ƞ1xP;A$,X!5Ӏk( 5Ɨ2Q;Tzg^#.K0<.~W(@axp j큘4 -HpA`U.S>댮歇XvMu"Da ,3\[Ļ9 ^|p`\uT,I3fdq 6 ,% *du llrͶClArqJwa3eNqh2 6o%lO#, 9ۨ<(La۠b^`U b.#KgŪ 3\苼E\7AlF&֧ P bDuo'(>h͆.1,k}DIԁk4:bvW6\M̂CYmlaW3O 3PE('4-lQp4B;CyX׾, tZ<8c X VGFIlsh%6B_dU AhH<+k*nL% %Ba5ԉ#|a` nb3I" :h`5 o Iޔ NgH`8F)b!,lFKFgP:.NI0~ װn` L8ۇl(oQ&YmX!C$uֶ}]1o%ȇ0dp-Yl.3 {l@Lj$c0L.|6hzѰ"Tɛ8W.plTL cM L$|Y\)y5:)F혰c',a;'.nspk6_w*Ӈ R3e[c ylVk=jm޷m6HJkĖ}xXF7ƐD[aV@L/_R2 3𐋎fmg" 1Ya E,hYlbs V^ogfQ2=yv`6;FTWtEU$#J*wlc0F)Q c!(}#tld2/t4wl5NnNФ8h+웬Á+c_]8Ͽ-IY{Et4_7cŞB;4qFUP/MAzy W yύ]z6&Toƒ:I^ޟg*|ha+|taos۹۪.F^;clG?6\Qd+JۿG^GЗvH}ӻfJhuy"=~?:Owط7מhFmd ƃ~t3=; F&DWn-tͯjNJͻ=2;y?_.|ٜ]Jji~SW:ѣDyl*7j }땙rf$::t,1u^^y)H.Wre/ v{Ǭ&,8>&Ƿ}ag$[wqD3ѸuX)U%7zCUr*D1BcBz{h6)3 8ҙ:ةZU^mQMnZ=g'e'R}Tݔi6Nix漣r3EzMMҧ'bZsH)}>0v_ !ݘO1ɽDr})*7rP?dNgssmsrٳԸW[b,;K% JSOɻQon뽆(߇]rI:]Y?.O&GR:'ZZ08܌Vٌ9쭲e%x}.M1ݹ3Wru[BK] 1n{\KX{jꗕJZG@dⱒVˇăzJb.{k'n[gȗ}esL]N5bYqk%c4s/Gt+M4=sNDו0o܅ WJ|^ƱSRe7c.ES$ y֨8@=%՚|Lp1E|^B7\+YZxDhFK|seu2g.S0w9/dJg.mKLxN[OwӥH4|^l_gݍZF/lcg3%:k_`D b8Rk)/eۗR1w)zaKs7wTrvK%-?Zr(I_ڈ6sR5Rl_H,Hlӫ'"˵D+Tږ5-ܩJ>1ͺ$ʗi[>)NL:Q;{*c,Um^~Ew'}e\& [uR1rec\vy̮0.ƨ] ͂;ur$JAR{ݖvsrFgkihpnd! v"9V=9Ǚލg=.oA챨/^ .,/qo6 n/ͯ. Xo- }$net4S pS{oQ+<2Q|c~'\yccbzeQ+-EZ1\'[;L5[y[VNSV{sL&M.{v7=_[U;>iDUaһw뢾SS3~:GgʍB~+VHùzM>_wܕ77?fQ>|<]\7;%,UXo^_@gG}'FF{5s-էKCQQpiX/ue9Vs>G5gBrGrpU2Q! qTBG8*Q&sjB-|MNJ7Y6%!ӟ;Q2~kp,k:Q! qTBG8*Q&EY^*:{,۱{%x&k]h&B?tK#CQ,FSPƢM}QWa([)/ .B?HMXɋ e*i!% R3R@5R+K{ޙD*e4^N0ٳgju1-\tȇ\|ʜ?W\~z/Vś%S9Z;=ϞtRkR+'KElsotVZ[*7nFKߞlu K"߯L:VN& I~ama$CavU^6 Z/ϐ/J̺xIj1%RG&&gI_=*[@4_c-@46g*a@JVlxDžduRQki+b<O4V/ >'PCT }RO*I>'BԿ蓪Dee;hG]mXiZ˱繹>;ԕWEs-ɩM܏Dմ.^vrۦ9'[nBvxΜR8+sI@*=}53M|Z$t:@*ue-JG\B H@*R! TM u5&Nikykeɚ-˶ζ<ߟ1lxjK4r{l"_WM5#Ϛ{RR6 ]ib->#&!xdHe#uwT5uH@*R! TB HvdڽT3_ 4kV4S' zL( y\$WB:^۹[Q,$iVOHWCRR h sǥrKJO HUr>aF9`z4~yZ)Ҟ㎹B#YڇFU0o̅?\mB-H<$8)eηe9:f9C݌KWF'\HG?,&"w b" y4ۜD/cMmS=T٦eWieɣbcS;$SYCcGHETf!C"\iVПmI̥.}oz{;wJ,}ZS6vIG\m1Dq]u|;X%~GXe=>Rơø?S_SRґ.%T+RM_S}R9:BT Ss*tNΩ9o:^eſiY+MlڗM\ƣ2t#z.?⑚9 CN@p>]6z7v.Knr;CN> } G} IDJ!'DW!'D?CNn(9>Wj_9qdr5!:3#QN>#QECNΑGCN>CNڥ!'DrJ&Dg}HrE/|[+ne%DNn%$DrJMnVV+2Z+Nڤ*eisoGg"{'"/D9iB(\w#w֣<ώNBg[)BO~[? O:UT3SɩySa_1%۟HFNɹo]MMM^N) aoT'g殩+)L+)E}^N+)2RR4RowM kTpJw6ެ!"n9RRA>RRgTӀkTHtx{;6RR#^N) k)=~O+yc{_a핂MTѽRةTQR}|y>rE#)ůrR-55{CZMv43~ֶZȒX3IJU}R;)L 'aP*5}%s4J͑qYa#EIP7K=t*ga_1~a_1~a߿W[x)+sSi¦m^,\ܸj߷e¿iT,Bo D'MIgG-ǻsH !O0@#;l\,5)ڵ>d>#ƏQ^>)>ժgÉyHE  @ ՀB*>,HR R/b A*| zLjxxB:LUÃT=.\oCT#w}D F}P5&c;kXrv2<}$_Ţj߇TR1 Ҏ;JN_9+[s3ɇg̞p>5Z;LٗI%󛝕oW-]䛴P賝[qzuBrPL|-oa~a_a~a_a~KJx߼#Ua2{&Km]#ss*+'UhbvaNb N>yj N>Af'CN?C oPOpSCM?'B5lV N>AXL?'7DpՐOp pOp R~O5' 05&C*iZ#c;_Y_)ͅe_Op菦;vUk]$NkO4eel7]kÉ3o=8OW}hyOƢUwt{wO<ɣljÏP%2I@)g*6}R3Li^Md$/Y@!Sa:0_/L t~a:7>F:__oEc|&TV\~~h:w-k~TPbZ.J!{!cا%"$==ro*9}#$Vz}E?7Ka='z|L`*S! TB0L˹/^UC$3Ro<yZ9r]FoF.;ͱ[Cy2~-#WW+j2v?`J`KK0TLyvZDYx*Ax*3'5uT"be$&  „~aB0_/L&7=~QŅ̝96Y:۞HBKvMR7ϨڻUBA"j% ռҝݖUÞ76)|-ʲzmt,GMGNM֢ A-LnBi_Y*6}5WCgdHܩqBC3 z}*tBN :B'T P<יu^;Yؕm9Ob!}~eګ2z@nKOVd}<іeB4kOsuzqt 5u4 '[(J[RPS Z_)K2̏eӫ~F0*Q! aTB¨Fa0r՝tYMWYKOZ|ۋS8pqW46Ohj軥_:"U<rF9w{ lD9N iڹ(O,.<"80 ˞;_\(^{WF %!oGBmt/ؾ17>6: F %dJl F %τ?D % E %Ϭ辡1 F % MtPb8o0o(}#ѿ@6辡aD %fmtPbDJl_^ߏJly?#}GB~C/b+7&c,kśnzgX+O}nTĔ=Z:#zYLH Yի#-z>ObOQJkmQiWRYiRW_"5>Ph9' $\J$iTzTBT RW*J^+zBԿbI4SPWu&"+ۈ|&i]4֊Ndx$$ӪxX|ޚ~W-Q&G XnZHH9"$*iW맥<''E-ҝ5w(6ft&e.7.h"^)eRR1. ׹ʤAze9eUӞݑhPИp5xԡ%nxyTgQ[ʞVP/底|M?#MrZJwgy!8)\xu vӰl qZ [G}0V1m0SؿA\nlPwbWY7fpva8Qs#Sw*`JY߭<{Xjg=;@,M-|/:G$ٿXtL,K-YlfZ;Z=י_܍خXwdez<ُsϙI%>EEl"W]EDANH{RK+Ox>>Ga0Etq߂Xl~}Ԑ=j>Z:q}sMҺVS1kvOMvp͉+Ѭɶ,7 ZV̥׾:O+gUZbUS\=VũSVkwPG8))S3,+ue,:f-ﶱ>Wݍx:?! /D1BSA3Q{{)?(-aek̗X BsV,D-oaT6I=w%Z3 xQAb*JQPIg{W`ۭ7V!yۻvQN]>Wϕs\>Wϕs\?+'zJi\up"B޵Zz,J_+ܧtQn0;*?ZψlRLS j+7Du)-1} ퟐCK}4%58KIKIKIKIԟ4/Z|(3r*D=!m&SR8_vHQґXz8y{vDj 8J'B )&K.<qKll ؾ*F ǐRPMdpnspJ\R%\R%\R%\R%\RϒRܑxWLaQEw(=yRz"ApT.֝t.,=h@ٸe؇RgS6z" xr Zx51MLE.1S.1S.1S.1S.1'-4\xjB$⻹hC#@/h@vq}սE#X-ZSw O 6 @Jjrk)׶gIq^l:; ?0;6SSfb)&et² ',;I~av RA~av&f٩xq)avj=eq$d<>@8NA!?0;%!sB;NAYrOβS8w;,*i[*~O)-S0TC)WϲSPcO;O K\SI+f\BUN'Zԫ{I k8 exKjtYakQ:Xo29:KIa-c/ڽ]v}JP%!.@犦k*ibj%{qeb*=0T"`vJB;.;S.;S.;S.;S.;'٩?+iIf u{x6d [46+y :'BhnV vϕJU o v~J iXR"_Lmʵ-Cv@ zC?0Sݩmj[qoo3k\X ̚nG͊T&b>T0@0{5qvN|RG2ήo%EepD t#х%1X][1^8|hama-.9KɣONrc"@i uۛH͔?I%GxX*x3`o {%HDB(`m <`zMUSt8/_1: O]P'@8[݀ػZX֣8PL'<ЧwpۡQ O))<]NO)ilFe]]~b^D5I@<=XxOb/LfNb)/Ȇ¸U6.|C*xMUYclE䝣GdK, NDBWޒ] MۖM@ؼKiX>"kD"eb ),en =}m]T4tK9/"eý8r:~NI,'oph{p5Zu6' D*%\2A( vPK,=;Ҷ*ΡܑyhΙn7 $\fkva1ωyi;l@7[Sw}fk|J%_oYDP<L-d;/a w$q} h7DS׫1>xVg$2 Eff[${:"܀=V>s]^?krT)jɥ;cT ГˮwHxwb 63G@VMA6 D [6OgˍZ[kfp&>,ѦhKs0ʻؓ9ƾ㻇O!&wRi?<Վ$bq'{U c3 A0,&HpǙN in\op#^jcqFCzMKN/GHP-|p T1WY:zF8lK*o>-D ߀SS) |Zp~ca?iaZ|_ {wulgI/"gRq)4.e yM:U п.([yIlX/E0NÙq/>)ݮGKuV` ӽt,.u6G<x]Zۗ8)_Q%鬔 sPmpx Rqf,/Sd $}r Ie[zWk'j)仑oWvUʯ^b\&\Y>¹o 18rbRfV./M^."]z |\H#[p(]%SOп|#eL2C2,e@a@hֻgR6 [k6pO.Ҙ߫w /  .Ү#\TNլ- t%a݋jr.O)sk.6`#؍6ū M8\eĮ -TUKmT :Bʸ[ypAoB=1D,7]wbY9PpdO&"l{]w-m6?Ef{t ,GJwɿ^*#C[vH9^ٜIБ7I|ddc@n(p˻Ne)J>)(nJ5C1DLY80) >GGהwLymD@'ή'1f٭?NW?'םl,}ew8w \. |֊S|C^ߘ#\-65Kˤ(`s | nwy4\6v&4%6'W*KvO'ŸH?s2(?(^"alI8w,KE˓ȒII]YGeIk3#?AL|ܕqN7Aoom ytշ [ BtNsj쒥Z'uZɭ=ssLC-t2j+~7Oq4w nvi.ĒaH+ק2>% a.)Z`O7۷92X#hEK;3ΉV7\oDSV&/֍INR0ugxU^J,a^jFI9Ka*ԌqTnRpy)Ld[䁱INR%楦5R߷ ^ y)T/,7}$/)y)L9Kt|/eyזTvYRd~Z5xmB|p"N䎆60oH34GջKyWd47=ZKQ?ħDSvbh+TBSRS*WҔM4'>MYzaY>\4)MhES.rє\4T%:GE64(P(=66]*_®ÍNh갪N.a.9g9QC'h @CkS4䦪I4h  _lX] tpSp߷h*2)kvՃQ/R'gɞ_Efn׏ح ]A۽(>aG-?_ujx5~DpO zS: /7GbV.Ƈz tw [|8ޔ.\)Wo՛r\)Wo՛r\?7ؼE +uY-I lkh80^Qg1|X:ly9t&)h;6ޔ)@' ~NX̯6X2, !wGi?a !~P;:߄z^D›4/TߜrbS9Rb7NnY*v}F8R񲰍t UfvgTbⲄv|<ӶI_ze‡1KaɕJ90ϊ@C~J0Љ2Q+VC2Wg"؏J7B<-08=jk;JPIz `rԨqHIJ?rœ$$=%uΜ$eTX* A%)tCG <> *IOA8KPIz kQJST&؏9KPx$YNA] *R}]ԨN~IeJFqU4w{xwqC38M7Ic{+cuj,ү\-h_2kBC.!"SIE0оحV'~r *r *r *r *'0̟[Tk G`@Ge>@O⨈uK(6)aSZpv?5 _953<ͩ95IS"4)iqJ6ũ-lNIS2'mN#lNI`4u攴8erYSx|ۜ`w9ksJT9%-Ny|ۜaFdlN=؜Bm\ ڜ21`?.ۜi,iPvct6ZɎ/dOtY7l&x!衫j.Wn'wF-[]nst~)12stT nj_ Fm<n}l_*-"{R 0,Nэ'c] ȹ\S)kqʵ8Zr-Nũ-}Žq<bDV2}!8^=s#jj;kWQ X&#A`& @|:PbF0R[$"Grs`9}\N\N\N\N$'u*+A"Z}ko{zG葋++7HTW[tBxoq^77n >|#J(gÎw F3 7/ԕGU;Un^I/`2j=/ᯢ |ɨ!*߇Dۑ3SIVAx!u9.S> EBs!>8K(‰Y|5 f]mh\g]LTL3fQ̽|3.pԜW|fVfAd6KY|5*SJUWh̭Ƨ>pnjn,g8KOV@o`ʭ|֬T1-\ZcZ>p̎a: 3z&f}'sk`3+Xơ]iƴYœv ecYn'1 gͭQ[7M$S:ߴg/蒩΂cgybG[s8P8_80!4=/ W}@-a.WȾ Jǿ3o#0Q\|ʚ="SVe~h0MΈV\8vUlnҨ]./Ӓ.Ǔ=ħ)ijaôr\,s[˞9诘1(VE"T}o8[><Ctt5fOVFp2nV/#fIwR6X.dV+@@MM @1qy] t-DC^kE[9\WW[UX=W*NEA4q<(^u3UD&,CD͏`0_q"I?Q0@ftԆ0! :ͼ K8nA`z&\ًJ}V/C-VNj%/*w}pB&O!l 1?-s~Z6h~̑ bY ڷFT֬Eѻ#w:ܚ!۲bL/b4,z0 aɆZ8bö!wV'Rc :nE;A@,sǪV9 ]!dZ]V;w$I vmپݬ nbsj/Al}M@HJ,v-/[ lNÆ0K::Rwscz?\\!{8_,~lDBט\/CSŤ#OdHh3\ eZ>IGmmgH%#B,)f =2z9oF׽o$_dc ӂJv&DPK66tg`e{هm؈nmLJKQ.F`Id7rxZmK~;1]D 2|^I1:4S fiuy|B/D_P$hƹt1[`<" a%~S{!:UTBDQ]Sn/` y-լᶧDmSbG^ v9B 7&Xo3oݠ|x9)w ]8#w7܍ DnۯpRf& "َHwXS0nd[&IK9,w3x|l ݠ̿5x|N6%o䍣r7=ݾ#nLsMkKSA0K+_ |+L K><bݐFuZm=Kt+$x8Xj#30x\z_!C/!+-,1*ڿa1>gyaH fB e\e\e\e\e\ţVr+IeW v@jNho:GkZ=߿y]Dmޟau3Lק4t@GmsJ/HXN56[3 p,ew7W''}qdj4(VA3+\[弎Il"efHo\jxܞ[@`M7֢fP2쎌vJy=漪, 4IC fmlǽOqFΣ&j2zȓ7legX_޶c^T؆2O*rIy|}N!DBFR9,,Ga$uR#)ܮA8DrI!ٵ: P9⯡;`_/!$I,08vKÜERhHX9$%a=*1I`KR$<'Y$k?]$I,H VaF{~6ܯ꣇K'Hօ4kd2D5|  X7?<dAxus2,Ni\@w AUF`E cU]Esͨ^(o61' j&g#*7/o&_ˉGhDsz',ydk70 =됌PP<#J:~J4UI'T!4NqLpE6Uq`͉o _j֓s0UfM(xk#dL;%` .X|9e P3kiD[ѡ﯀( UX40aCa4bz0ݨ&L`8pD;fq > :ʲcZ3̓'c[m$)SLdz /@!4+0d4}'v|T>7G?͕`R4e=ja3\w7d< 'l endstream endobj 783 0 obj <> endobj xref 0 789 0000000003 65535 f 0000000016 00000 n 0000046524 00000 n 0000000004 00000 f 0000000006 00000 f 0000059667 00000 n 0000000007 00000 f 0000000008 00000 f 0000000009 00000 f 0000000010 00000 f 0000000011 00000 f 0000000012 00000 f 0000000013 00000 f 0000000014 00000 f 0000000018 00000 f 0000046575 00000 n 0000059428 00000 n 0000059459 00000 n 0000000019 00000 f 0000000020 00000 f 0000000021 00000 f 0000000022 00000 f 0000000023 00000 f 0000000027 00000 f 0000046646 00000 n 0000059312 00000 n 0000059343 00000 n 0000000031 00000 f 0000046718 00000 n 0000059196 00000 n 0000059227 00000 n 0000000035 00000 f 0000046790 00000 n 0000059080 00000 n 0000059111 00000 n 0000000039 00000 f 0000046861 00000 n 0000058964 00000 n 0000058995 00000 n 0000000043 00000 f 0000046932 00000 n 0000058848 00000 n 0000058879 00000 n 0000000047 00000 f 0000047003 00000 n 0000058732 00000 n 0000058763 00000 n 0000000051 00000 f 0000047074 00000 n 0000058616 00000 n 0000058647 00000 n 0000000055 00000 f 0000047145 00000 n 0000058500 00000 n 0000058531 00000 n 0000000059 00000 f 0000047216 00000 n 0000058384 00000 n 0000058415 00000 n 0000000063 00000 f 0000047287 00000 n 0000058268 00000 n 0000058299 00000 n 0000000067 00000 f 0000047359 00000 n 0000058152 00000 n 0000058183 00000 n 0000000071 00000 f 0000047431 00000 n 0000058036 00000 n 0000058067 00000 n 0000000072 00000 f 0000000073 00000 f 0000000074 00000 f 0000000075 00000 f 0000000076 00000 f 0000000077 00000 f 0000000078 00000 f 0000000079 00000 f 0000000080 00000 f 0000000081 00000 f 0000000082 00000 f 0000000083 00000 f 0000000084 00000 f 0000000085 00000 f 0000000086 00000 f 0000000087 00000 f 0000000088 00000 f 0000000089 00000 f 0000000090 00000 f 0000000091 00000 f 0000000092 00000 f 0000000093 00000 f 0000000094 00000 f 0000000095 00000 f 0000000096 00000 f 0000000097 00000 f 0000000098 00000 f 0000000099 00000 f 0000000100 00000 f 0000000101 00000 f 0000000102 00000 f 0000000103 00000 f 0000000104 00000 f 0000000105 00000 f 0000000106 00000 f 0000000107 00000 f 0000000108 00000 f 0000000109 00000 f 0000000110 00000 f 0000000111 00000 f 0000000112 00000 f 0000000113 00000 f 0000000114 00000 f 0000000115 00000 f 0000000116 00000 f 0000000117 00000 f 0000000118 00000 f 0000000119 00000 f 0000000120 00000 f 0000000121 00000 f 0000000122 00000 f 0000000123 00000 f 0000000124 00000 f 0000000125 00000 f 0000000129 00000 f 0000047503 00000 n 0000057918 00000 n 0000057950 00000 n 0000000130 00000 f 0000000131 00000 f 0000000132 00000 f 0000000133 00000 f 0000000134 00000 f 0000000135 00000 f 0000000136 00000 f 0000000137 00000 f 0000000138 00000 f 0000000139 00000 f 0000000140 00000 f 0000000141 00000 f 0000000142 00000 f 0000000143 00000 f 0000000144 00000 f 0000000145 00000 f 0000000146 00000 f 0000000147 00000 f 0000000148 00000 f 0000000149 00000 f 0000000150 00000 f 0000000151 00000 f 0000000155 00000 f 0000047578 00000 n 0000057800 00000 n 0000057832 00000 n 0000000156 00000 f 0000000157 00000 f 0000000158 00000 f 0000000159 00000 f 0000000160 00000 f 0000000164 00000 f 0000047652 00000 n 0000057682 00000 n 0000057714 00000 n 0000000168 00000 f 0000047727 00000 n 0000057564 00000 n 0000057596 00000 n 0000000172 00000 f 0000047802 00000 n 0000057446 00000 n 0000057478 00000 n 0000000176 00000 f 0000047876 00000 n 0000057328 00000 n 0000057360 00000 n 0000000180 00000 f 0000047950 00000 n 0000057210 00000 n 0000057242 00000 n 0000000184 00000 f 0000048024 00000 n 0000057092 00000 n 0000057124 00000 n 0000000188 00000 f 0000048098 00000 n 0000056974 00000 n 0000057006 00000 n 0000000192 00000 f 0000048172 00000 n 0000056856 00000 n 0000056888 00000 n 0000000196 00000 f 0000048246 00000 n 0000056738 00000 n 0000056770 00000 n 0000000200 00000 f 0000048320 00000 n 0000056620 00000 n 0000056652 00000 n 0000000204 00000 f 0000048395 00000 n 0000056502 00000 n 0000056534 00000 n 0000000208 00000 f 0000048470 00000 n 0000056384 00000 n 0000056416 00000 n 0000000209 00000 f 0000000210 00000 f 0000000211 00000 f 0000000212 00000 f 0000000213 00000 f 0000000214 00000 f 0000000215 00000 f 0000000216 00000 f 0000000217 00000 f 0000000218 00000 f 0000000219 00000 f 0000000220 00000 f 0000000221 00000 f 0000000222 00000 f 0000000223 00000 f 0000000224 00000 f 0000000225 00000 f 0000000226 00000 f 0000000227 00000 f 0000000228 00000 f 0000000229 00000 f 0000000230 00000 f 0000000231 00000 f 0000000232 00000 f 0000000233 00000 f 0000000234 00000 f 0000000235 00000 f 0000000236 00000 f 0000000237 00000 f 0000000238 00000 f 0000000239 00000 f 0000000240 00000 f 0000000241 00000 f 0000000242 00000 f 0000000243 00000 f 0000000244 00000 f 0000000245 00000 f 0000000246 00000 f 0000000247 00000 f 0000000248 00000 f 0000000249 00000 f 0000000250 00000 f 0000000251 00000 f 0000000252 00000 f 0000000253 00000 f 0000000254 00000 f 0000000255 00000 f 0000000256 00000 f 0000000257 00000 f 0000000258 00000 f 0000000259 00000 f 0000000260 00000 f 0000000261 00000 f 0000000262 00000 f 0000000266 00000 f 0000048545 00000 n 0000056266 00000 n 0000056298 00000 n 0000000267 00000 f 0000000268 00000 f 0000000269 00000 f 0000000270 00000 f 0000000271 00000 f 0000000272 00000 f 0000000273 00000 f 0000000274 00000 f 0000000275 00000 f 0000000276 00000 f 0000000277 00000 f 0000000278 00000 f 0000000279 00000 f 0000000280 00000 f 0000000281 00000 f 0000000282 00000 f 0000000283 00000 f 0000000284 00000 f 0000000285 00000 f 0000000286 00000 f 0000000287 00000 f 0000000288 00000 f 0000000292 00000 f 0000048620 00000 n 0000056148 00000 n 0000056180 00000 n 0000000293 00000 f 0000000294 00000 f 0000000295 00000 f 0000000296 00000 f 0000000297 00000 f 0000000301 00000 f 0000048694 00000 n 0000056030 00000 n 0000056062 00000 n 0000000305 00000 f 0000048769 00000 n 0000055912 00000 n 0000055944 00000 n 0000000309 00000 f 0000048844 00000 n 0000055794 00000 n 0000055826 00000 n 0000000313 00000 f 0000048918 00000 n 0000055676 00000 n 0000055708 00000 n 0000000317 00000 f 0000048992 00000 n 0000055558 00000 n 0000055590 00000 n 0000000321 00000 f 0000049066 00000 n 0000055440 00000 n 0000055472 00000 n 0000000325 00000 f 0000049140 00000 n 0000055322 00000 n 0000055354 00000 n 0000000329 00000 f 0000049214 00000 n 0000055204 00000 n 0000055236 00000 n 0000000333 00000 f 0000049288 00000 n 0000055086 00000 n 0000055118 00000 n 0000000337 00000 f 0000049362 00000 n 0000054968 00000 n 0000055000 00000 n 0000000341 00000 f 0000049437 00000 n 0000054850 00000 n 0000054882 00000 n 0000000345 00000 f 0000049512 00000 n 0000054732 00000 n 0000054764 00000 n 0000000346 00000 f 0000000347 00000 f 0000000348 00000 f 0000000349 00000 f 0000000350 00000 f 0000000351 00000 f 0000000352 00000 f 0000000353 00000 f 0000000354 00000 f 0000000355 00000 f 0000000356 00000 f 0000000357 00000 f 0000000358 00000 f 0000000359 00000 f 0000000360 00000 f 0000000361 00000 f 0000000362 00000 f 0000000363 00000 f 0000000364 00000 f 0000000365 00000 f 0000000366 00000 f 0000000367 00000 f 0000000368 00000 f 0000000369 00000 f 0000000370 00000 f 0000000371 00000 f 0000000372 00000 f 0000000373 00000 f 0000000374 00000 f 0000000375 00000 f 0000000376 00000 f 0000000377 00000 f 0000000378 00000 f 0000000379 00000 f 0000000380 00000 f 0000000381 00000 f 0000000382 00000 f 0000000383 00000 f 0000000384 00000 f 0000000385 00000 f 0000000386 00000 f 0000000387 00000 f 0000000388 00000 f 0000000389 00000 f 0000000390 00000 f 0000000391 00000 f 0000000392 00000 f 0000000393 00000 f 0000000394 00000 f 0000000395 00000 f 0000000396 00000 f 0000000397 00000 f 0000000398 00000 f 0000000399 00000 f 0000000403 00000 f 0000049587 00000 n 0000054614 00000 n 0000054646 00000 n 0000000404 00000 f 0000000405 00000 f 0000000406 00000 f 0000000407 00000 f 0000000408 00000 f 0000000409 00000 f 0000000410 00000 f 0000000411 00000 f 0000000412 00000 f 0000000413 00000 f 0000000414 00000 f 0000000415 00000 f 0000000416 00000 f 0000000417 00000 f 0000000418 00000 f 0000000419 00000 f 0000000420 00000 f 0000000421 00000 f 0000000422 00000 f 0000000423 00000 f 0000000424 00000 f 0000000428 00001 f 0000049662 00000 n 0000054496 00000 n 0000054528 00000 n 0000000432 00000 f 0000049734 00000 n 0000054378 00000 n 0000054410 00000 n 0000000436 00000 f 0000049811 00000 n 0000054260 00000 n 0000054292 00000 n 0000000440 00000 f 0000049880 00000 n 0000054142 00000 n 0000054174 00000 n 0000000444 00000 f 0000049949 00000 n 0000054024 00000 n 0000054056 00000 n 0000000448 00000 f 0000050018 00000 n 0000053906 00000 n 0000053938 00000 n 0000000452 00000 f 0000050087 00000 n 0000053788 00000 n 0000053820 00000 n 0000000456 00000 f 0000050156 00000 n 0000053670 00000 n 0000053702 00000 n 0000000460 00000 f 0000050225 00000 n 0000053552 00000 n 0000053584 00000 n 0000000464 00000 f 0000050294 00000 n 0000053434 00000 n 0000053466 00000 n 0000000468 00000 f 0000050363 00000 n 0000053316 00000 n 0000053348 00000 n 0000000472 00000 f 0000050436 00000 n 0000053198 00000 n 0000053230 00000 n 0000000473 00000 f 0000000474 00000 f 0000000475 00000 f 0000000476 00000 f 0000000477 00000 f 0000000478 00000 f 0000000479 00000 f 0000000480 00000 f 0000000481 00000 f 0000000482 00000 f 0000000483 00000 f 0000000484 00000 f 0000000485 00000 f 0000000486 00000 f 0000000487 00000 f 0000000488 00000 f 0000000489 00000 f 0000000490 00000 f 0000000491 00000 f 0000000492 00000 f 0000000493 00000 f 0000000494 00000 f 0000000495 00000 f 0000000496 00000 f 0000000497 00000 f 0000000498 00000 f 0000000499 00000 f 0000000500 00000 f 0000000501 00000 f 0000000502 00000 f 0000000503 00000 f 0000000504 00000 f 0000000505 00000 f 0000000506 00000 f 0000000507 00000 f 0000000508 00000 f 0000000509 00000 f 0000000510 00000 f 0000000511 00000 f 0000000512 00000 f 0000000513 00000 f 0000000514 00000 f 0000000515 00000 f 0000000516 00000 f 0000000517 00000 f 0000000518 00000 f 0000000519 00000 f 0000000520 00000 f 0000000521 00000 f 0000000522 00000 f 0000000523 00000 f 0000000524 00000 f 0000000525 00000 f 0000000526 00000 f 0000000527 00000 f 0000000528 00000 f 0000000529 00000 f 0000000530 00000 f 0000000531 00000 f 0000000532 00000 f 0000000533 00000 f 0000000534 00000 f 0000000535 00000 f 0000000536 00000 f 0000000537 00000 f 0000000538 00000 f 0000000539 00000 f 0000000540 00000 f 0000000541 00000 f 0000000542 00000 f 0000000543 00000 f 0000000544 00000 f 0000000545 00000 f 0000000546 00000 f 0000000547 00000 f 0000000548 00000 f 0000000549 00000 f 0000000550 00000 f 0000000551 00000 f 0000000552 00000 f 0000000553 00000 f 0000000554 00000 f 0000000555 00000 f 0000000556 00000 f 0000000557 00000 f 0000000558 00000 f 0000000559 00000 f 0000000560 00000 f 0000000561 00000 f 0000000562 00000 f 0000000563 00000 f 0000000564 00000 f 0000000565 00000 f 0000000566 00000 f 0000000567 00000 f 0000000568 00000 f 0000000569 00000 f 0000000570 00000 f 0000000571 00000 f 0000000572 00000 f 0000000573 00000 f 0000000574 00000 f 0000000575 00000 f 0000000576 00000 f 0000000577 00000 f 0000000578 00000 f 0000000579 00000 f 0000000583 00000 f 0000050521 00000 n 0000053080 00000 n 0000053112 00000 n 0000000584 00000 f 0000000585 00000 f 0000000586 00000 f 0000000587 00000 f 0000000588 00000 f 0000000589 00000 f 0000000590 00000 f 0000000591 00000 f 0000000592 00000 f 0000000593 00000 f 0000000594 00000 f 0000000595 00000 f 0000000596 00000 f 0000000597 00000 f 0000000598 00001 f 0000000599 00000 f 0000000600 00000 f 0000000601 00000 f 0000000602 00000 f 0000000609 00000 f 0000074725 00000 n 0000074801 00000 n 0000075002 00000 n 0000075965 00000 n 0000085088 00000 n 0000150677 00000 n 0000000618 00001 f 0000059544 00000 n 0000050604 00000 n 0000052962 00000 n 0000052994 00000 n 0000068138 00000 n 0000050676 00000 n 0000052844 00000 n 0000052876 00000 n 0000000622 00001 f 0000050753 00000 n 0000052726 00000 n 0000052758 00000 n 0000000626 00001 f 0000050822 00000 n 0000052608 00000 n 0000052640 00000 n 0000000630 00001 f 0000050891 00000 n 0000052490 00000 n 0000052522 00000 n 0000000634 00001 f 0000050960 00000 n 0000052372 00000 n 0000052404 00000 n 0000000638 00001 f 0000051029 00000 n 0000052254 00000 n 0000052286 00000 n 0000000642 00001 f 0000051098 00000 n 0000052136 00000 n 0000052168 00000 n 0000000646 00001 f 0000051167 00000 n 0000052018 00000 n 0000052050 00000 n 0000000650 00001 f 0000051236 00000 n 0000051900 00000 n 0000051932 00000 n 0000000654 00001 f 0000051305 00000 n 0000051782 00000 n 0000051814 00000 n 0000000658 00001 f 0000051378 00000 n 0000051664 00000 n 0000051696 00000 n 0000000659 00001 f 0000000660 00001 f 0000000661 00001 f 0000000662 00001 f 0000000663 00001 f 0000000664 00001 f 0000000665 00001 f 0000000666 00001 f 0000000667 00001 f 0000000668 00001 f 0000000669 00001 f 0000000670 00001 f 0000000671 00001 f 0000000672 00001 f 0000000673 00001 f 0000000674 00001 f 0000000675 00001 f 0000000676 00001 f 0000000677 00001 f 0000000678 00001 f 0000000679 00001 f 0000000680 00001 f 0000000681 00001 f 0000000682 00001 f 0000000683 00001 f 0000000684 00001 f 0000000685 00001 f 0000000686 00001 f 0000000687 00001 f 0000000688 00001 f 0000000689 00001 f 0000000690 00001 f 0000000691 00001 f 0000000692 00001 f 0000000693 00001 f 0000000694 00001 f 0000000695 00001 f 0000000696 00001 f 0000000697 00001 f 0000000698 00001 f 0000000699 00001 f 0000000700 00001 f 0000000701 00001 f 0000000702 00001 f 0000000703 00001 f 0000000704 00001 f 0000000705 00001 f 0000000706 00001 f 0000000707 00001 f 0000000708 00001 f 0000000709 00001 f 0000000710 00001 f 0000000711 00001 f 0000000712 00001 f 0000000713 00001 f 0000000714 00001 f 0000000715 00001 f 0000000716 00001 f 0000000717 00001 f 0000000718 00001 f 0000000719 00001 f 0000000720 00001 f 0000000721 00001 f 0000000722 00001 f 0000000723 00001 f 0000000724 00001 f 0000000725 00001 f 0000000726 00001 f 0000000727 00001 f 0000000728 00001 f 0000000729 00001 f 0000000730 00001 f 0000000731 00001 f 0000000732 00001 f 0000000733 00001 f 0000000734 00001 f 0000000735 00001 f 0000000736 00001 f 0000000737 00001 f 0000000738 00001 f 0000000739 00001 f 0000000740 00001 f 0000000741 00001 f 0000000742 00001 f 0000000743 00001 f 0000000744 00001 f 0000000745 00001 f 0000000746 00001 f 0000000747 00001 f 0000000748 00001 f 0000000749 00001 f 0000000750 00001 f 0000000751 00001 f 0000000752 00001 f 0000000753 00001 f 0000000754 00001 f 0000000755 00001 f 0000000756 00001 f 0000000757 00001 f 0000000758 00001 f 0000000759 00001 f 0000000760 00001 f 0000000761 00001 f 0000000762 00001 f 0000000763 00001 f 0000000764 00001 f 0000000765 00001 f 0000000769 00001 f 0000051463 00000 n 0000051546 00000 n 0000051578 00000 n 0000000770 00001 f 0000000771 00001 f 0000000772 00001 f 0000000773 00001 f 0000000774 00001 f 0000000775 00001 f 0000000776 00001 f 0000000777 00001 f 0000000000 00001 f 0000071475 00000 n 0000071512 00000 n 0000068252 00000 n 0000068648 00000 n 0000060267 00000 n 0000197228 00000 n 0000068910 00000 n 0000074161 00000 n 0000074211 00000 n 0000067924 00000 n 0000001203 00000 n trailer <<218D9257ADCA41849C96DA42A790832E>]>> startxref 197363 %%EOF ================================================ FILE: pylons/docs/en/_oldtemplates/genindex.html ================================================ {% extends "layout.html" %} {% set title = _('Index') %} {% block body %}

{{ _('Index') }}

{% for key, dummy in genindexentries -%} {{ key }} {% if not loop.last %}| {% endif %} {%- endfor %}
{% for key, entries in genindexentries %}

{{ key }}

{%- set breakat = genindexcounts[loop.index0] // 2 %} {%- set numcols = 1 %} {%- set numitems = 0 %} {% for entryname, (links, subitems) in entries %}
{%- if links -%}{{ entryname|e }} {%- for link in links[1:] %}, [{{ loop.index }}]{% endfor -%} {%- else -%} {{ entryname|e }} {%- endif -%}
{%- if subitems %}
{%- for subentryname, subentrylinks in subitems %}
{{ subentryname|e }} {%- for link in subentrylinks[1:] %}, [{{ loop.index }}]{% endfor -%}
{%- endfor %}
{%- endif -%} {%- set numitems = numitems + 1 + (subitems|length) -%} {%- if numcols < 2 and numitems > breakat -%} {%- set numcols = numcols+1 -%}
{%- endif -%} {%- endfor %}
{% endfor %} {% endblock %} {% block sidebarrel %} {% if split_index %}

{{ _('Index') }}

{% for key, dummy in genindexentries -%} {{ key }} {% if not loop.last %}| {% endif %} {%- endfor %}

{{ _('Full index on one page') }}

{% endif %} {{ super() }} {% endblock %} ================================================ FILE: pylons/docs/en/_oldtemplates/layout.html ================================================ {%- block doctype -%} {%- endblock %} {%- set reldelim1 = reldelim1 is not defined and ' »' or reldelim1 %} {%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %} {%- macro relbar() %} {%- endmacro %} {%- macro sidebar() %} {%- if builder != 'htmlhelp' %}
{%- block sidebarlogo %} {%- if logo %} {%- endif %} {%- endblock %} {%- block sidebartoc %} {%- if display_toc %}

{{ _('Table Of Contents') }}

{{ toc }} {%- endif %} {%- endblock %} {%- block sidebarrel %} {%- if prev %}

{{ _('Previous topic') }}

{{ prev.title }}

{%- endif %} {%- if next %}

{{ _('Next topic') }}

{{ next.title }}

{%- endif %} {%- endblock %} {%- if sourcename %}

{{ _('This Page') }}

{%- endif %} {%- if customsidebar %} {{ rendertemplate(customsidebar) }} {%- endif %} {%- block sidebarsearch %} {%- if pagename != "search" %}

{% if builder == 'web' %}{{ _('Keyword search')}}{% else %}{{ _('Quick search') }}{% endif %}

{%- if builder == 'web' %}

{{ _('Enter a module, class or function name.') }}

{%- endif %} {%- endif %} {%- endblock %}
{%- endif %} {%- endmacro -%} {%- if builder != 'htmlhelp' %} {%- set titlesuffix = " — " + docstitle %} {%- endif %} {{ title|striptags }}{{ titlesuffix }} {%- if builder == 'web' %} {%- for link, type, title in page_links %} {%- endfor %} {%- else %} {%- endif %} {%- if builder != 'htmlhelp' %} {%- for scriptfile in script_files %} {%- endfor %} {%- if use_opensearch %} {%- endif %} {%- if favicon %} {%- endif %} {%- endif %} {%- block rellinks %} {%- if hasdoc('about') %} {%- endif %} {%- if hasdoc('copyright') %} {%- endif %} {%- if parents %} {%- endif %} {%- if next %} {%- endif %} {%- if prev %} {%- endif %} {%- endblock %} {%- block extrahead %}{% endblock %} {%- block relbar1 %}{{ relbar() }}{% endblock %} {%- block sidebar1 %}{# possible location for sidebar #}{% endblock %} {%- block document %}
{%- if builder != 'htmlhelp' %}
{%- endif %}
{% block body %}{% endblock %}
{%- if builder != 'htmlhelp' %}
{%- endif %}
{%- endblock %} {%- block sidebar2 %}{{ sidebar() }}{% endblock %}
{%- block relbar2 %}{{ relbar() }}{% endblock %} {%- block footer %} {%- endblock %} ================================================ FILE: pylons/docs/en/_oldtemplates/modindex.html ================================================ {% extends "layout.html" %} {% set title = _('Global Module Index') %} {% block extrahead %} {{ super() }} {% if builder != 'htmlhelp' and collapse_modindex %} {% endif %} {% endblock %} {% block body %}

{{ _('Global Module Index') }}

{% if builder == 'web' and freqentries %}

{{ _('Most popular modules:') }}

{%- for module in freqentries %} {{ module.name|e }} {%- endfor %}
{% endif %} {% if builder == 'web' %}
{{ _('Show modules only available on these platforms') }}:
{% for pl in platforms -%} {% endfor %}
{% endif %} {%- for letter in letters %} {{ letter }} {% if not loop.last %}| {% endif %} {%- endfor %}
{%- for modname, collapse, cgroup, indent, fname, synops, pform, dep, stripped in modindexentries %} {%- if not modname -%} {%- else -%} {%- endif -%} {% endfor %}
 
{{ fname }}
{% if collapse -%} {%- endif %} {% if indent %}   {% endif %} {% if fname %}{% endif -%} {{ stripped|e }}{{ modname|e }} {%- if fname %}{% endif %} {%- if pform[0] %} ({{ pform|join(', ') }}){% endif -%} {% if dep %}{{ _('Deprecated')}}:{% endif %} {{ synops|e }}
{% endblock %} ================================================ FILE: pylons/docs/en/_oldtemplates/page.html ================================================ {% extends "layout.html" %} {% set page_links = [ (pathto('@rss/' + sourcename), 'application/rss+xml', 'Page Comments'), ] %} {% block body %} {% if oldurl %}
{% trans %}Note: You requested an out-of-date URL from this server. We've tried to redirect you to the new location of this page, but it may not be the right one.{% endtrans %}
{% endif %} {{ body }} {% endblock %} ================================================ FILE: pylons/docs/en/advanced_models.rst ================================================ .. _advanced_models: =============== Advanced Models =============== Pylons works well with many different types of databases, in addition to other database object-relational mappers. Advanced SQLAlchemy =================== Alternative SQLAlchemy Styles ----------------------------- In addition to the declarative style, SQLAlchemy has a default more verbose and explicit approach. Definitions using the default SQLAlchemy approach ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Here is a sample :file:`model/__init__.py` with a "persons" table, based on the default SQLAlchemy approach: .. code-block:: python """The application's model objects""" import sqlalchemy as sa from sqlalchemy import orm from myapp.model import meta def init_model(engine): meta.Session.configure(bind=engine) meta.engine = engine t_persons = sa.Table("persons", meta.metadata, sa.Column("id", sa.types.Integer, primary_key=True), sa.Column("name", sa.types.String(100), primary_key=True), sa.Column("email", sa.types.String(100)), ) class Person(object): pass orm.mapper(Person, t_persons) This model has one table, "persons", assigned to the variable ``t_persons``. :class:`Person` is an ORM class which is bound to the table via the mapper. Relation example ++++++++++++++++ Here's an example of a `Person` and an `Address` class with a many:many relationship on `people.my_addresses`. See `Relational Databases for People in a Hurry `_ and the `SQLAlchemy manual`_ for details. .. code-block:: python t_people = sa.Table('people', meta.metadata, sa.Column('id', sa.types.Integer, primary_key=True), sa.Column('name', sa.types.String(100)), sa.Column('email', sa.types.String(100)), ) t_addresses_people = sa.Table('addresses_people', meta.metadata, sa.Column('id', sa.types.Integer, primary_key=True), sa.Column('person_id', sa.types.Integer, sa.ForeignKey('people.id')), sa.Column('address_id', sa.types.Integer, sa.ForeignKey('addresses.id')), ) t_addresses = sa.Table('addresses', meta.metadata, sa.Column('id', sa.types.Integer, primary_key=True), sa.Column('address', sa.types.String(100)), ) class Person(object): pass class Address(object): pass orm.mapper(Address, t_addresses) orm.mapper(Person, t_people, properties = { 'my_addresses' : orm.relation(Address, secondary = t_addresses_people), }) Definitions using "reflection" of an existing database table ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If the table already exists, SQLAlchemy can read the column definitions directly from the database. This is called *reflecting* the table. The advantage of this approach is that it allows you to dispense with the task of specifying the column types in Python code. Reflecting existing database tables must be done inside :func:`init_model` because to perform the reflection, a live database engine is required and this is not available when the module is imported. A live database engine is bound explicitly in the :func:`init_model` function and so enables reflection. (An *engine* is a SQLAlchemy object that knows how to connect to a particular database.) Here's the second example with reflection: .. code-block:: python """The application's model objects""" import sqlalchemy as sa from sqlalchemy import orm from myapp.model import meta def init_model(engine): """Call me before using any of the tables or classes in the model""" # Reflected tables must be defined and mapped here global t_persons t_persons = sa.Table("persons", meta.metadata, autoload=True, autoload_with=engine) orm.mapper(Person, t_persons) meta.Session.configure(bind=engine) meta.engine = engine t_persons = None class Person(object): pass Note how ``t_persons`` and the :func:`orm.mapper` call moved into :func:`init_model`, while the ``Person`` class didn't have to. Also note the ``global t_persons`` statement. This tells Python that ``t_persons`` is a global variable outside the function. ``global`` is required when assigning to a global variable inside a function. It's not required if you're merely modifying a mutable object in place, which is why ``meta`` doesn't have to be declared global. Using the model standalone ^^^^^^^^^^^^^^^^^^^^^^^^^^ You now have everything necessary to use the model in a standalone script such as a cron job, or to test it interactively. You just need to create a SQLAlchemy engine and connect it to the model. This example uses a database "test.sqlite" in the current directory: .. code-block:: pycon % python Python 2.5.1 (r251:54863, Oct 5 2007, 13:36:32) [GCC 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)] on linux2 Type "help", "copyright", "credits" or "license" for more information. >>> import sqlalchemy as sa >>> engine = sa.create_engine("sqlite:///test.sqlite") >>> from myapp import model >>> model.init_model(engine) Now you can use the tables, classes, and Session as described in the `SQLAlchemy manual`_. For example: .. code-block:: python #!/usr/bin/env python import sqlalchemy as sa import tmpapp.model as model import tmpapp.model.meta as meta DB_URL = "sqlite:///test.sqlite" engine = sa.create_engine(DB_URL) model.init_model(engine) # Create all tables, overwriting them if they exist. if hasattr(model, "_Base"): # SQLAlchemy 0.5 Declarative syntax model._Base.metadata.drop_all(bind=engine, checkfirst=True) model._Base.metadata.create_all(bind=engine) else: # SQLAlchemy 0.4 and 0.5 syntax without Declarative meta.metadata.drop_all(bind=engine, checkfirst=True) meta.metadataa.create_all(bind=engine) # Create two records and insert them into the database using the ORM. a = model.Person() a.name = "Aaa" a.email = "aaa@example.com" meta.Session.add(a) b = model.Person() b.name = "Bbb" b.email = "bbb@example.com" meta.Session.add(b) meta.Session.commit() # Display all records in the persons table. print "Database data:" for p in meta.Session.query(model.Person): print "id:", p.id print "name:", p.name print "email:", p.email print .. _multiple_databases: Talking to Multiple Databases at Once ------------------------------------- Some applications need to connect to multiple databases (engines). Some always bind certain tables to the same engines (e.g., a general database and a logging database); this is called "horizontal partitioning". Other applications have several databases with the same structure, and choose one or another depending on the current request. A blogging app with a separate database for each blog, for instance. A few large applications store different records from the same logical table in different databases to prevent the database size from getting too large; this is called "vertical partitioning" or "sharding". The pattern above can accommodate any of these schemes with a few minor changes. First, you can define multiple engines in your config file like this: .. code-block:: ini sqlalchemy.default.url = "mysql://..." sqlalchemy.default.pool_recycle = 3600 sqlalchemy.log.url = "sqlite://..." This defines two engines, "default" and "log", each with its own set of options. Now you have to instantiate every engine you want to use. .. code-block:: python default_engine = engine_from_config(config, 'sqlalchemy.default.') log_engine = engine_from_config(config, 'sqlalchemy.log.') init_model(default_engine, log_engine) Of course you'll have to modify `init_model()` to accept both arguments and create two engines. To bind different tables to different databases, but always with a particular table going to the same engine, use the `binds` argument to `sessionmaker` rather than `bind`: .. code-block:: python binds = {"table1": engine1, "table2": engine2} Session = scoped_session(sessionmaker(binds=binds)) To choose the bindings on a per-request basis, skip the sessionmaker bind(s) argument, and instead put this in your base controller's `\_\_call\_\_` method before the superclass call, or directly in a specific action method: .. code-block:: python meta.Session.configure(bind=meta.engine) `binds=` works the same way here too. Multiple Application Instances ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If you're running multiple instances of the _same_ Pylons application in the same WSGI process (e.g., with Paste HTTPServer's "composite" application), you may run into concurrency issues. The problem is that :class:`Session` is thread local but not application-instance local. We're not sure how much this is really an issue if ``Session.remove()`` is properly called in the base controller, but just in case it becomes an issue, here are possible remedies: 1) Attach the engine(s) to ``pylons.g`` (aka. ``config["pylons.g"]``) rather than to the `meta` module. The globals object is not shared between application instances. 2) Add a scoping function. This prevents the application instances from sharing the same session objects. Add the following function to your model, and pass it as the second argument to `scoped_session`: .. code-block:: python def pylons_scope(): import thread from pylons import config return "Pylons|%s|%s" % (thread.get_ident(), config._current_obj()) Session = scoped_session(sessionmaker(), pylons_scope) If you're affected by this, or think you might be, please bring it up on the pylons-discuss mailing list. We need feedback from actual users in this situation to verify that our advice is correct. Non-SQLAlchemy libraries ======================== Most of these expose only the object-relational mapper; their SQL builder and connection pool are not meant to be used directly. `Storm `_ `Geniusql `_ DB-API ------ All the SQL libraries above are built on top of Python's DB-API, which provides a common low-level interface for interacting with several database engines: MySQL, PostgreSQL, SQLite, Oracle, Firebird, MS-SQL, Access via ODBC, etc. Most programmers do not use DB-API directly because its API is low-level and repetitive and does not provide a connection pool. There's no "DB-API package" to install because it's an abstract interface rather than software. Instead, install the Python package for the particular engine you're interested in. Python's `Database Topic Guide `_ describes the DB-API and lists the package required for each engine. The `sqlite3 `_ package for SQLite is included in Python 2.5. Object Databases ================ Object databases store Python dicts, lists, and classes in pickles, allowing you to access hierarchical data using normal Python statements rather than having to map them to tables, relations, and a foreign language (SQL). `ZODB `_ `Durus `_ [#]_ .. [#] Durus is not thread safe, so you should use its server mode if your application writes to the database. Do not share connections between threads. ZODB is thread safe, so it may be a more convenient alternative. Popular No-SQL Databases ======================== Pylons can also work with other database systems, such as the following: `Schevo `_ uses Durus to combine some features of relational and object databases. It is written in Python. `CouchDb `_ is a document-based database. It features a `Python API `_. The Datastore database in Google App Engine. ================================================ FILE: pylons/docs/en/advanced_pylons/creating_paste_templates.rst ================================================ .. _creating_paste_templates: ======================== Creating Paste templates ======================== Introduction ============ `Python Paste `_ is an extremely powerful package that isn't just about WSGI middleware. The related document :ref:`entry_points_and_plugins` demonstrates how to use entry_points to create simple plugins. This document describes how to write just such a plugin for use Paste's project template creation facility and how to add a command to Paste's ``paster`` script. The example task is to create a template for an imaginary content management system. The template is going to produce a project directory structure for a Python package, so we need to be able to specify a package name. Creating The Directory Structure and Templates ============================================== The directory structure for the new project needs to look like this: .. code-block:: text - default_project - +package+ - __init__.py - static - layout - region - renderer - service - layout - __init__.py - region - __init__.py - renderer - __init__.py - setup.py_tmpl - setup.cfg_tmpl - development.ini_tmpl - README.txt_tmpl - ez_setup.py Of course, the actual project's directory structure might look very different. In fact the ``paster create`` command can even be used to generate directory structures which *aren't* project templates --- although this wasn't what it was designed for. When the ``paster create`` command is run, any directories with ``+package+`` in their name will have that portion of the name replaced by a simplified package name and likewise any directories with ``+egg+`` in their name will have that portion replaced by the name of the egg directory, although we don't make use of that feature in this example. All of the files with ``_tmpl`` at the end of their filenames are treated as templates and will have the variables they contain replaced automatically. All other files will remain unchanged. .. note:: The small templating language used with ``paster create`` in files ending in ``_tmpl`` is described in detail in the `Paste util module documentation `_ When specifying a package name it can include capitalisation and ``_`` characters but it should be borne in mind that the actual name of the package will be the *lowercase* package name with the ``_`` characters removed. If the package name contains an ``_``, the egg name will contain a ``_`` character so occasionally the ``+egg+`` name is different to the ``+package+`` name. To avoid difficulty always recommend to users that they stick with package names that contain no ``_`` characters so that the names remain unique when made lowercase. Implementing the Code ===================== Now that the directory structure has been defined, the next step is to implement the commands that will convert this to a ready-to-run project. The template creation commands are implemented by a class derived from ``paste.script.templates.Template``. This is how our example appears: .. code-block:: python from paste.script.templates import Template, var vars = [ var('version', 'Version (like 0.1)'), var('description', 'One-line description of the package'), var('long_description', 'Multi-line description (in reST)'), var('keywords', 'Space-separated keywords/tags'), var('author', 'Author name'), var('author_email', 'Author email'), var('url', 'URL of homepage'), var('license_name', 'License name'), var('zip_safe', 'True/False: if the package can be distributed as a .zip file', default=False), ] class ArtProjectTemplate(Template): _template_dir = 'templates/default_project' summary = 'Art project template' vars = vars The ``vars`` arguments can all be set at run time and will be available to be used as (in this instance) Cheetah template variables in the files which end ``_tmpl``. For example the ``setup.py_tmpl`` file for the ``default_project`` might look like this: .. code-block:: html+mako from setuptools import setup, find_packages version = ${repr(version)|"0.0"} setup(name=${repr(project)}, version=version, description="${description|nothing}", long_description="""\ ${long_description|nothing}""", classifiers=[], keywords=${repr(keywords)|empty}, author=${repr(author)|empty}, author_email=${repr(author_email)|empty}, url=${repr(url)|empty}, license=${repr(license_name)|empty}, packages=find_packages(exclude=['ez_setup']), include_package_data=True, zip_safe=${repr(bool(zip_safe))|False}, install_requires=[ # Extra requirements go here # ], entry_points=""" [paste.app_factory] main=${package}:make_app """, ) .. note: The list of available classifier strings can be obtained from: ``http://www.python.org/pypi?%3Aaction=list_classifiers`` Note how the variables specified in ``vars`` earlier are used to generate the actual ``setup.py`` file. In order to use the new templates they must be hooked up to the ``paster create`` command by means of an entry point. In the ``setup.py`` file of the project (in which created the project template is going to be stored) we need to add the following: .. code-block:: python entry_points=""" [paste.paster_create_template] art_project=art.entry.template:ArtProjectTemplate """, We also need to add ``PasteScript>=1.3`` to the ``install_requires`` line. .. code-block:: python install_requires=["PasteScript>=1.3"], We just need to install the entry points now by running: .. code-block:: bash python setup.py develop We should now be able to see a list of available templates with this command: .. code-block:: bash $ paster create --list-templates .. note:: Windows users will need to add their Python scripts directory to their path or enter the full version of the command, similar to this: .. code-block:: bash C:\Python24\Scripts\paster.exe create --list-templates You should see the following: .. code-block:: text Available templates: art_project: Art project template basic_package: A basic setuptools-enabled package There may be other projects too. Troubleshooting =============== If the Art entries don't show up, check whether it is possible to import the ``template.py`` file because any errors are simply ignored by the paster create command rather than output as a warning. If the code is correct, the issue might be that the entry points data hasn't been updated. Examine the Python ``site-packages`` directory and delete the ``Art.egg-link`` files, any ``Art*.egg`` files or directories and remove any entries for art from ``easy_install.pth`` (replacing ``Art`` with the name chosen for the project of course). Then re-run ``python setup.py develop`` to install the correct information. If problems are still evident, then running the following code will print out a list of all entry points. It might help track the problem down: .. code-block:: python import pkg_resources for x in pkg_resources.iter_group_name(None, None): print x Using the Template =================== Now that the entry point is working, a new project can be created: .. code-block:: bash $ paster create --template=art TestProject Paster will ask lots of questions based on the variables set up in ``vars`` earlier. Pressing ``return`` will cause the default to be used. The final result is a nice project template ready for people to start coding with. Implementing Pylons Templates ============================= If the development context is subject to a frequent need to create lots of Pylons projects, each with a slightly different setup from the standard Pylons defaults then it is probably desirable to create a customised Pylons template to use when generating projects. This can be done in exactly the way described in this document. First, set up a new Python package, perhaps called something like ``CustomPylons`` (obviously, don't use the Pylons name because Pylons itself is already using it). Then check out the Pylons source code and copy the `pylons/templates/default_project `_ directory into the new project as a starting point. The next stage is to add the custom ``vars`` and ``Template`` class and set up the entry points in the ``CustomPylons`` ``setup.py`` file. After those tasks have been completed, it is then possible to create customised templates (ultimately based on the Pylons one) by using the ``CustomPylons`` package. ================================================ FILE: pylons/docs/en/advanced_pylons/entry_points_and_plugins.rst ================================================ .. _entry_points_and_plugins: =================================== Using Entry Points to Write Plugins =================================== Introduction ============ An entry point is a Python object in a project's code that is identified by a string in the project's ``setup.py`` file. The entry point is referenced by a group and a name so that the object may be discoverable. This means that another application can search for all the installed software that has an entry point with a particular group name, and then access the Python object associated with that name. This is extremely useful because it means it is possible to write plugins for an appropriately-designed application that can be loaded at run time. This document describes just such an application. It is important to understand that entry points are a feature of the new Python eggs package format and are *not* a standard feature of Python. To learn about eggs, their benefits, how to install them and how to set them up, see: * `Python Eggs `_ * `Easy Install `_ * `Setuptools `_ If reading the above documentation is inconvenient, suffice it to say that eggs are created via a similar ``setup.py`` file to the one used by Python's own `distutils `_ module --- except that eggs have some powerful extra features such as entry points and the ability to specify module dependencies and have them automatically installed by ``easy_install`` when the application itself is installed. For those developers unfamiliar with ``distutils``: it is the standard mechanism by which Python packages should be distributed. To use it, add a ``setup.py`` file to the desired project, insert the required metadata and specify the important files. The ``setup.py`` file can be used to issue various commands which create distributions of the pacakge in various formats for users to install. Creating Plugins ================ This document describes how to use entry points to create a plugin mechansim which allows new types of content to be added to a content management system but we are going to start by looking at the plugin. Say the standard way the CMS creates a plugin is with the ``make_plugin()`` function. In order for a plugin to be a plugin it must therefore have the function which takes the same arguments as the :func:`make_plugin` function and returns a plugin. We are going to add some image plugins to the CMS so we setup a project with the following directory structure: .. code-block:: text + image_plugins + __init__.py + setup.py The ``image_plugins/__init__.py`` file looks like this: .. code-block:: python def make_jpeg_image_plugin(): return "This would return the JPEG image plugin" def make_png_image_plugin(): return "This would return the PNG image plugin" We have now defined our plugins so we need to define our entry points. First lets write a basic ``setup.py`` for the project: .. code-block:: python from setuptools import setup, find_packages setup( name='ImagePlugins', version="1.0", description="Image plugins for the imaginary CMS 1.0 project", author="James Gardner", packages=find_packages(), include_package_data=True, ) When using ``setuptools`` we can specify the ``find_packages()`` function and ``include_package_data=True`` rather than having to manually list all the modules and package data like we had to do in the old ``distutils`` ``setup.py``. Because the plugin is designed to work with the (imaginary) CMS 1.0 package, we need to specify that the plugin requires the CMS to be installed too and so we add this line to the ``setup()`` function: .. code-block:: python install_requires=["CMS>=1.0"], Now when the plugins are installed, CMS 1.0 or above will be installed automatically if it is not already present. There are lots of other arguments such as ``author_email`` or ``url`` which you can add to the ``setup.py`` function too. We are interested in adding the entry points. We need to decide on a group name for the entry points. It is traditional to use the name of the package using the entry point, separated by a ``.`` character and then use a name that describes what the entry point does. For our example ``cms.plugin`` might be an appropriate name for the entry point. Since the ``image_plugin`` module contains two plugins we will need two entries. Add the following to the ``setup.py`` function: .. code-block:: python entry_points=""" [cms.plugin] jpg_image=image_plugin:make_jpeg_image_plugin png_image=image_plugin:make_png_image_plugin """, Group names are specified in square brackets, plugin names are specified in the format ``name=module.import.path:object_within_the_module``. The object doesn't have to be a function and can have any valid Python name. The module import path doesn't have to be a top level component as it is in this example and the name of the entry point doesn't have to be the same as the name of the object it is pointing to. The developer can add as many entries as desired in each group as long as the names are different and the same holds for adding groups. It is also possible to specify the entry points as a Python dictionary rather than a string if that approach is preferred. There are two more things we need to do to complete the plugin. The first is to include an ``ez_setup`` module so that if the user installing the plugin doesn't have ``setuptools`` installed, it will be installed for them. We do this by adding the following to the very top of the ``setup.py`` file before the import: .. code-block:: python from ez_setup import use_setuptools use_setuptools() We also need to download the ``ez_setup.py`` file into our project directory at the same level as ``setup.py``. .. note:: If you keep your project in SVN there is a `trick you can use with the `SVN:externals `_ to keep the ``ez_setup.py`` file up to date. Finally in order for the CMS to find the plugins we need to install them. We can do this with: .. code-block:: bash $ python setup.py install as usual or, since we might go on to develop the plugins further we can install them using a special development mode which sets up the paths to run the plugins from the source rather than installing them to Python's ``site-packages`` directory: .. code-block:: bash $ python setup.py develop Both commands will download and install ``setuptools`` if you don't already have it installed. Using Plugins ============= Now that the plugin is written we need to write the code in the CMS package to load it. Luckily this is even easier. There are actually lots of ways of discovering plugins. For example: by distribution name and version requirement (such as ``ImagePlugins>=1.0``) or by the entry point group and name (eg ``jpg_image``). For this example we are choosing the latter, here is a simple script for loading the plugins: .. code-block:: python from pkg_resources import iter_entry_points for entry_point in iter_entry_points(group='cms.plugin', name=None): print(entry_point) from pkg_resources import iter_entry_points available_methods = [] for entry_point in iter_entry_points(group='authkit.method', name=None): available_methods.append(entry_point.load()) Executing this short script, will result in the following output: .. code-block:: text This would return the JPEG image plugin This would return the PNG image plugin The ``iter_entry_points()`` function has looped though all the objects in the ``cms.plugin`` group and returned the function they were associated with. The application then called the function that the entry point was pointing to. We hope that we have demonstrated the power of entry points for building extensible code and developers are encouraged to read the `pkg_resources `_ module documentation to learn about some more features of the eggs format. ================================================ FILE: pylons/docs/en/advanced_pylons/index.rst ================================================ .. _advanced_pylons: =============== Advanced Pylons =============== .. toctree:: :maxdepth: 1 paster paster_commands creating_paste_templates entry_points_and_plugins ================================================ FILE: pylons/docs/en/advanced_pylons/paster.rst ================================================ .. _paster: WSGI, CLI scripts ================= Working with :class:`wsgiwrappers.WSGIRequest` ---------------------------------------------- Pylons uses a specialised *WSGIRequest* class that is accessible via the ``paste.wsgiwrappers`` module. The ``wsgiwrappers.WSGIRequest`` object represents a WSGI request that has a more programmer-friendly interface. This interface does not expose every detail of the WSGI environment *(why?)* and does not attempt to express anything beyond what is available in the environment dictionary. The only state maintained in this object is the desired ``charset``, an associated errors handler and a ``decode_param_names`` option. .. _note: *Unicode notes* When ``charset`` is set, the incoming parameter values will be automatically coerced to unicode objects of the charset encoding. When unicode is expected, ``charset`` will be overridden by the the value of the charset parameter set in the Content-Type header, if one was specified by the client. The incoming parameter names are not decoded to unicode unless the decode_param_names option is enabled. The class variable ``defaults`` specifies default values for charset, errors, and language. These default values can be overridden for the current request via the registry *(what's a registry?)*. The language default value is considered the fallback during i18n translations to ensure in odd cases that mixed languages don't occur should the language file contain the string but not another language in the accepted languages list. The language value only applies when getting a list of accepted languages from the HTTP Accept header. This behavior is duplicated from Aquarium, and may seem strange but is very useful. Normally, everything in the code is in "en-us". However, the "en-us" translation catalog is usually empty. If the user requests ["en-us", "zh-cn"] and a translation isn't found for a string in "en-us", you don't want gettext to fallback to "zh-cn". You want it to just use the string itself. Hence, if a string isn't found in the language catalog, the string in the source code will be used. All other state is kept in the environment dictionary; this is essential for interoperability. You are free to subclass this object. Attributes ---------- GET ^^^ A dictionary-like object representing the QUERY_STRING parameters. Always present, possibly empty. If the same key is present in the query string multiple times, a list of its values can be retrieved from the :class:`MultiDict` via the :meth:``getall`` method. Returns a :class:`MultiDict` container or, when charset is set, a :class:`UnicodeMultiDict`. POST ^^^^ A dictionary-like object representing the ``POST`` body. Most values are encoded strings, or unicode strings when charset is set. There may also be FieldStorage objects representing file uploads. If this is not a POST request, or the body is not encoded fields (e.g., an XMLRPC request) then this will be empty. This will consume wsgi.input when first accessed if applicable, but the raw version will be put in environ['paste.parsed_formvars']. Returns a MultiDict container or a UnicodeMultiDict when charset is set. cookies ^^^^^^^ A dictionary of cookies, keyed by cookie name. Just a plain dictionary, may be empty but not None. defaults ^^^^^^^^ .. code-block:: python {'errors': 'replace', 'decode_param_names': False, 'charset': None, 'language': 'en-us'} host ^^^^ The host name, as provided in ``HTTP_HOST`` with a fall-back to :envvar:`SERVER_NAME` is_xhr ^^^^^^ Returns a boolean if ``X-Requested-With`` is present and is a ``XMLHttpRequest`` languages ^^^^^^^^^ Returns a (possibly empty) list of preferred languages, most preferred first. params ^^^^^^ A dictionary-like object of keys from ``POST``, ``GET``, ``URL`` dicts Return a key value from the parameters, they are checked in the following order: POST, GET, URL Additional methods supported: ----------------------------- getlist(key) ^^^^^^^^^^^^ Returns a list of all the values by that key, collected from POST, GET, URL dicts Returns a :class:`MultiDict` container or a :class:`UnicodeMultiDict` when :data:`charset` is set. urlvars ^^^^^^^ Return any variables matched in the URL (e.g. wsgiorg.routing_args). Methods ------- __init__(self, environ) ^^^^^^^^^^^^^^^^^^^^^^^ determine_browser_charset(self) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Determine the encoding as specified by the browser via the Content-Type's ``charset parameter``, if one is set match_accept(self, mimetypes) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Return a list of specified mime-types that the browser's HTTP Accept header allows in the order provided. ================================================ FILE: pylons/docs/en/advanced_pylons/paster_commands.rst ================================================ .. _paster_commands: - Adding commands to paster ========================= Adding commands to Paster ========================= Paster command ============== The command line will be ``paster my-command arg1 arg2`` if the current directory is the application egg, or ``paster --plugin=MyPylonsApp my-command arg1 arg2`` otherwise. In the latter case, ``MyPylonsApp`` must have been installed via ``easy_install`` or ``python setup.py develop``. Make a package directory for your commands: .. code-block:: bash $ mkdir myapp/commands $ touch myapp/commands/__init__.py Create a module ``myapp/commands/my_command.py`` like this: .. code-block:: python from paste.script.command import Command class MyCommand(Command): # Parser configuration summary = "--NO SUMMARY--" usage = "--NO USAGE--" group_name = "myapp" parser = Command.standard_parser(verbose=False) def command(self): import pprint print "Hello, app script world!" print print "My options are:" print " ", pprint.pformat(vars(self.options)) print "My args are:" print " ", pprint.pformat(self.args) print print "My parser help is:" print print self.parser.format_help() .. note:: The class _must_ define ``.command``, ``.parser``, and ``.summary`` Modify the ``entry_points`` argument in :file:`setup.py` to contain: .. code-block:: python [paste.paster_command] my-command = myapp.commands.my_command:MyCommand Run ``python setup.py develop`` or ``easy_install .`` to update the entry points in the egg in sys.path. Now you should be able to run: .. code-block:: bash $ paster --plugin=MyApp my-command arg1 arg2 Hello, MyApp script world! My options are: {'interactive': False, 'overwrite': False, 'quiet': 0, 'verbose': 0} My args are: ['arg1', 'arg2'] My parser help is: Usage: /usr/local/bin/paster my-command [options] --NO USAGE-- --NO SUMMARY-- Options: -h, --help show this help message and exit $ paster --plugin=MyApp --help Usage: paster [paster_options] COMMAND [command_options] ... myapp: my-command --NO SUMMARY-- pylons: controller Create a Controller and accompanying functional test restcontroller Create a REST Controller and accompanying functional test shell Open an interactive shell with the Pylons app loaded Required class attributes ========================== In addition to the ``.command`` method, the class should define ``.parser`` and ``.summary``. Command-line options ==================== :func:`Command.standard_parser` returns a Python :obj:`OptionParser`. Calling ``parser.add_option`` enables the developer to add as many options as desired. Inside the ``.command`` method, the user's options are available under ``self.options``, and any additional arguments are in ``self.args``. There are several other class attributes that affect the parser; see them defined in ``paste.script.command:Command``. The most useful attributes are ``.usage``, ``.description``, ``.min_args``, and ``.max_args``. ``.usage`` is the part of the usage string _after_ the command name. The ``.standard_parser()`` method has several optional arguments to add standardized options; some of these got added to my parser although I don't see how. See the ``paster shell`` command, ``pylons.commands:ShellCommand``, for an example of using command-line options and loading the ``.ini file`` and model. Also see "paster setup-app" where it is defined in ``paste.script.appinstall.SetupCommand``. This is evident from the entry point in PasteScript (:file:`PasteScript-VERSION.egg/EGG_INFO/entry_points.txt`). It is a complex example of reading a config file and delegating to another entry point. The code for calling ``myapp.websetup:setup_config`` is in ``paste.script.appinstall``. The ``Command`` class also has several convenience methods to handle console prompts, enable logging, verify directories exist and that files have expected content, insert text into a file, run a shell command, add files to Subversion, parse "var=value" arguments, add variables to an .ini file. Using paster to access a Pylons app =================================== Paster provides ``request`` and ``post`` commands for running requests on an application. These commands will be run in the full configuration context of a normal application. Useful for cron jobs, the error handler will also be in place and you can get email reports of failed requests. Because arguments all just go in ``QUERY_STRING``, ``request.GET`` and ``request.PARAMS`` won't look like you expect. But you can parse them with something like: .. code-block:: python parser = optparse.OptionParser() parser.add_option(etc) args = [item[0] for item in cgi.parse_qsl(request.environ['QUERY_STRING'])] options, args = parser.parse_args(args) paster request / post --------------------- Usage: paster request / post [options] CONFIG_FILE URL [OPTIONS/ARGUMENTS] Run a request for the described application This command makes an artifical request to a web application that uses a ``paste.deploy`` configuration file for the server and application. Use 'paster request config.ini /url' to request ``/url``. Use 'paster post config.ini /url < data' to do a POST with the given request body. If the URL is relative (i.e. doesn't begin with /) it is interpreted as relative to /.command/. The variable ``environ['paste.command_request']`` will be set to True in the request, so your application can distinguish these calls from normal requests. Note that you can pass options besides the options listed here; any unknown options will be passed to the application in ``environ['QUERY_STRING']``. .. code-block:: none Options: -h, --help show this help message and exit -v, --verbose -q, --quiet -n NAME, --app-name=NAME Load the named application (default main) --config-var=NAME:VALUE Variable to make available in the config for %()s substitution (you can use this option multiple times) --header=NAME:VALUE Header to add to request (you can use this option multiple times) --display-headers Display headers before the response body Future development ------------------ A Pylons controller that handled some of this would probably be quite useful. Probably even nicer with additions to the current template, so that ``/.command/`` all gets routed to a single controller that uses actions for the various sub-commands, and can provide a useful response to ``/.command/?-h``, etc. ================================================ FILE: pylons/docs/en/caching.rst ================================================ .. _caching: ======= Caching ======= Inevitably, there will be occasions during applications development or deployment when some task is revealed to be taking a significant amount of time to complete. When this occurs, the best way to speed things up is with :term:`caching`. Caching is enabled in Pylons using `Beaker`_, the same package that provides session handling. Beaker supports a variety of caching backends: in-memory, database, Google Datastore, filesystem, and memcached. Additional extensions are available that support Tokyo Cabinet, Redis, Dynomite, and Ringo. Back-ends can be added with Beaker's extension system. .. seealso:: `Beaker Extension Add-ons `_ Types of Caching ================ Pylons offers a variety of caching options depending on the granularity of caching desired. Fine-grained caching down to specific sub-sections of a template, arbitrary Python functions, all the way up to entire controller actions and browser-side full-page caching are available. Available caching options (ordered by granularity, least to most specific): * **Browser-side** - HTTP/1.1 supports the :term:`ETag` caching system that allows the browser to use its own cache instead of requiring regeneration of the entire page. ETag-based caching avoids repeated generation of content but if the browser has never seen the page before, the page will still be generated. Therefore using ETag caching in conjunction with one of the other types of caching listed here will achieve optimal throughput and avoid unnecessary calls on resource-intensive operations. * **Controller Actions** - A Pylons controller action can have its entire result cached, including response headers if desired. * **Templates** - The results of an entire rendered template can be cached using the :meth:`3 cache keyword arguments to the render calls `. These render commands can also be used inside templates. * **Arbitrary Functions** - Any function can be independently cached using Beaker's cache decorators. This allows fine-grained caching of just the parts of the code that can be cached. * **Template Fragments** - Built-in caching options are available for both `Mako`_ and `Myghty `_ template engines. They allow fine-grained caching of only certain sections of the template. This is also sometimes called fragment caching since individual fragments of a page can be cached. Namespaces and Keys =================== `Beaker`_ is used for caching arbitrary Python functions, template results, and in `Mako`_ for caching individual `` blocks. Browser-side caching does *not* utilize `Beaker`_. The two primary concepts to bear in mind when caching with `Beaker`_ are: 1. Caches have a *namespace*, this is to organize a cache such that variations of the same thing being cached are associated under a single place. 2. Variations of something being cached, are *keys* which are under that namespace. For example, if we want to cache a function, the function name along with a unique name for it would be considered the *namespace*. The arguments it takes to differentiate the output to cache, are the *keys*. An example of caching with the :func:`~beaker.cache.cache_region` decorator:: @cache_region('short_term', 'search_func') def get_results(search_param): # do something to retrieve data data = get_data(search_param) return data results = get_results('gophers') In this example, the namespace will be the function name + module + 'search_func'. Since a single module might have multiple methods of the same name you wish to cache, the :func:`~beaker.cache.cache_region` decorator takes another argument in addition to the region to use, which is added to the namespace. The key in this example is the `search_param` value. For each value of it, a separate result will be cached. .. seealso:: Stephen Pierzchala's `Caching for Performance `_ (stephen@pierzchala.com) Beaker `Caching Docs `_ Configuring =========== `Beaker`_'s cache options can be easily configured in the project's INI file. Beaker's `configuration documentation `_ explains how to setup the most common options. The cache options specified will be used in the absence of more specific keyword arguments to individual cache functions. Functions that support :ref:`cache_regions` will use the settings for that region. .. _cache_regions: Cache Regions ------------- Cache regions are named groupings of related options. For example, in many web applications, there might be a few cache strategies used in a company, with short-term cached objects ending up in Memcached, and longer-term cached objects stored in the filesystem or a database. Using cache regions makes it easy to declare the cache strategies in one place, then use them throughout the application by referencing the cache strategy name. Cache regions should be setup in the :file:`development.ini` file, but can also be configured and passed directly into the `CacheManager` instance that is created in the :file:`lib/app_globals.py` file. Example INI section for two cache regions (put these under your `[app:main]` section): .. code-block:: ini beaker.cache.regions = short_term, long_term beaker.cache.short_term.type = ext:memcached beaker.cache.short_term.url = 127.0.0.1:11211 beaker.cache.short_term.expire = 3600 beaker.cache.long_term.type = ext:database beaker.cache.long_term.url = mysql://dbuser:dbpass@127.0.0.1/cache_db beaker.cache.long_term.expire = 86400 This sets up two cache regions, `short_term` and `long_term`. Browser-Side ============ Browser-side caching can utilize one of several methods. The entire page can have cache headers associated with it to indicate to the browser that it should be cached. Or, using the ETag Cache header, a page can have more fine-grained caching rules applied. Cache Headers ------------- Cache headers may be set directly on the :class:`~pylons.controllers.util.Response` object by setting the headers directly using the :meth:`~webob.response.Response.headers` property, or by using the cache header helpers. To ensure pages aren’t accidentally cached in dynamic web applications, Pylons default behavior sets the `Pragma` and `Cache-Control` headers to `no-cache`. Before setting cache headers, these default values should be cleared. Clearing the default `no-cache` response headers:: class SampleController(BaseController): def index(self): # Clear the default cache headers del response.headers['Cache-Control'] del response.headers['Pragma'] return render('/index.html) Using the response cache helpers:: # Set an action response to expires in 30 seconds class SampleController(BaseController): def index(self): # Clear the default cache headers del response.headers['Cache-Control'] del response.headers['Pragma'] response.cache_expires(seconds=30) return render('/index.html') # Set the cache-control to private with a max-age of 30 seconds class SampleController(BaseController): def index(self): # Clear the default cache headers del response.headers['Cache-Control'] del response.headers['Pragma'] response.cache_control = {'max-age': 30, 'public': True} return render('/index.html') All of the values that can be passed to the `cache_control` property dict, also may be passed into the `cache_expires` function call. It's recommended that you use the `cache_expires` helper as it also sets the Last-Modified and Expires headers to the second interval as well. .. seealso:: `Cache Control Header RFC `_ E-Tag Caching ------------- Caching via ETag involves sending the browser an ETag header so that it knows to save and possibly use a cached copy of the page from its own cache, instead of requesting the application to send a fresh copy. Because the ETag cache relies on sending headers to the browser, it works in a slightly different manner to the other caching mechanisms. The :func:`~pylons.controllers.util.etag_cache` function will set the proper HTTP headers if the browser doesn't yet have a copy of the page. Otherwise, a 304 HTTP Exception will be thrown that is then caught by Paste middleware and turned into a proper 304 response to the browser. This will cause the browser to use its own locally-cached copy. ETag-based caching requires a single key which is sent in the ETag HTTP header back to the browser. The `RFC specification for HTTP headers `_ indicates that an ETag header merely needs to be a string. This value of this string does not need to be unique for every URL as the browser itself determines whether to use its own copy, this decision is based on the URL and the ETag key. .. code-block:: python def my_action(self): etag_cache('somekey') return render('/show.myt', cache_expire=3600) Or to change other aspects of the response: .. code-block:: python def my_action(self): etag_cache('somekey') response.headers['content-type'] = 'text/plain' return render('/show.myt') The frequency with which an ETag cache key is changed will depend on the web application and the developer's assessment of how often the browser should be prompted to fetch a fresh copy of the page. Controller Actions ================== The :func:`~pylons.decorators.cache.beaker_cache` decorator is for caching the results of a complete controller action. Example: .. code-block:: python from pylons.decorators.cache import beaker_cache class SampleController(BaseController): # Cache this controller action forever (until the cache dir is # cleaned) @beaker_cache() def home(self): c.data = expensive_call() return render('/home.myt') # Cache this controller action by its GET args for 10 mins to memory @beaker_cache(expire=600, type='memory', query_args=True) def show(self, id): c.data = expensive_call(id) return render('/show.myt') By default the decorator uses a composite of all of the decorated function's arguments as the cache key. It can alternatively use a composite of the `request.GET` query args as the cache key when the `query_args` option is enabled. The cache key can be further customized via the `key` argument. .. warning:: By default, the :func:`~pylons.decorators.cache.beaker_cache` decorator will cache the entire response object. This means the headers that were generated during the action will be cached as well. This can be disabled by providing `cache_response = False` to the decorator. Templates ========= All :func:`render ` commands have caching functionality built in. To use it, merely add the appropriate cache keyword to the render call. .. code-block:: python class SampleController(BaseController): def index(self): # Cache the template for 10 mins return render('/index.html', cache_expire=600) def show(self, id): # Cache this version of the template for 3 mins return render('/show.html', cache_key=id, cache_expire=180) def feed(self): # Cache for 20 mins to memory return render('/feed.html', cache_type='memory', cache_expire=1200) def home(self, user): # Cache this version of a page forever (until the cache dir # is cleaned) return render('/home.html', cache_key=user, cache_expire='never') .. note:: At the moment, these functions do not support the use of cache region pre-defined argument sets. Arbitrary Functions =================== Any Python function that returns a pickle-able result can be cached using `Beaker`_. The recommended way to cache functions is to use the :meth:`~beaker.cache.cache_region` decorator. This decorator requires the :ref:`cache_regions` to be configured. Using the :meth:`~beaker.cache.cache_region` decorator:: @cache_region('short_term', 'search_func') def get_results(search_param): # do something to retrieve data data = get_data(search_param) return data results = get_results('gophers') .. seealso:: `Beaker Caching Documentation `_ Invalidating ------------ A cached function can be manually invalidated by using the :meth:`~beaker.cache.region_invalidate` function. Example:: region_invalidate(get_results, None, 'search_func', search_param) Fragments ========= Individual template files, and `` blocks within them can be independently cached. Since the caching system utilizes `Beaker`_, any available `Beaker`_ back-ends are present in `Mako`_ as well. Example:: <%def name="mycomp" cached="True" cache_timeout="30" cache_type="memory"> other text .. seealso:: `Mako Caching Documentation `_ .. _cache: http://en.wikipedia.org/wiki/Cache .. _Beaker: http://beaker.groovie.org .. _Mako: http://www.makotemplates.org/ ================================================ FILE: pylons/docs/en/concepts.rst ================================================ .. _concepts: ================== Concepts of Pylons ================== Understanding the basic concepts of Pylons, the flow of a request and response through the stack and how Pylons operates makes it easier to customize when needed, in addition to clearing up misunderstandings about why things behave the way they do. This section acts as a basic introduction to the concept of a :term:`WSGI` application, and :term:`WSGI Middleware` in addition to showing how Pylons utilizes them to assemble a complete working web framework. To follow along with the explanations below, create a project following the :ref:`getting_started` Guide. ***************************** The 'Why' of a Pylons Project ***************************** A new Pylons project works a little differently than in many other web frameworks. Rather than loading the framework, which then finds a new projects code and runs it, Pylons creates a Python package that does the opposite. That is, when its run, it imports objects from Pylons, assembles the WSGI Application and stack, and returns it. If desired, a new project could be completely cleared of the Pylons imports and run any arbitrary WSGI application instead. This is done for a greater degree of freedom and flexibility in building a web application that works the way the developer needs it to. By default, the project is configured to use standard components that most developers will need, such as sessions, template engines, caching, high level request and response objects, and an :term:`ORM`. By having it all setup in the project (rather than hidden away in 'framework' code), the developer is free to tweak and customize as needed. In this manner, Pylons has setup a project with its *opinion* of what may be needed by the developer, but the developer is free to use the tools needed to accomplish the projects goals. Pylons offers an unprecedented level of customization by exposing its functionality through the project while still maintaining a remarkable amount of simplicity by retaining a single standard interface between core components (:term:`WSGI`). ***************** WSGI Applications ***************** WSGI is a basic specification known as :pep:`333`, that describes a method for interacting with a HTTP server. This involves a way to get access to HTTP headers from the request, and how set HTTP headers and return content on the way back out. A 'Hello World' WSGI Application: .. code-block :: python def simple_app(environ, start_response): start_response('200 OK', [('Content-type', 'text/html')]) return ['Hello World'] This WSGI application does nothing but set a 200 status code for the response, set the HTTP 'Content-type' header, and return some HTML. The WSGI specification lays out a `set of keys that will be set in the environ dict `_. The WSGI interface, that is, this method of calling a function (or method of a class) with two arguments, and handling a response as shown above, is used throughout Pylons as a standard interface for passing control to the next component. Inside a new project's :file:`config/middleware.py`, the `make_app` function is responsible for creating a WSGI application, wrapping it in WSGI middleware (explained below) and returning it so that it may handle requests from a HTTP server. .. _wsgi-middleware: *************** WSGI Middleware *************** Within :file:`config/middleware.py` a Pylons application is wrapped in successive layers which add functionality. The process of wrapping the Pylons application in middleware results in a structure conceptually similar to the layers in an onion. .. image:: _static/pylons_as_onion.png :alt: Pylons middleware onion analogy :align: center Once the middleware has been used to wrap the Pylons application, the make_app function returns the completed app with the following structure (outermost layer listed first): .. code-block:: text Registry Manager Status Code Redirect Error Handler Cache Middleware Session Middleware Routes Middleware Pylons App (WSGI Application) WSGI middleware is used extensively in Pylons to add functionality to the base WSGI application. In Pylons, the 'base' WSGI Application is the :class:`~pylons.wsgiapp.PylonsApp`. It's responsible for looking in the `environ` dict that was passed in (from the Routes Middleware). To see how this functionality is created, consider a small class that looks at the `HTTP_REFERER` header to see if it's Google: .. code-block :: python class GoogleRefMiddleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): environ['google'] = False if 'HTTP_REFERER' in environ: if environ['HTTP_REFERER'].startswith('http://google.com'): environ['google'] = True return self.app(environ, start_response) This is considered WSGI Middleware as it still can be called and returns like a WSGI Application, however, it's adding something to environ, and then calls a WSGI Application that it is initialized with. That's how the layers are built up in the `WSGI Stack` that is configured for a new Pylons project. Some of the layers, like the Session, Routes, and Cache middleware, only add objects to the `environ` dict, or add HTTP headers to the response (the Session middleware for example adds the session cookie header). Others, such as the Status Code Redirect, and the Error Handler may fully intercept the request entirely, and change how it's responded to. ******************* Controller Dispatch ******************* When the request passes down the middleware, the incoming URL gets parsed in the RoutesMiddleware, and if it matches a URL (See :ref:`url-config`), the information about the controller that should be called is put into the `environ` dict for use by :class:`~pylons.wsgiapp.PylonsApp`. The :class:`~pylons.wsgiapp.PylonsApp` then attempts to find a controller in the :file:`controllers` directory that matches the name of the controller, and searches for a class inside it by a similar scheme (controller name + 'Controller', ie, HelloController). Upon finding a controller, its then called like any other WSGI application using the same WSGI interface that :class:`~pylons.wsgiapp.PylonsApp` was called with. .. versionadded:: 1.0 Controller name can also be a dotted path to the module / callable that should be imported and called. For example, to use a controller named 'Foo' that is in the 'bar.controllers' package, the controller name would be `bar.controllers:Foo`. This is why the BaseController that resides in a project's :file:`lib/base.py` module inherits from :class:`~pylons.controllers.core.WSGIController` and has a `__call__` method that takes the `environ` and `start_response`. The :class:`~pylons.controllers.core.WSGIController` locates a method in the class that corresponds to the `action` that Routes found, calls it, and returns the response completing the request. ****** Paster ****** Running the :command:`paster` command all by itself will show the sets of commands it accepts: .. code-block :: bash $ paster Usage: paster [paster_options] COMMAND [command_options] Options: --version show program's version number and exit --plugin=PLUGINS Add a plugin to the list of commands (plugins are Egg specs; will also require() the Egg) -h, --help Show this help message Commands: create Create the file layout for a Python distribution grep Search project for symbol help Display help make-config Install a package and create a fresh config file/directory points Show information about entry points post Run a request for the described application request Run a request for the described application serve Serve the described application setup-app Setup an application, given a config file pylons: controller Create a Controller and accompanying functional test restcontroller Create a REST Controller and accompanying functional test shell Open an interactive shell with the Pylons app loaded If :command:`paster` is run inside of a Pylons project, this should be the output that will be printed. The last section, `pylons` will be absent if it is not run inside a Pylons project. This is due to a dynamic plugin system the :command:`paster` script uses, to determine what sets of commands should be made available. Inside a Pylons project, there is a directory ending in `.egg-info`, that has a :file:`paster_plugins.txt` file in it. This file is looked for and read by the :command:`paster` script, to determine what other packages should be searched dynamically for commands. Pylons makes several commands available for use in a Pylons project, as shown above. *********************** Loading the Application *********************** Running (and thus loading) an application is done using the :command:`paster` command: .. code-block :: bash $ paster serve development.ini This instructs the paster script to go into a 'serve' mode. It will attempt to load both a server and a WSGI application that should be served, by parsing the configuration file specified. It looks for a `[server]` block to determine what server to use, and an `[app]` block for what WSGI application should be used. The basic egg block in the :file:`development.ini` for a `helloworld` project: .. code-block :: ini [app:main] use = egg:helloworld That will tell paster that it should load the helloworld :term:`egg` to locate a WSGI application. A new Pylons application includes a line in the :file:`setup.py` that indicates what function should be called to make the WSGI application: .. code-block :: python entry_points=""" [paste.app_factory] main = helloworld.config.middleware:make_app [paste.app_install] main = pylons.util:PylonsInstaller """, Here, the `make_app` function is specified as the `main` WSGI application that Paste (the package that :command:`paster` comes from) should use. The `make_app` function from the project is then called, and the server (by default, a HTTP server) runs the WSGI application. ================================================ FILE: pylons/docs/en/conf.py ================================================ # -*- coding: utf-8 -*- # # Pylons documentation build configuration file, created by # sphinx-quickstart on Mon Apr 21 20:41:33 2008. # # This file is execfile()d with the current directory set to its containing dir. # # The contents of this file are pickled, so don't put values in the namespace # that aren't pickleable (module imports are okay, they're removed automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. import sys, os # If your extensions are in another directory, add it here. #sys.path.append('some/directory') sys.path.append(os.path.dirname(os.path.abspath(__file__))) # General configuration # --------------------- # 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 = { 'http://www.sqlalchemy.org/docs/': None, 'http://sluggo.scrapping.cc/python/WebHelpers/': None, 'http://routes.groovie.org/': None, 'http://beaker.groovie.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 master toctree document. master_doc = 'index' # General substitutions. project = 'Pylons Framework' copyright = '2008-2015, Ben Bangert, James Gardner, Philip Jenvey' # The default replacements for |version| and |release|, also used in various # other places throughout the built documents. # # The short X.Y version. version = '1.0.2' # The full version, including alpha/beta/rc tags. release = '1.0.2' # 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 documents that shouldn't be included in the build. #unused_docs = [] # 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' # Options for HTML output # ----------------------- # Add and use Pylons theme from subprocess import call, Popen, PIPE p = Popen('which git', shell=True, stdout=PIPE) git = p.stdout.read().strip() cwd = os.getcwd() _themes = os.path.join(cwd, '_themes') if not os.path.isdir(_themes): call([git, 'clone', 'git://github.com/Pylons/pylons_sphinx_theme.git', '_themes']) else: os.chdir(_themes) call([git, 'checkout', 'master']) call([git, 'pull']) os.chdir(cwd) sys.path.append(os.path.abspath('_themes')) html_theme_path = ['_themes'] html_theme = 'pylonsfw' html_theme_options = dict( github_url='https://github.com/Pylons/pylons' ) # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. #html_style = 'default.css' # 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 # Content template for the index page. #html_index = '' # 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_use_modindex = True # If true, the reST sources are included in the HTML build as _sources/. #html_copy_source = True # Output file base name for HTML help builder. htmlhelp_basename = 'Pylonsfwdoc' # 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, document class [howto/manual]). latex_documents = [ ('index', 'Pylons.tex', 'Pylons Reference Documentation', 'Ben Bangert, Graham Higgins, James Gardner, Philip Jenvey', 'manual', 'toctree_only'), ] # Additional stuff for the LaTeX preamble. latex_preamble = ''' \usepackage{palatino} \definecolor{TitleColor}{rgb}{0.7,0,0} \definecolor{InnerLinkColor}{rgb}{0.7,0,0} \definecolor{OuterLinkColor}{rgb}{0.8,0,0} \definecolor{VerbatimColor}{rgb}{0.985,0.985,0.985} \definecolor{VerbatimBorderColor}{rgb}{0.8,0.8,0.8} ''' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. latex_use_modindex = False ================================================ FILE: pylons/docs/en/configuration.rst ================================================ .. _configuration: ============= Configuration ============= Pylons comes with two main ways to configure an application: * The configuration file (:ref:`run-config`) * The application's ``config`` directory The files in the ``config`` directory change certain aspects of how the application behaves. Any options that the webmaster should be able to change during deployment should be specified in a configuration file. .. tip:: A good indicator of whether an option should be set in the ``config`` directory code vs. the configuration file is whether or not the option is necessary for the functioning of the application. If the application won't function without the setting, it belongs in the appropriate :file:`config/` directory file. If the option should be changed depending on deployment, it belongs in the :ref:`run-config`. The applications :file:`config/` directory includes: * :file:`config/environment.py` described in :ref:`environment-config` * :file:`config/middleware.py` described in :ref:`middleware-config` * :file:`config/deployment.ini_tmpl` described in :ref:`production-config` * :file:`config/routing.py` described in :ref:`url-config` Each of these files allows developers to change key aspects of how the application behaves. .. _run-config: ********************* Runtime Configuration ********************* When a new project is created a sample configuration file called :file:`development.ini` is automatically produced as one of the project files. This default configuration file contains sensible options for development use, for example when developing a Pylons application it is very useful to be able to see a debug report every time an error occurs. The :file:`development.ini` file includes options to enable debug mode so these errors are shown. Since the configuration file is used to determine which application is run, multiple configuration files can be used to easily toggle sets of options. Typically a developer might have a ``development.ini`` configuration file for testing and a ``production.ini`` file produced by the :command:`paster make-config` command for testing the command produces sensible production output. A :file:`test.ini` configuration is also included in the project for test-specific options. To specify a configuration file to use when running the application, change the last part of the :command:`paster serve` to include the desired config file: .. code-block :: bash $ paster serve production.ini .. seealso:: Configuration file format **and options** are described in great detail in the `Paste Deploy documentation `_. Getting Information From Configuration Files ============================================ All information from the configuration file is available in the ``pylons.config`` object. ``pylons.config`` also contains application configuration as defined in the project's :file:`config.environment` module. .. code-block :: python from pylons import config ``pylons.config`` behaves like a dictionary. For example, if the configuration file has an entry under the ``[app:main]`` block: .. code-block :: ini cache_dir = %(here)s/data That can then be read in the projects code: .. code-block :: python from pylons import config cache_dir = config['cache.dir'] Or the current debug status like this: .. code-block :: python debug = config['debug'] Evaluating Non-string Data in Configuration Files ------------------------------------------------- By default, all the values in the configuration file are considered strings. To make it easier to handle boolean values, the Paste library comes with a function that will convert ``true`` and ``false`` to proper Python boolean values: .. code-block :: python from paste.deploy.converters import asbool debug = asbool(config['debug']) This is used already in the default projects' :ref:`middleware-config` to toggle middleware that should only be used in development mode (with ``debug``) set to true. .. _production-config: Production Configuration Files ============================== To change the defaults of the configuration INI file that should be used when deploying the application, edit the :file:`config/deployment.ini_tmpl` file. This is the file that will be used as a template during deployment, so that the person handling deployment has a starting point of the minimum options the application needs set. One of the most important options set in the deployment ini is the ``debug = true`` setting. The email options should be setup so that errors can be e-mailed to the appropriate developers or webmaster in the event of an application error. Generating the Production Configuration --------------------------------------- To generate the production.ini file from the projects' :file:`config/deployment.ini_tmpl` it must first be installed either as an :term:`egg` or under development mode. Assuming the name of the Pylons application is ``helloworld``, run: .. code-block :: bash $ paster make-config helloworld production.ini .. note:: This command will also work from inside the project when its being developed. It is the responsibility of the developer to ensure that a sensible set of default configuration values exist when the webmaster uses the ``paster make-config`` command. .. warning:: **Always** make sure that the ``debug`` is set to ``false`` when deploying a Pylons application. .. _environment-config: *********** Environment *********** The :file:`config/environment.py` module sets up the basic Pylons environment variables needed to run the application. Objects that should be setup once for the entire application should either be setup here, or in the :file:`lib/app_globals` :meth:`__init__` method. It also calls the :ref:`url-config` function to setup how the URL's will be matched up to :ref:`controllers`, creates the :term:`app_globals` object, configures which module will be referred to as :term:`h`, and is where the template engine is setup. When using SQLAlchemy it's recommended that the SQLAlchemy engine be setup in this module. The default SQLAlchemy configuration that Pylons comes with creates the engine here which is then used in :file:`model/__init__.py`. .. _url-config: ***************** URL Configuration ***************** A Python library called Routes handles mapping URLs to controllers and their methods, or their :term:`action` as Routes refers to them. By default, Pylons sets up the following :term:`route`\s (found in :file:`config/routing.py`): .. code-block:: python map.connect('/{controller}/{action}') map.connect('/{controller}/{action}/{id}') .. versionchanged:: 0.9.7 Prior to Routes 1.9, all map.connect statements required variable parts to begin with a ``:`` like ``map.connect(':controller/:action')``. This syntax is now optional, and the new ``{}`` syntax is recommended. Any part of the path inside the curly braces is a variable (a `variable part` ) that will match any text in the URL for that 'part'. A 'part' of the URL is the text between two forward slashes. Every part of the URL must be present for the :term:`route` to match, otherwise a 404 will be returned. The routes above are translated by the Routes library into regular expressions for high performance URL matching. By default, all the variable parts (except for the special case of ``{controller}``) become a matching regular expression of ``[^/]+`` to match anything except for a forward slash. This can be changed easily, for example to have the ``{id}`` only match digits: .. code-block :: python map.connect('/{controller}/{action}/{id:\d+}') If the desired regular expression includes the ``{}``, then it should be specified separately for the variable part. To limit the ``{id}`` to only match at least 2-4 digits: .. code-block :: python map.connect('/{controller}/{action}/{id}', requirements=dict(id='\d{2,4}')) The controller and action can also be specified as keyword arguments so that they don't need to be included in the URL: .. code-block :: python # Archives by 2 digit year -> /archives/08 map.connect('/archives/{year:\d\d}', controller='articles', action='archives') Any variable part, or keyword argument in the ``map.connect`` statement will be available for use in the action used. For the route above, which resolves to the `articles` controller: .. code-block :: python class ArticlesController(BaseController): def archives(self, year): ... The part of the URL that matched as the year is available by name in the function argument. .. note:: Routes also includes the ability to attempt to 'minimize' the URL. This behavior is generally not intuitive, and starting in Pylons 0.9.7 is turned off by default with the ``map.minimization=False`` setting. The default mapping can match to any controller and any of their actions which means the following URLs will match: .. code-block:: text /hello/index >> controller: hello, action: index /entry/view/4 >> controller: entry, action: view, id:4 /comment/edit/2 >> controller: comment, action: edit, id:2 This simple scheme can be suitable for even large applications when complex URL's aren't needed. Controllers can be organized into directories as well. For example, if the admins should have a separate ``comments`` controller: .. code-block:: bash $ paster controller admin/comments Will create the ``admin`` directory along with the appropriate ``comments`` controller under it. To get to the comments controller: .. code-block:: text /admin/comments/index >> controller: admin/comments, action: index .. note:: The ``{controller}`` match is special, in that it doesn't always stop at the next forward slash (``/``). As the example above demonstrates, it is able to match controllers nested under a directory should they exist. Adding a route to match ``/`` ============================= The controller and action can be specified directly in the :meth:`map.connect` statement, as well as the raw URL to be matched: .. code-block:: python map.connect('/', controller='main', action='index') results in ``/`` being handled by the ``index`` method of the ``main`` controller. .. note:: By default, projects' static files (in the :file:`public/` directory) are served in preference to controllers. New Pylons projects include a welcome page (:file:`public/index.html`) that shows up at the ``/`` url. You'll want to remove this file before mapping a route there. Generating URLs =============== URLs are generated via the callable :class:`routes.util.URLGenerator` object. Pylons provides an instance of this special object at :data:`pylons.url`. It accepts keyword arguments indicating the desired controller, action and additional variables defined in a route. .. code-block:: python # generates /content/view/2 url(controller='content', action='view', id=2) To generate the URL of the matched route of the current request, call :meth:`routes.util.URLGenerator.current`: .. code-block:: python # Generates /content/view/3 during a request for /content/view/3 url.current() :meth:`routes.util.URLGenerator.current` also accepts the same arguments as `url()`. This uses `Routes memory `_ to generate a small change to the current URL without the need to specify all the relevant arguments: .. code-block:: python # Generates /content/view/2 during a request for /content/view/3 url.current(id=2) .. seealso:: `Routes manual `_ Full details and source code. .. _middleware-config: ********** Middleware ********** A projects WSGI stack should be setup in the :file:`config/middleware.py` module. Ideally this file should import middleware it needs, and set it up in the `make_app` function. The default stack that is setup for a Pylons application is described in detail in :ref:`wsgi-middleware`. Default middleware stack: .. code-block :: python # The Pylons WSGI app app = PylonsApp() # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map']) app = SessionMiddleware(app, config) app = CacheMiddleware(app, config) # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) # Establish the Registry for this application app = RegistryManager(app) if asbool(static_files): # Serve static files static_app = StaticURLParser(config['pylons.paths']['static_files']) app = Cascade([static_app, app]) return app Since each piece of middleware wraps the one before it, the stack needs to be assembled in reverse order from the order in which its called. That is, the very last middleware that wraps the WSGI Application, is the very first that will be called by the server. The last piece of middleware in the stack, called Cascade, is used to serve static content files during development. For top performance, consider disabling the Cascade middleware via setting the ``static_files = false`` in the configuration file. Then have the webserver or a :term:`CDN` serve static files. .. warning:: When unsure about whether or not to change the middleware, **don't**. The order of the middleware is important to the proper functioning of a Pylons application, and shouldn't be altered unless needed. Adding custom middleware ======================== Custom middleware should be included in the :file:`config/middleware.py` at comment marker:: # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) For example, to add a middleware component named `MyMiddleware`, include it in :file:`config/middleware.py`:: # The Pylons WSGI app app = PylonsApp() # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map']) app = SessionMiddleware(app, config) app = CacheMiddleware(app, config) # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) app = MyMiddleware(app) The app object is simply passed as a parameter to the `MyMiddleware` middleware which in turn should return a wrapped WSGI application. Care should be taken when deciding in which layer to place custom middleware. In most cases middleware should be placed before the Pylons WSGI application and its supporting Routes/Session/Cache middlewares, however if the middleware should run *after* the CacheMiddleware:: # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map']) app = SessionMiddleware(app, config) # MyMiddleware can only see the cache object, nothing *above* here app = MyMiddleware(app) app = CacheMiddleware(app, config) What is full_stack? =================== In the Pylons ini file {:file:`development.ini` or :file:`production.ini`} this block determines if the flag full_stack is set to true or false:: [app:main] use = egg:app_name full_stack = true The full_stack flag determines if the ErrorHandler and StatusCodeRedirect is included as a layer in the middleware wrapping process. The only condition in which this option would be set to `false` is if multiple Pylons applications are running and will be wrapped in the appropriate middleware elsewhere. .. _setup-config: ***************** Application Setup ***************** There are two kinds of 'Application Setup' that are occasionally referenced with regards to a project using Pylons. * Setting up a new application * Configuring project information and package dependencies Setting Up a New Application ============================ To make it easier to setup a new instance of a project, such as setting up the basic database schema, populating necessary defaults, etc. a setup script can be created. In a Pylons project, the setup script to be run is located in the projects' :file:`websetup.py` file. The default script loads the projects configuration to make it easier to write application setup steps: .. code-block :: python import logging from helloworld.config.environment import load_environment log = logging.getLogger(__name__) def setup_app(command, conf, vars): """Place any commands to setup helloworld here""" load_environment(conf.global_conf, conf.local_conf) .. note:: If the project was configured during creation to use SQLAlchemy this file will include some commands to setup the database connection to make it easier to setup database tables. To run the setup script using the development configuration: .. code-block :: bash $ paster setup-app development.ini Configuring the Package ======================= A newly created project with Pylons is a standard Python package. As a Python package, it has a :file:`setup.py` file that records meta-information about the package. Most of the options in it are fairly self-explanatory, the most important being the 'install_requires' option: .. code-block :: python install_requires=[ "Pylons>=0.9.7", ], These lines indicate what packages are required for the proper functioning of the application, and should be updated as needed. To re-parse the :file:`setup.py` line for new dependencies: .. code-block :: bash $ python setup.py develop In addition to updating the packages as needed so that the dependency requirements are made, this command will ensure that this package is active in the system (without requiring the traditional :command:`python setup.py install`). .. seealso:: `Declaring Dependencies `_ ================================================ FILE: pylons/docs/en/controllers.rst ================================================ .. _controllers: =========== Controllers =========== .. image:: _static/pylon2.jpg :alt: :align: left :height: 450px :width: 368px In the :term:`MVC` paradigm the *controller* interprets the inputs, commanding the model and/or the view to change as appropriate. Under Pylons, this concept is extended slightly in that a Pylons controller is not directly interpreting the client's request, but is acting to determine the appropriate way to assemble data from the model, and render it with the correct template. The controller interprets requests from the user and calls portions of the model and view as necessary to fulfill the request. So when the user clicks a Web link or submits an HTML form, the controller itself doesn’t output anything or perform any real processing. It takes the request and determines which model components to invoke and which formatting to apply to the resulting data. Pylons uses a class, where the superclass provides the :term:`WSGI` interface and the subclass implements the application-specific controller logic. The Pylons WSGI Controller handles incoming web requests that are dispatched from the Pylons WSGI application :class:`~pylons.wsgiapp.PylonsApp`. These requests result in a new instance of the :class:`~pylons.controllers.core.WSGIController` being created, which is then called with the dict options from the Routes match. The standard WSGI response is then returned with start_response called as per the WSGI spec. Since Pylons controllers are actually called with the WSGI interface, normal WSGI applications can also be Pylons ‘controllers’. Standard Controllers ==================== Standard Controllers intended for subclassing by web developers Keeping methods private ----------------------- The default route maps any controller and action, so you will likely want to prevent some controller methods from being callable from a URL. Pylons uses the default Python convention of private methods beginning with ``_``. To hide a method ``edit_generic`` in this class, just changing its name to begin with ``_`` will be sufficient: .. code-block:: python class UserController(BaseController): def index(self): return "This is the index." def _edit_generic(self): """I can't be called from the web!""" return True Special methods --------------- Special controller methods you may define: ``__before__`` This method is called before your action is, and should be used for setting up variables/objects, restricting access to other actions, or other tasks which should be executed before the action is called. ``__after__`` This method is called after the action is, unless an unexpected exception was raised. Subclasses of :class:`~webob.exc.HTTPException` (such as those raised by ``redirect_to`` and ``abort``) are expected; e.g. ``__after__`` will be called on redirects. Adding Controllers dynamically ------------------------------ It is possible for an application to add controllers without restarting the application. This requires telling Routes to re-scan the controllers directory. New controllers may be added from the command line with the paster command (recommended as that also creates the test harness file), or any other means of creating the controller file. For Routes to become aware of new controllers present in the controller directory, an internal flag is toggled to indicate that Routes should rescan the directory: .. code-block:: python from routes import request_config mapper = request_config().mapper mapper._created_regs = False On the next request, Routes will rescan the controllers directory and those routes that use the ``:controller`` dynamic part of the path will be able to match the new controller. Customizing the Controller Name ------------------------------- By default, Pylons looks for a controller named 'Something'Controller. This naming scheme can be overridden by supplying an optional module-level variable called ``__controller__`` to indicate the desired controller class:: import logging from pylons import request, response, session, tmpl_context as c from pylons.controllers.util import abort, redirect_to from helloworld.lib.base import BaseController, render log = logging.getLogger(__name__) __controller__ = 'Hello' class Hello(BaseController): def index(self): # Return a rendered template #return render('/hello.mako') # or, return a string return 'Hello World' Attaching WSGI apps ------------------- .. note:: This recipe assumes a basic level of familiarity with the WSGI Specification (PEP 333) WSGI runs deep through Pylons, and is present in many parts of the architecture. Since Pylons controllers are actually called with the WSGI interface, normal WSGI applications can also be Pylons 'controllers'. Optionally, if a full WSGI app should be mounted and handle the remainder of the URL, Routes can automatically move the right part of the URL into the :envvar:`SCRIPT_NAME`, so that the WSGI application can properly handle its :envvar:`PATH_INFO` part. This recipe will demonstrate adding a basic WSGI app as a Pylons controller. Create a new controller file in your Pylons project directory: .. code-block:: python $ paster controller wsgiapp This sets up the basic imports that you may want available when using other WSGI applications. Edit your controller so it looks like this: .. code-block:: python import logging from YOURPROJ.lib.base import * log = logging.getLogger(__name__) def WsgiappController(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) return ["Hello World"] When hooking up other WSGI applications, they will expect the part of the URL that was used to get to this controller to have been moved into :envvar:`SCRIPT_NAME`. :mod:`Routes ` can properly adjust the environ if a map route for this controller is added to the :file:`config/routing.py` file: .. code-block:: python # CUSTOM ROUTES HERE # Map the WSGI application map.connect('wsgiapp/{path_info:.*}', controller='wsgiapp') By specifying the ``path_info`` dynamic path, Routes will put everything leading up to the ``path_info`` in the :envvar:`SCRIPT_NAME` and the rest will go in the :envvar:`PATH_INFO`. Using the WSGI Controller to provide a WSGI service =================================================== The Pylons WSGI Controller -------------------------- Pylons' own WSGI Controller follows the WSGI spec for calling and return values The Pylons WSGI Controller handles incoming web requests that are dispatched from ``PylonsApp``. These requests result in a new instance of the ``WSGIController`` being created, which is then called with the dict options from the Routes match. The standard WSGI response is then returned with :meth:`start_response` called as per the WSGI spec. WSGIController methods ---------------------- Special ``WSGIController`` methods you may define: ``__before__`` This method will be run before your action is, and should be used for setting up variables/objects, restricting access to other actions, or other tasks which should be executed before the action is called. ``__after__`` Method to run after the action is run. This method will *always* be run after your method, even if it raises an Exception or redirects. Each action to be called is inspected with :meth:`_inspect_call` so that it is only passed the arguments in the Routes match dict that it asks for. The arguments passed into the action can be customized by overriding the :meth:`_get_method_args` function which is expected to return a dict. In the event that an action is not found to handle the request, the Controller will raise an "Action Not Found" error if in debug mode, otherwise a ``404 Not Found`` error will be returned. .. _rest_controller: Using the REST Controller with a RESTful API ============================================ Using the paster restcontroller template ---------------------------------------- .. code-block:: bash $ paster restcontroller --help Create a REST Controller and accompanying functional test The RestController command will create a REST-based Controller file for use with the :meth:`~routes.base.Mapper.resource` REST-based dispatching. This template includes the methods that :meth:`~routes.base.Mapper.resource` dispatches to in addition to doc strings for clarification on when the methods will be called. The first argument should be the singular form of the REST resource. The second argument is the plural form of the word. If its a nested controller, put the directory information in front as shown in the second example below. Example usage: .. code-block:: bash $ paster restcontroller comment comments Creating yourproj/yourproj/controllers/comments.py Creating yourproj/yourproj/tests/functional/test_comments.py If you'd like to have controllers underneath a directory, just include the path as the controller name and the necessary directories will be created for you: .. code-block:: bash $ paster restcontroller admin/trackback admin/trackbacks Creating yourproj/controllers/admin Creating yourproj/yourproj/controllers/admin/trackbacks.py Creating yourproj/yourproj/tests/functional/test_admin_trackbacks.py An Atom-Style REST Controller for Users --------------------------------------- .. code-block:: python # From http://pylonshq.com/pasties/503 import logging from formencode.api import Invalid from pylons import url from simplejson import dumps from restmarks.lib.base import * log = logging.getLogger(__name__) class UsersController(BaseController): """REST Controller styled on the Atom Publishing Protocol""" # To properly map this controller, ensure your # config/routing.py file has a resource setup: # map.resource('user', 'users') def index(self, format='html'): """GET /users: All items in the collection.
@param format the format passed from the URI. """ #url('users') users = model.User.select() if format == 'json': data = [] for user in users: d = user._state['original'].data del d['password'] d['link'] = url('user', id=user.name) data.append(d) response.headers['content-type'] = 'text/javascript' return dumps(data) else: c.users = users return render('/users/index_user.mako') def create(self): """POST /users: Create a new item.""" # url('users') user = model.User.get_by(name=request.params['name']) if user: # The client tried to create a user that already exists abort(409, '409 Conflict', headers=[('location', url('user', id=user.name))]) else: try: # Validate the data that was sent to us params = model.forms.UserForm.to_python(request.params) except Invalid, e: # Something didn't validate correctly abort(400, '400 Bad Request -- %s' % e) user = model.User(**params) model.objectstore.flush() response.headers['location'] = url('user', id=user.name) response.status_code = 201 c.user_name = user.name return render('/users/created_user.mako') def new(self, format='html'): """GET /users/new: Form to create a new item. @param format the format passed from the URI. """ # url('new_user') return render('/users/new_user.mako') def update(self, id): """PUT /users/id: Update an existing item. @param id the id (name) of the user to be updated """ # Forms posted to this method should contain a hidden field: # # Or using helpers: # h.form(url('user', id=ID), # method='put') # url('user', id=ID) old_name = id new_name = request.params['name'] user = model.User.get_by(name=id) if user: if (old_name != new_name) and model.User.get_by(name=new_name): abort(409, '409 Conflict') else: params = model.forms.UserForm.to_python(request.params) user.name = params['name'] user.full_name = params['full_name'] user.email = params['email'] user.password = params['password'] model.objectstore.flush() if user.name != old_name: abort(301, '301 Moved Permanently', [('Location', url('users', id=user.name))]) else: return def delete(self, id): """DELETE /users/id: Delete an existing item. @param id the id (name) of the user to be updated """ # Forms posted to this method should contain a hidden field: # # Or using helpers: # h.form(url('user', id=ID), # method='delete') # url('user', id=ID) user = model.User.get_by(name=id) user.delete() model.objectstore.flush() return def show(self, id, format='html'): """GET /users/id: Show a specific item. @param id the id (name) of the user to be updated. @param format the format of the URI requested. """ # url('user', id=ID) user = model.User.get_by(name=id) if user: if format=='json': data = user._state['original'].data del data['password'] data['link'] = url('user', id=user.name) response.headers['content-type'] = 'text/javascript' return dumps(data) else: c.data = user return render('/users/show_user.mako') else: abort(404, '404 Not Found') def edit(self, id, format='html'): """GET /users/id;edit: Form to edit an existing item. @param id the id (name) of the user to be updated. @param format the format of the URI requested. """ # url('edit_user', id=ID) user = model.User.get_by(name=id) if not user: abort(404, '404 Not Found') # Get the form values from the table c.values = model.forms.UserForm.from_python(user.__dict__) return render('/users/edit_user.mako') .. _xmlrpc_controller: Using the XML-RPC Controller for XML-RPC requests ================================================= In order to deploy this controller you will need at least a passing familiarity with XML-RPC itself. We will first review the basics of XML-RPC and then describe the workings of the ``Pylons XMLRPCController``. Finally, we will show an example of how to use the controller to implement a simple web service. After you've read this document, you may be interested in reading the companion document: "A blog publishing web service in XML-RPC" which takes the subject further, covering details of the MetaWeblog API (a popular XML-RPC service) and demonstrating how to construct some basic service methods to act as the core of a MetaWeblog blog publishing service. A brief introduction to XML-RPC ------------------------------- XML-RPC is a specification that describes a Remote Procedure Call (RPC) interface by which an application can use the Internet to execute a specified procedure call on a remote XML-RPC server. The name of the procedure to be called and any required parameter values are "marshalled" into XML. The XML forms the body of a POST request which is despatched via HTTP to the XML-RPC server. At the server, the procedure is executed, the returned value(s) is/are marshalled into XML and despatched back to the application. XML-RPC is designed to be as simple as possible, while allowing complex data structures to be transmitted, processed and returned. XML-RPC Controller that speaks WSGI ----------------------------------- Pylons uses Python's xmlrpclib library to provide a specialised :class:`XMLRPCController` class that gives you the full range of these XML-RPC Introspection facilities for use in your service methods and provides the foundation for constructing a set of specialised service methods that provide a useful web service --- such as a blog publishing interface. This controller handles XML-RPC responses and complies with the `XML-RPC Specification `_ as well as the `XML-RPC Introspection `_ specification. As part of its basic functionality an XML-RPC server provides three standard introspection procedures or "service methods" as they are called. The Pylons :class:`XMLRPCController` class provides these standard service methods ready-made for you: * :meth:`system.listMethods` Returns a list of XML-RPC methods for this XML-RPC resource * :meth:`system.methodSignature` Returns an array of arrays for the valid signatures for a method. The first value of each array is the return value of the method. The result is an array to indicate multiple signatures a method may be capable of. * :meth:`system.methodHelp` Returns the documentation for a method By default, methods with names containing a dot are translated to use an underscore. For example, the ``system.methodHelp`` is handled by the method :meth:`system_methodHelp`. Methods in the XML-RPC controller will be called with the method given in the XML-RPC body. Methods may be annotated with a signature attribute to declare the valid arguments and return types. For example: .. code-block:: python class MyXML(XMLRPCController): def userstatus(self): return 'basic string' userstatus.signature = [['string']] def userinfo(self, username, age=None): user = LookUpUser(username) result = {'username': user.name} if age and age > 10: result['age'] = age return result userinfo.signature = [['struct', 'string'], ['struct', 'string', 'int']] Since XML-RPC methods can take different sets of data, each set of valid arguments is its own list. The first value in the list is the type of the return argument. The rest of the arguments are the types of the data that must be passed in. In the last method in the example above, since the method can optionally take an integer value, both sets of valid parameter lists should be provided. Valid types that can be checked in the signature and their corresponding Python types: +--------------------+--------------------+ | XMLRPC | Python | +====================+====================+ | string | str | +--------------------+--------------------+ | array | list | +--------------------+--------------------+ | boolean | bool | +--------------------+--------------------+ | int | int | +--------------------+--------------------+ | double | float | +--------------------+--------------------+ | struct | dict | +--------------------+--------------------+ | dateTime.iso8601 | xmlrpclib.DateTime | +--------------------+--------------------+ | base64 | xmlrpclib.Binary | +--------------------+--------------------+ Note, requiring a signature is optional. Also note that a convenient fault handler function is provided. .. code-block:: python def xmlrpc_fault(code, message): """Convenience method to return a Pylons response XMLRPC Fault""" (The `XML-RPC Home page `_ and the `XML-RPC HOW-TO `_ both provide further detail on the XML-RPC specification.) A simple XML-RPC service ------------------------ This simple service ``test.battingOrder`` accepts a positive integer < 51 as the parameter ``posn`` and returns a string containing the name of the US state occupying that ranking in the order of ratifying the constitution / joining the union. .. code-block:: python import xmlrpclib from pylons import request from pylons.controllers import XMLRPCController states = ['Delaware', 'Pennsylvania', 'New Jersey', 'Georgia', 'Connecticut', 'Massachusetts', 'Maryland', 'South Carolina', 'New Hampshire', 'Virginia', 'New York', 'North Carolina', 'Rhode Island', 'Vermont', 'Kentucky', 'Tennessee', 'Ohio', 'Louisiana', 'Indiana', 'Mississippi', 'Illinois', 'Alabama', 'Maine', 'Missouri', 'Arkansas', 'Michigan', 'Florida', 'Texas', 'Iowa', 'Wisconsin', 'California', 'Minnesota', 'Oregon', 'Kansas', 'West Virginia', 'Nevada', 'Nebraska', 'Colorado', 'North Dakota', 'South Dakota', 'Montana', 'Washington', 'Idaho', 'Wyoming', 'Utah', 'Oklahoma', 'New Mexico', 'Arizona', 'Alaska', 'Hawaii'] class RpctestController(XMLRPCController): def test_battingOrder(self, posn): """This docstring becomes the content of the returned value for system.methodHelp called with the parameter "test.battingOrder"). The method signature will be appended below ... """ # XML-RPC checks agreement for arity and parameter datatype, so # by the time we get called, we know we have an int. if posn > 0 and posn < 51: return states[posn-1] else: # Technically, the param value is correct: it is an int. # Raising an error is inappropriate, so instead we # return a facetious message as a string. return 'Out of cheese error.' test_battingOrder.signature = [['string', 'int']] Testing the service ------------------- For developers using OS X, there's an `XML/RPC client `_ that is an extremely useful diagnostic tool when developing XML-RPC (it's free ... but not entirely bug-free). Or, you can just use the Python interpreter: .. code-block:: pycon >>> from pprint import pprint >>> import xmlrpclib >>> srvr = xmlrpclib.Server("http://example.com/rpctest/") >>> pprint(srvr.system.listMethods()) ['system.listMethods', 'system.methodHelp', 'system.methodSignature', 'test.battingOrder'] >>> print srvr.system.methodHelp('test.battingOrder') This docstring becomes the content of the returned value for system.methodHelp called with the parameter "test.battingOrder"). The method signature will be appended below ... Method signature: [['string', 'int']] >>> pprint(srvr.system.methodSignature('test.battingOrder')) [['string', 'int']] >>> pprint(srvr.test.battingOrder(12)) 'North Carolina' To debug XML-RPC servers from Python, create the client object using the optional verbose=1 parameter. You can then use the client as normal and watch as the XML-RPC request and response is displayed in the console. ================================================ FILE: pylons/docs/en/debugging.rst ================================================ .. _debugging: ====================================== Errors, Troubleshooting, and Debugging ====================================== When a web application has an error in production, a few different options for handling it are available. Pylons comes with error handlers to allow the following options: * E-mail the traceback as HTML to the administrators * Show the :ref:`interactive_debugging` interface to the developer * Programmatically handle the error in another controller * Display a plain error on the web page Some of these options can be combined by enabling or disabling the appropriate middleware. Error Middleware ================ In a new Pylons project, the error handling middleware is configured in the projects :file:`config/middleware.py`:: # Excerpt of applicable section if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) The first middleware configured, :func:`~pylons.middleware.ErrorHandler`, actually configures one of two :mod:`WebError ` middlewares depending on whether the project is in ``debug`` mode or not. If it is in ``debug`` mode, then the :ref:`interactive_debugging` is enabled, otherwise, the :ref:`e-mail error handling ` will be used. The second middleware configured is the :class:`~pylons.middleware.StatusCodeRedirect` middleware. This middleware watches the request, and if the application returns a response containing one of the status code's listed, it will call back into the application to the error controller, and use that output instead. None of these are required for a Pylons project to run, and commenting them all out results in the plain text of the error to display on the web page. .. warning:: If no middleware at all is used, the error will appear on the screen in its entirety, *including full traceback output*. Recommended Configurations -------------------------- * For plain-text output or errors and non-200 status codes, comment out the :class:`~pylons.middleware.StatusCodeRedirect`. Tracebacks will be e-mailed to you in production, and the :ref:`interactive_debugging` will be used during development. * For programmatic error and non-200 status code handling, keep the stack as-is. * To *not* have tracebacks e-mailed, remove only the :func:`~pylons.middleware.ErrorHandler` middleware. This will also disable :ref:`interactive_debugging` however. To retain :ref:`interactive_debugging` but disable traceback e-mails:: if asbool(config['debug']): app = ErrorHandler(app, global_conf, **config['pylons.errorware']) .. note:: To only capture specific non-200 status codes, the :class:`~pylons.middleware.StatusCodeRedirect` middleware can be passed a list of the codes that it should intercept and redirect to the error controller. When in non-debug mode, it captures the 400-404, and 500 status codes. Altering the list will capture more or less types of requests as desired. Avoiding Displaying Tracebacks ------------------------------ When disabling the :func:`~pylons.middleware.ErrorHandler` middleware, a replacement middleware should be created and used that captures exceptions and changes them into a normal WSGI response, otherwise the raw traceback error will be displayed on the browser. An example middleware that just captures exceptions and changes them to a 500 error:: from webob import Request, Response class EatExceptions(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): req = Request(environ) try: response = req.get_response(self.app) except: response = Response() response.status_int = 500 response.body = 'An error has occured' return response(environ, start_response) Replacing the ``ErrorHandler`` with this middleware will cause tracebacks to not be displayed to the user. .. _interactive_debugging: Interactive Debugging ===================== Things break, and when they do, quickly pinpointing what went wrong and why makes a huge difference. By default, Pylons uses a customized version of `Ian Bicking's `_ EvalException middleware that also includes full Mako/Myghty Traceback information. The Debugging Screen -------------------- The debugging screen has three tabs at the top: ``Traceback`` Provides the raw exception trace with the interactive debugger ``Extra Data`` Displays CGI, WSGI variables at the time of the exception, in addition to configuration information ``Template`` Human friendly traceback for Mako or Myghty templates Since Mako and Myghty compile their templates to Python modules, it can be difficult to accurately figure out what line of the template resulted in the error. The `Template` tab provides the full Mako or Myghty traceback which contains accurate line numbers for your templates, and where the error originated from. If your exception was triggered before a template was rendered, no Template information will be available in this section. Example: Exploring the Traceback -------------------------------- Using the interactive debugger can also be useful to gain a deeper insight into objects present only during the web request like the ``session`` and ``request`` objects. To trigger an error so that we can explore what's happening just raise an exception inside an action you're curious about. In this example, we'll raise an error in the action that's used to display the page you're reading this on. Here's what the docs controller looks like: .. code-block:: python class DocsController(BaseController): def view(self, url): if request.path_info.endswith('docs'): redirect(url('/docs/')) return render('/docs/' + url) Since we want to explore the ``session`` and ``request``, we'll need to bind them first. Here's what our action now looks like with the binding and raising an exception: .. code-block:: python def view(self, url): raise "hi" if request.path_info.endswith('docs'): redirect(url('/docs/')) return render('/docs/' + url) Here's what exploring the Traceback from the above example looks like (Excerpt of the relevant portion): .. image:: _static/doctraceback.png :width: 750px :height: 260px .. _error_emails: E-mailing Errors ================ You can make various of changes to how the debugging works. For example if you disable the ``debug`` variable in the config file Pylons will email you an error report instead of displaying it as long as you provide your email address at the top of the config file: .. code-block:: ini error_email_from = you@example.com This is very useful for a production site. Emails are sent via SMTP so you need to specify a valid SMTP server too. Programmatically Handling Errors ================================ By default, the :class:`~pylons.middleware.StatusCodeRedirect` will redirect any response with the designated status codes back into the application again. This will result in the ``error`` controller in the Pylons project being called. This is why there is a default route in :file:`config/routing.py` of:: map.connect('/error/{action}', controller='error') map.connect('/error/{action}/{id}', controller='error') The error controller allows a project to theme the error message appropriately by changing it to render a template, or redirect as desired. Original Request Information ---------------------------- The original request and response that resulted in the error controller being called is available inside the error controller as:: # Original request request.environ['pylons.original_request'] # Original response request.environ['pylons.original_response'] If an :exc:`~webob.exc.HTTPException` was thrown in the controller (the :func:`~pylons.controllers.util.abort` function throws these), the original object is available as:: request.environ['pylons.controller.exception'] This allows access to the error message on the exception object. ================================================ FILE: pylons/docs/en/deployment.rst ================================================ .. _deployment: Packaging and Deployment Overview ================================= TODO: some of this is redundant to the (more current) :ref:`configuration` doc -- should be consolidated and cross-referenced This document describes how a developer can take advantage of Pylons' application setup functionality to allow webmasters to easily set up their application. Installation refers to the process of downloading and installing the application with :term:`easy_install` whereas setup refers to the process of setting up an instance of an installed application so it is ready to be deployed. For example, a wiki application might need to create database tables to use. The webmaster would only install the wiki ``.egg`` file once using :term:`easy_install` but might want to run 5 wikis on the site so would setup the wiki 5 times, each time specifying a different database to use so that 5 wikis can run from the same code, but store their data in different databases. Egg Files ********* Before you can understand how a user configures an application you have to understand how Pylons applications are distributed. All Pylons applications are distributed in ``.egg`` format. An egg is simply a Python executable package that has been put together into a single file. You create an egg from your project by going into the project root directory and running the command: .. code-block:: bash $ python setup.py bdist_egg If everything goes smoothly a ``.egg`` file with the correct name and version number appears in a newly created ``dist`` directory. When a webmaster wants to install a Pylons application he will do so by downloading the egg and then installing it. Installing as a Non-root User ***************************** It's quite possible when using shared hosting accounts that you do not have root access to install packages. In this case you can install :term:`setuptools` based packages like Pylons and Pylons web applications in your home directory using a :term:`virtualenv` setup. This way you can install all the packages you want to use without super-user access. Understanding the Setup Process ******************************* Say you have written a Pylons wiki application called ``wiki``. When a webmaster wants to install your wiki application he will run the following command to generate a config file: .. code-block:: bash $ paster make-config wiki wiki_production.ini He will then edit the config file for his production environment with the settings he wants and then run this command to setup the application: .. code-block:: bash $ paster setup-app wiki_production.ini Finally he might choose to deploy the wiki application through the paste server like this (although he could have chosen CGI/FastCGI/SCGI etc): .. code-block:: bash $ paster serve wiki_production.ini The idea is that an application only needs to be installed once but if necessary can be set up multiple times, each with a different configuration. All Pylons applications are installed in the same way, so you as the developer need to know how the above commands work. Make Config ----------- The ``paster make-config`` command looks for the file ``deployment.ini_tmpl`` and uses it as a basis for generating a new ``.ini`` file. Using our new wiki example again, the ``wiki/config/deployment.ini_tmpl`` file contains the text: .. code-block:: ini [DEFAULT] debug = true email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 0.0.0.0 port = 5000 [app:main] use = egg:wiki full_stack = true static_files = true cache_dir = %(here)s/data beaker.session.key = wiki beaker.session.secret = ${app_instance_secret} app_instance_uuid = ${app_instance_uuid} # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings # here: #beaker.cache.data_dir = %(here)s/data/cache #beaker.session.data_dir = %(here)s/data/sessions # WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* # Debug mode will enable the interactive debugging tool, allowing ANYONE to # execute malicious code after an exception is raised. set debug = false # Logging configuration [loggers] keys = root [handlers] keys = console [formatters] keys = generic [logger_root] level = INFO handlers = console [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s When the command ``paster make-config wiki wiki_production.ini`` is run, the contents of this file are produced so you should tweak this file to provide sensible default configuration for production deployment of your app. Setup App --------- The ``paster setup-app`` command references the newly created ``.ini`` file and calls the function ``wiki.websetup.setup_app()`` to set up the application. If your application needs to be set up before it can be used, you should edit the ``websetup.py`` file. Here's an example which just prints the location of the cache directory via Python's logging facilities: .. code-block:: python """Setup the helloworld application""" import logging from pylons import config from helloworld.config.environment import load_environment log = logging.getLogger(__name__) def setup_app(command, conf, vars): """Place any commands to setup helloworld here""" load_environment(conf.global_conf, conf.local_conf) log.info("Using cache dirctory %s" % config['cache.dir']) For a more useful example, say your application needs a database set up and loaded with initial data. The user will specify the location of the database to use by editing the config file before running the ``paster setup-app`` command. The ``setup_app()`` function will then be able to load the configuration and act on it in the function body. This way, the ``setup_app()`` function can be used to initialize the database when ``paster setup-app`` is run. Using the optional :term:`SQLAlchemy` project template support when creating a Pylons project will set all of this up for you in a basic way. The :ref:`quickwiki_tutorial` illustrates an example of this configuration. Deploying the Application ************************* Once the application is setup it is ready to be deployed. There are lots of ways of deploying an application, one of which is to use the ``paster serve`` command which takes the configuration file that has already been used to setup the application and serves it on a local HTTP server for production use: .. code-block:: bash $ paster serve wiki_production.ini More information on Paste deployment options is available on the Paste website at http://pythonpaste.org. See :ref:`deployment_webservers` for alternative Pylons deployment scenarios. Advanced Usage ************** So far everything we have done has happened through the ``paste.script.appinstall.Installer`` class which looks for the ``deployment.ini_tmpl`` and ``websetup.py`` file and behaves accordingly. If you need more control over how your application is installed you can use your own installer class. Create a file, for example ``wiki/installer.py`` and code your new installer class in the file by deriving it from the existing one: .. code-block:: python from paste.script.appinstall import Installer class MyInstaller(Installer): pass You then override the functionality as necessary (have a look at the source code for ``Installer`` as a basis. You then change your application's ``setup.py`` file so that the ``paste.app_install`` entry point ``main`` points to your new installer: .. code-block:: python entry_points=""" ... [paste.app_install] main=wiki.installer:MyInstaller ... """, Depending on how you code your ``MyInstaller`` class you may not even need your ``websetup.py`` or ``deployment.ini_tmpl`` as you might have decided to create the ``.ini`` file and setup the application in an entirely different way. .. _deployment_webservers: Running Pylons Apps with Other Web Servers ========================================== This document assumes that you have already installed a Pylons web application, and :ref:`run-config` for it. Pylons applications use `PasteDeploy `_ to start up your Pylons WSGI application, and can use the flup package to provide a Fast-CGI, SCGI, or AJP connection to it. Using Fast-CGI ************** `Fast-CGI `_ is a gateway to connect web severs like `Apache `_ and `lighttpd `_ to a CGI-style application. Out of the box, Pylons applications can run with Fast-CGI in either a threaded or forking mode. (Threaded is the recommended choice) Setting a Pylons application to use Fast-CGI is very easy, and merely requires you to change the config line like so: .. code-block:: ini # default [server:main] use = egg:Paste#http # Use Fastcgi threaded [server:main] use = egg:PasteScript#flup_fcgi_thread host = 0.0.0.0 port = 6500 Note that you will need to install the `flup `_ package, which can be installed via easy_install: .. code-block:: bash $ easy_install -U flup The options in the config file are passed onto flup. The two common ways to run Fast CGI is either using a socket to listen for requests, or listening on a port/host which allows a webserver to send your requests to web applications on a different machine. To configure for a socket, your ``server:main`` section should look like this: .. code-block:: ini [server:main] use = egg:PasteScript#flup_fcgi_thread socket = /location/to/app.socket If you want to listen on a host/port, the configuration cited in the first example will do the trick. Apache Configuration ******************** For this example, we will assume you're using Apache 2, though Apache 1 configuration will be very similar. First, make sure that you have the Apache `mod_fastcgi `_ module installed in your Apache. There will most likely be a section where you declare your FastCGI servers, and whether they're external: .. code-block:: apacheconf FastCgiIpcDir /tmp FastCgiExternalServer /some/path/to/app/myapp.fcgi -host some.host.com:6200 In our example we'll assume you're going to run a Pylons web application listening on a host/port. Changing ``-host`` to ``-socket`` will let you use a Pylons web application listening on a socket. The filename you give in the second option does not need to physically exist on the webserver, URIs that Apache resolve to this filename will be handled by the FastCGI application. The other important line to ensure that your Apache webserver has is to indicate that fcgi scripts should be handled with Fast-CGI: .. code-block:: apacheconf AddHandler fastcgi-script .fcgi Finally, to configure your website to use the Fast CGI application you will need to indicate the script to be used: .. code-block:: apacheconf ServerAdmin george@monkey.com ServerName monkey.com ServerAlias www.monkey.com DocumentRoot /some/path/to/app ScriptAliasMatch ^(/.*)$ /some/path/to/app/myapp.fcgi$1 Other useful directives should be added as needed, for example, the ErrorLog directive, etc. This configuration will result in all requests being sent to your FastCGI application. PrefixMiddleware **************** ``PrefixMiddleware`` provides a way to manually override the root prefix (``SCRIPT_NAME``) of your application for certain situations. When running an application under a prefix (such as '``/james``') in FastCGI/apache, the ``SCRIPT_NAME`` environment variable is automatically set to to the appropriate value: '``/james``'. Pylons' URL generators such as ``url`` always take the ``SCRIPT_NAME`` value into account. One situation where ``PrefixMiddleware`` is required is when an application is accessed via a reverse proxy with a prefix. The application is accessed through the reverse proxy via the the URL prefix '``/james``', whereas the reverse proxy forwards those requests to the application at the prefix '``/``'. The reverse proxy, being an entirely separate web server, has no way of specifying the ``SCRIPT_NAME`` variable; it must be manually set by a ``PrefixMiddleware`` instance. Without setting ``SCRIPT_NAME``, ``url`` will generate URLs such as: '``/purchase_orders/1``', when it should be generating: '``/james/purchase_orders/1``'. To filter your application through a ``PrefixMiddleware`` instance, add the following to the '``[app:main]``' section of your .ini file: .. code-block :: ini filter-with = proxy-prefix [filter:proxy-prefix] use = egg:PasteDeploy#prefix prefix = /james The name ``proxy-prefix`` simply acts as an identifier of the filter section; feel free to rename it. These .ini settings are equivalent to adding the following to the end of your application's ``config/middleware.py``, right before the ``return app`` line: .. code-block :: python # This app is served behind a proxy via the following prefix (SCRIPT_NAME) app = PrefixMiddleware(app, global_conf, prefix='/james') This requires the additional import line: .. code-block :: python from paste.deploy.config import PrefixMiddleware Whereas the modification to ``config/middleware.py`` will setup an instance of ``PrefixMiddleware`` under every environment (.ini). Using Java Web Servers with Jython ********************************** See :ref:`java_deployment`. .. _adding_documentation: Documenting Your Application ============================ TODO: this needs to be rewritten -- Pudge is effectively dead While the information in this document should be correct, it may not be entirely complete... Pudge is somewhat unruly to work with at this time, and you may need to experiment to find a working combination of package versions. In particular, it has been noted that an older version of Kid, like 0.9.1, may be required. You might also need to install {{RuleDispatch}} if you get errors related to {{FormEncode}} when attempting to build documentation. Apologies for this suboptimal situation. Considerations are being taken to fix Pudge or supplant it for future versions of Pylons. Introduction ************ Pylons comes with support for automatic documentation generation tools like `Pudge `_. Automatic documentation generation allows you to write your main documentation in the docs directory of your project as well as throughout the code itself using docstrings. When you run a simple command all the documentation is built into sophisticated HTML. Tutorial ******** First create a project as described in :ref:`getting_started`. You will notice a docs directory within your main project directory. This is where you should write your main documentation. There is already an ``index.txt`` file in ``docs`` so you can already generate documentation. First we'll install Pudge and buildutils. By default, Pylons sets an option to use `Pygments `_ for syntax-highlighting of code in your documentation, so you'll need to install it too (unless you wish to remove the option from ``setup.cfg``): .. code-block:: bash $ easy_install pudge buildutils $ easy_install Pygments then run the following command from your project's main directory where the ``setup.py`` file is: .. code-block:: bash $ python setup.py pudge .. Note:: The ``pudge`` command is currently disabled by default. Run the following command first to enable it: ..code-block:: bash $ python setup.py addcommand -p buildutils.pudge_command Thanks to Yannick Gingras for the tip. Pudge will produce output similar to the following to tell you what it is doing and show you any problems: .. code-block:: text running pudge generating documentation copying: pudge\template\pythonpaste.org\rst.css -> do/docs/html\rst.css copying: pudge\template\base\pudge.css -> do/docs/html\pudge.css copying: pudge\template\pythonpaste.org\layout.css -> do/docs/html\layout.css rendering: pudge\template\pythonpaste.org\site.css.kid -> site.css colorizing: do/docs/html\do/__init__.py.html colorizing: do/docs/html\do/tests/__init__.py.html colorizing: do/docs/html\do/i18n/__init__.py.html colorizing: do/docs/html\do/lib/__init__.py.html colorizing: do/docs/html\do/controllers/__init__.py.html colorizing: do/docs/html\do/model.py.html Once finished you will notice a ``docs/html`` directory. The ``index.html`` is the main file which was generated from ``docs/index.txt``. Learning ReStructuredText ************************* Python programs typically use a rather odd format for documentation called `reStructuredText`_. It is designed so that the text file used to generate the HTML is as readable as possible but as a result can be a bit confusing for beginners. Read the reStructuredText tutorial which is part of the `docutils `_ project. Once you have mastered reStructuredText you can write documentation until your heart's content. .. _reStructuredText: http://docutils.sourceforge.net/rst.html Using Docstrings **************** Docstrings are one of Python's most useful features if used properly. They are described in detail in the Python documentation but basically allow you to document any module, class, method or function, in fact just about anything. Users can then access this documentation interactively. Try this: .. code-block:: pycon >>> import pylons >>> help(pylons) ... As you can see if you tried it you get detailed information about the pylons module including the information in the docstring. Docstrings are also extracted by Pudge so you can describe how to use all the controllers, actions and modules that make up your application. Pudge will extract that information and turn it into useful API documentation automatically. Try clicking the ``Modules`` link in the HTML documentation you generated earlier or look at the Pylons source code for some examples of how to use docstrings. Using doctest ************* The final useful thing about docstrings is that you can use the ``doctest`` module with them. ``doctest`` again is described in the Python documentation but it looks through your docstrings for things that look like Python code written at a Python prompt. Consider this example: .. code-block:: pycon >>> a = 2 >>> b = 3 >>> a + b 5 If ``doctest`` was run on this file it would have found the example above and executed it. If when the expression ``a + b`` is executed the result was not ``5``, ``doctest`` would raise an Exception. This is a very handy way of checking that the examples in your documentation are actually correct. To run ``doctest`` on a module use: .. code-block:: python if __name__ == "__main__": import doctest doctest.testmod() The ``if __name__ == "__main__":`` part ensures that your module won't be tested if it is just imported, only if it is run from the command line To run ``doctest`` on a file use: .. code-block:: python import doctest doctest.testfile("docs/index.txt") You might consider incorporating this functionality in your ``tests/test.py`` file to improve the testing of your application. Summary ******* So if you write your documentation in reStructuredText, in the ``docs`` directory and in your code's docstrings, liberally scattered with example code, Pylons provides a very useful and powerful system for you. If you want to find out more information have a look at the Pudge documentation or try tinkering with your project's ``setup.cfg`` file which contains the Pudge settings. .. _app_distribution: Distributing Your Application ============================= TODO: this assumes helloworld tutorial context that is no longer present, and could be consolidated with packaging info in :ref:`deployment` As mentioned earlier eggs are a convenient format for packaging applications. You can create an egg for your project like this: .. code-block:: bash $ cd helloworld $ python setup.py bdist_egg Your egg will be in the ``dist`` directory and will be called ``helloworld-0.0.0dev-py2.4.egg``. You can change options in ``setup.py`` to change information about your project. For example change version to ``version="0.1.0",`` and run ``python setup.py bdist_egg`` again to produce a new egg with an updated version number. You can then register your application with the `Python Package Index`_ (PyPI) with the following command: .. code-block:: bash $ python setup.py register .. note:: You should not do this unless you actually want to register a package! If users want to install your software and have installed :term:`easy_install` they can install your new egg as follows: .. code-block:: bash $ easy_install helloworld==0.1.0 This will retrieve the package from PyPI and install it. Alternatively you can install the egg locally: .. code-block:: bash $ easy_install -f C:\path\with\the\egg\files\in helloworld==0.1.0 In order to use the egg in a website you need to use Paste. You have already used Paste to create your Pylons template and to run a test server to test the tutorial application. Paste is a set of tools available at http://pythonpaste.org for providing a uniform way in which all compatible Python web frameworks can work together. To run a paste application such as any Pylons application you need to create a Paste configuration file. The idea is that the your paste configuration file will contain all the configuration for all the different Paste applications you run. A configuration file suitable for development is in the ``helloworld/development.ini`` file of the tutorial but the idea is that the person using your egg will add relevant configuration options to their own Paste configuration file so that your egg behaves they way they want. See the section below for more on this configuration. Paste configuration files can be run in many different ways, from CGI scripts, as standalone servers, with FastCGI, SCGI, mod_python and more. This flexibility means that your Pylons application can be run in virtually any environment and also take advantage of the speed benefits that the deployment option offers. .. seealso:: :ref:`deployment_webservers` .. _Python Package Index: http://pypi.python.org/pypi Running Your Application ************************ In order to run your application your users will need to install it as described above but then generate a config file and setup your application before deploying it. This is described in :ref:`run-config` and :ref:`deployment`. ================================================ FILE: pylons/docs/en/events.rst ================================================ .. _events: ====== Events ====== ================================================ FILE: pylons/docs/en/execution.rst ================================================ Pylons Execution Analysis %%%%%%%%%%%%%%%%%%%%%%%%% *By Mike Orr and Alfredo Deza* This chapter shows how Pylons calls your application, and how Pylons interacts with Paste, Routes, Mako, and its other dependencies. We'll create a simple application and then analyze the Python code executed starting from the moment we run the "paster serve" command. **Abbreviations:** **$APP** is your top-level application directory. **$SP** is the site-packages directory where Pylons is installed. **$BIN** is the location of ``paster`` and other executables. $SP paths are shown in pip style ($SP/pylons) rather than easy_install style ($SP/Pylons-VERSION.egg/pylons). The sample application ======================== 1. Create an application called "Analysis" with a controller called "main":: $ paster create -t pylons Analysis $ cd Analysis $ paster controller main Press Enter at all question prompts. 2. Edit **analysis/controllers/main.py** to look like this:: from analysis.lib.base import BaseController class MainController(BaseController): def index(self): return '

Welcome to the Analysis Demo

Here is a link.' def page2(self): return 'Thank you for using the Analysis Demo. Home' There are two shortcuts here which you would not use in a normal application. One, we're returning incomplete HTML documents. Two, we've hardcoded the URLs to make the analysis easier to follow, rather than using the ``url`` object. 3. Now edit **analysis/config/routing.py**. Add these lines after "CUSTOM ROUTES HERE" (line 21):: map.connect("home", "/", controller="main", action="index") map.connect("page2", "/page2", controller="main", action="page2") 4. Delete the file **analysis/public/index.html**. 5. Now run the server. (Press ctrl-C to quit it.) :: $ paster serve development.ini Starting server in PID 7341. serving on http://127.0.0.1:5000 Pylons' dependencies ==================== Pylons 1.0 has the following direct and indirect dependencies, which will be found in your site-packages directory ($SP): * Beaker 1.5.4 * decorator 3.2.0 * FormEncode 1.2.2 * Mako 0.3.4 * MarkupSafe 0.9.3 * Nose 0.11.4 * Paste 1.7.3.1 * PasteDeploy 1.3.3 * PasteScript 1.7.3 * Routes 1.12.3 * simplejson 2.0.9 (if Python < 2.6) * Tempita 0.4 * WebError 0.10.2 * WebHelpers 1.2 * WebOb 0.9.8 * Webtest 1.2.1 These are the current versions as of August 29, 2010. Your installation may have slightly newer or older versions. The analysis ============ Startup (PasteScript) --------------------- When you run ``paster serve development.ini``, it runs the "$BIN/paster" program. This is a platform-specific stub created by ``pip`` or ``easy_install``. It does this:: __requires__ = 'PasteScript==1.7.3' import sys from pkg_resources import load_entry_point sys.exit( load_entry_point('PasteScript==1.7.3', 'console_scripts', 'paster')() ) This says to load a Python object "paster" located in an egg "PasteScript", version 1.7.3, under the entry point group ``[console_scripts]``. To explain what this means we have to get into Setuptools. Setuptools is Python's de facto package manager, and was installed as part of your virtualenv or Pylons installation. (If you're using Distribute 0.6, an alternative package manager, it works the same way.) ``load_entry_point`` is a function that looks up a Python object via entry point and returns it. So what's an entry point? It's an alias for a Python object. Here's the entry point itself:: [console_scripts] paster=paste.script.command:run This is from $SP/PasteScript-VERSION.egg-info/entry_points.txt. (If you used easy_install rather than pip, the path would be slightly different: $APP/PasteScript-VERSION.egg/EGG-INFO/entry_points.txt.) "console_scripts" is the entry point group. "paster" is the entry point. The right side of the value tells which module to import (``paste.script.command``) and which object in it to return (the ``run`` function). (To create an entry point, define it in your package's setup.py. Pip or easy_install will create the egg_info metadata from that. If you modify a package's entry points, you must reinstall the package to update the egg_info.) The most common use case for entry points is for plugins. So Nose for instance defines an entry point group by which it will look for plugins. Any other package can provide plugins for Nose by defining entry points in that group. Paster uses plugins extensively, as we'll soon see. So to make a long story short, "paster serve" calls this ``run`` function. I inserted print statements into ``paste.script.command`` to figure out what it does. Here's a simplified description: 1. The ``run()`` function parses the command-line options into a subcommand ``"serve"`` with arguments ``["development.ini"]``. 2. It calls ``get_commands()``, which loads Paster commands from plugins located at various entry points. (You can add custom commands with the "--plugin" command-line argument.) Paste's standard commands are listed in the same entry_points.txt file we saw above:: [paste.global_paster_command] serve=paste.script.serve:ServeCommand [Config] #... other commands like "make-config", "setup-app", etc ... 3. It calls ``invoke()``, which essentially does ``paste.script.serve.ServeCommand(["development.ini"]).run()``. This in turn calls ``ServeCommand.command()``, which handles daemonizing and other top-level stuff. Since our command line is short, there's no top-level stuff to do. It creates 'server' and 'app' objects based on the configuration file, and calls ``server(app)``. Loading the server and the application (PasteDeploy) ---------------------------------------------------- This all happens during step 3 of the application startup. We need to find and instantiate the WSGI application and server based on the configuration file. The application is our Analysis application. The server is Paste's built-in multithreaded HTTP server. A simplified version of the code is:: # Inside paste.script.serve module, ServeCommand.command() method. from paste.deploy.loadwsgi import loadapp, loadserver server = self.loadserver(server_spec, name=server_name, relative_to=base, global_conf=vars) app = self.loadapp(app_spec, name=app_name, relative_to=base, global_conf=vars) ``loadserver()`` and ``loadapp()`` are defined in module ``paste.deploy.loadwsgi``. The code here is complex, so we'll just look at its general behavior. Both functions see the "config:" URI and read our config file. Since there is no server name or app name they both default to "main". Therefore loadserver() looks for a "\[server:main]" section in the config file, and loadapp()` looks for "\[app:main]". Here's what they find in "development.ini":: [server:main] use = egg:Paste#http host = 127.0.0.1 port = 5000 [app:main] use = egg:Analysis full_stack = true static_files = true ... The "use =" line in each section tells which object to load. The other lines are configuration parameters for that object, or for plugins that object is expected to load. We can also put custom parameters in \[app:main] for our application to read directly. Server loading ++++++++++++++ 1. ``loadserver()``'s args are ``uri="config:development.ini", name=None, relative_to="$APP"``. 2. A "config:" URI means to read a config file. 3. A server name was not specified so it defaults to "main". So loadserver() looks for a section "\[server:main]". The "server" part comes from the loadwsgi._Server.config_prefixes class attribute in $SP/paste/deploy/loadwsgi.py). 4. "use = egg:Paste#http" says to load an egg called "Paste". 5. loadwsgi._Server.egg_protocols lists two protocols it supports: "server_factory" and "server_runner". 6. "paste.server_runner" is an entry point group in the "Paste" egg, and it has an entry point "http". The relevant lines in $SP/Paste\*.egg_info/entry_points.txt are:: [paste.server_runner] http = paste.httpserver:server_runner 7. There's a server_runner() function in the paste.httpserver module ($SP/paste/httpserver.py). We'll stop here for a moment and look at how the application is loaded. Application loading +++++++++++++++++++ 1. loadapp() looks for a section "\[app:main]" in the config file. The "app" part comes from the loadwsgi._App.config_prefixes class attribute (in $SP/paste/deploy/loadwsgi.py). 2. "use = egg:Analysis" says to find an egg called "Analysis". 3. loadwsgi._App.egg_protocols lists "paste.app_factory" as one of the protocols it supports. 4. "paste.app_factory" is also an entry point group in the egg, as seen in $APP/Analysis.egg-info/entry_points.txt:: [paste.app_factory] main = analysis.config.middleware:make_app 5. The line "main = analysis.config.middleware:make_app" means to look for a ``make_app()`` object in the ``analysis`` package. This is a function imported from ``analysis.config.middleware`` ($APP/analysis/config/middleware.py). Instantiating the application (Analysis) ---------------------------------------- Here's a closer look at our application's ``make_app`` function:: # In $APP/analysis/config/middleware.py def make_app(global_conf, full_stack=True, static_files=True, **app_conf): config = load_environment(global_conf, app_conf) app = PylonsApp(config=config) app = SomeMiddleware(app, ...) # Repeated for several middlewares. app.config = config return app This sets up the Pylons environment (next subsection), creates the application object (following subsection), wraps it in several layers of middleware (listed in "Anatomy of a Request" below), and returns the complete application object. The \[DEFAULT] section of the config file is passed as dict ``global_conf``. The \[app:main] section is passed as keyword arguments into dict ``app_conf``. ``full_stack`` defaults to True because we're running the application standalone. If we were embedding this application as a WSGI component of some larger application, we'd set ``full_stack`` to False to disable some of the middleware. ``static_files=True`` means to serve static files from our public directory ($APP/analysis/public). Advanced users can arrange for Apache to serve the static files itself, and put "static_files = false" in their configuration file to gain a bit of efficiency. load_environment & pylons.config ++++++++++++++++++++++++++++++++ Before we begin, remember that ``pylons.config``, ``pylons.app_globals``, ``pylons.request``, ``pylons.response``, ``pylons.session``, ``pylons.url``, and ``pylons.cache`` are special globals that change value depending on the current request. The objects are proxies which maintain a thread-local stack of real values. Pylons pushes the actual values onto them at the beginning of a request, and pops them off at the end. (Some of them it also pushes at other times so they can be used outside of requests.) The proxies delegate attribute access and key access to the topmost actual object on the stack. (You can also call ``myproxy._current_obj()`` to get the actual object itself.) The proxy code is in ``paste.registry.StackedObjectProxy``, so these are called "StackedObjectProxies", or "SOPs" for short. The first thing ``analysis.config.middleware.make_app()`` does is call ``analysis.config.environment.load_environment()``:: def load_environment(global_conf, app_conf): config = PylonsConfig() root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) paths = dict(root=root, controllers=os.path.join(root, 'controllers'), static_files=os.path.join(root, 'public'), templates=[os.path.join(root, 'templates')]) # Initialize config with the basic options config.init_app(global_conf, app_conf, package='analysis', paths=paths) config['routes.map'] = make_map(config) config['pylons.app_globals'] = app_globals.Globals(config) config['pylons.h'] = analysis.lib.helpers # Setup cache object as early as possible import pylons pylons.cache._push_object(config['pylons.app_globals'].cache) # Create the Mako TemplateLookup, with the default auto-escaping config['pylons.app_globals'].mako_lookup = TemplateLookup( directories=paths['templates'], error_handler=handle_mako_error, module_directory=os.path.join(app_conf['cache_dir'], 'templates'), input_encoding='utf-8', default_filters=['escape'], imports=['from webhelpers.html import escape']) # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) return config ``config`` is the Pylons configuration object, which will later be pushed onto ``pylons.config``. It's an instance of ``pylons.configuration.PylonsConfig``, a dict subclass. ``config.init_app()`` initializes the dict's keys. It sets the keys to a merger of app_conf and global_conf (with app_conf overriding). It also adds "app_conf" and "global_conf" keys so you can access the original app_conf and global_conf if desired. It also adds several Pylons-specific keys. ``config["routes.map"]`` is the Routes map defined in ``analysis.config.routing.make_map()``. ``config["pylons.app_globals"]`` is the application's globals object, which will later be pushed onto ``pylons.app_globals``. It's an instance of ``analysis.lib.app_globals.Globals``. ``config["pylons.h"]`` is the helpers module, ``analysis.lib.helpers``. Pylons will assign it to ``h`` in the templates' namespace. The "cache" lines push ``pylons.app_globals.cache`` onto ``pylons.cache`` for backward compatibility. This gives a preview of how StackedObjectProxies work. The Mako stanza creates a TemplateLookup, which ``render()`` will use to find templates. The object is put on ``app_globals``. If you've used older versions of Pylons, you'll notice a couple differences in 1.0. The ``config`` object is created as a local variable and returned, and it's passed explicitly to the route map factory and globals factory. Previous versions pushed it onto ``pylons.config`` immediately and used it from there. This was changed to make it easier to nest Pylons applications inside other Pylons applications. The other difference is that Buffet is gone, and along with it the ``template_engine`` argument and template config options. Pylons 1.0 gets out of the business of initializing template engines. You use one of the standard render functions such as ``render_mako`` or write your own, and define any attributes in ``app_globals`` that your render function depends on. PylonsApp +++++++++ The second line of ``make_app()`` creates a Pylons application object based on your configuration. Again the ``config`` object is passed around explicitly, unlike older versions of Pylons. A Pylons application is an instance of ``pylons.wsgiapp.PylonsApp`` instance. (Older versions of Pylons had a ``PylonsBaseWSGIApp`` superclass, but that has been merged into ``PylonsApp``.) Middleware ++++++++++ ``make_app()`` then wraps the application (the ``app`` variable) in several layers of middleware. Each middleware provides an optional add-on service. ================== ============================ =============================== Middleware Service Effect if disabled ================== ============================ =============================== RoutesMiddleware Use Routes to manage URLs. Routes and ``pylons.url`` won't work. SessionMiddleware HTTP sessions using Beaker, ``pylons.session`` won't work. with flexible persistence backends (disk, memached, database). ErrorHandler Display interactive Paste will catch exceptions and traceback if an exception convert them to Internal Server occurs. In production mode, Error. email the traceback to the site admin. StatusCodeRedirect If an HTTP error occurs, If an HTTP error occurs, make a subrequest to display display a plain white HTML page a fancy styled HTML error with the error message. page. RegistryManager Handles the special globals The special globals won't work. (``pylons.request``, etc). There are other ways to access the objects without going through the special globals. StaticURLParser Serve the static files The static files won't be in the application's found. Presumably you've public directory. configured Apache to serve them directly. Cascade Call several sub-middlewares No cascading through in order, and use the first alternative apps. one that doesn't return "404 Not Found". Used in conjunction with StaticURLParser. ================== ============================ =============================== At the end of the function, ``app.config`` is set to the ``config`` object, so that any part of the application can access the config without going through the special global. Anatomy of a request -------------------- Let's say you're running the demo and click the "link" link on the home page. The browser sends a request for "http://localhost:5000/page2". In my Firefox the HTTP request headers are:: GET /page2 Host: 127.0.0.1:5000 User-Agent: Mozilla/5.0 ... Accept: text/html,... Accept-Language: en-us,en;q=0.5 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7*;q=0.7 Keep-Alive: 300 Connection: keep-alive Referer: http://127.0.0.1/5000/ Cache-Control max-age=0 The response is:: HTTP/1.x 200 OK Server: PasteWSGIServer/0.5 Python/2.6.4 Date: Sun, 06 Dec 2009 14:06:05 GMT Content-Type: text/html; charset=utf-8 Pragma: no-cache Cache-Control: no-cache Content-Length: 59 Thank you for using the Analysis Demo. Home Here's the processing sequence: 1. ``server(app)`` is still running, called by ``ServeCommand.command()`` in $SP/paste/script/serve.py. 2. ``server`` is actually ``paste.httpserver.server_runner()`` in $SP/paste/httpserver. The only keyword args are 'host' and 'port' extracted from the config file. ``server_runner`` de-stringifies the arguments and calls ``serve(wsgi_app, **kwargs)`` (same module). 3. ``serve()``'s 'use_threadpool' arg defaults to True, so it creates a ``WSGIThreadPoolServer`` instance called (``server``) with the following inheritance:: SocketServer.BaseServer # In SocketServer.py in Python stdlib. BaseHTTPServer.HTTPServer # In BaseHTTPServer.py in Python stdlib. paste.httpserver.SecureHTTPServer # Adds SSL (HTTPS). paste.httpserver.WSGIServerBase # Adds WSGI. paste.httpserver.WSGIThreadPoolServer multiple inheritance: ThreadPoolMixIn <= ThreadPool Note that SecureHTTPServer overrides the implementation of Python's SocketServer.TCPServer 4. It calls ``server.serve_forever()``, implemented by the ``ThreadPoolMixIn`` superclass. This calls ``self.handle_request()`` in a loop until ``self.running`` becomes false. That initiates this call stack:: # In paste.httpserver.serve(), calling 'server.serve_forever()' ThreadPoolMixIn.serve_forever() # Defined in paste.httpserver. -> TCPServer.handle_request() # Called for every request. -> WSGIServerBase.get_request() -> SecureHTTPServer.get_request() -> self.socket.accept() # Defined in stdlib socket module. ``self.socket.accept()`` blocks, waiting for the next request. 5. The request arrives and ``self.socket.accept()`` returns a new socket for the connection. ``TCPServer.handle_request()`` continues. It calls ``ThreadPoolMixIn.process_request()``, which puts the request in a thread queue:: self.thread_pooladd.add_task( lambda: self.process_request_in_thread(request, client_address)) # 'request' is the connection socket. The thread pool is defined in the ``ThreadPool`` class. It spawns a number of threads which each wait on the queue for a callable to run. In this case the callable will be a complete Web transaction including sending the HTML page to the client. Each thread will repeatedly process transactions from the queue until they receive a sentinel value ordering them to die. The main thread goes back to listening for other requests, so we're no longer interested in it. 6. **Thread #2** pulls the lambda out of the queue and calls it:: lambda -> ThreadPoolMixIn.process_request_in_thread() -> BaseServer.finish_request() -> self.RequestHandlerClass(request, client_address, self) # Instantiates this. The class instantiated is paste.httpserver.WSGIHandler; i.e., the 'handler' variable in serve(). 7. The newly-created request handler takes over:: SocketServer.BaseRequestHandler.__init__(request, client_address, server) -> WSGIHandler.handle() -> BaseHTTPRequestHandler.handle() # In stdlib BaseHTTPServer.py Handles requests in a loop until self.close_connection is true. (For HTTP keepalive?) -> WSGIHandler.handle_one_request() Reads the command from the socket. The command is "GET /page2 HTTP/1.1" plus the HTTP headers above. BaseHTTPRequestHandler.parse_request() parses this into attributes .command, .path, .request_version, and .headers. -> WSGIHandlerMixin.wsgi_execute(). -> WSGIHandlerMixin.wsgi_setup() Creates the .wsgi_environ dict. The WSGI environment dict is described in PEP 333, the WSGI specification. It contains various keys specifying the URL to fetch, query parameters, server info, etc. All keys required by the CGI specification are present, as are other keys specific to WSGI or to paricular middleware. The application will calculate a response based on the dict. The application is wrapped in layers of middleware -- nested function calls -- which modify the dict on the way in and modify the response on the way out. 8. The request handler, still in ``WSGIHandlerMixin.wsgi_execute()``, calls the application thus:: result = self.server.wsgi_application(self.wsgi_environ, self.wsgi_start_response) ``wsgi_start_response`` is a callable mandated by the WSGI spec. The application will call it to specify the HTTP headers. The return value is an iteration of strings, which when concatenated form the HTML document to send to the browser. Other MIME types are handled analagously. 9. The application, as we remember, was returned by ``analysis.config.middleware.make_app()``. It's wrapped in several layers of middleware, so calling it will execute the middleware in reverse order of how they're listed in $APP/analysis/config/middleware.py and $SP/pylons/wsgiapp.py: * ``Cascade`` (defined in $SP/paste/cascade.py) lists a series of applications which will be tried in order (Skipped if static_files is set to False): 1. ``StaticURLParser`` (defined in $SP/paste/urlparser) looks for a file URL under $APP/analysis/public that matches the URL. The demo has no static files. 2. If that fails the cascader tries your application. But first there are other middleware to go through... * ``RegistryManager`` (defined in $SP/paste/registry.py) makes Pylons special globals both thread-local and middleware-local. This includes **app_globals**, **cache**, **request**, **response**, **session**, **tmpl_context**, **url**, and any other ``StackedObjectProxy`` listed in $SP/pylons/__init__.py. (**h** is a module so it doesn't need a proxy.) * ``StatusCodeRedirect`` (defined in $SP/pylons/middleware.py) intercepts any HTTP error status returned by the application (e.g., "Page Not Found", "Internal Server Error") and sends another request to the application to get the appropriate error page to display instead. (Skipped if ``full_stack`` argument was false.) * ``ErrorHandler`` (defined in $SP/pylons/middleware.py) sends an interactive traceback to the browser if the app raises an exception, if "debug" is true in the config file. Otherwise it attempts to email the traceback to the site administrator, and substitutes a generic Internal Server Error for the response. (Skipped if ``full_stack`` argument was false. * User-defined middleware goes here. * ``SessionMiddleware`` (wsgiapp.py) adds `Beaker`_ session support (the ``pylons.session`` object). (Skipped if the WSGI environment has a key 'session' -- it doesn't in this demo.) * ``RoutesMiddleware`` (wsgiapp.py) compares the request URI against the routing rules in $APP/analysis/config/routing.py and sets 'wsgi.routing_args' to the routing match dict (useful) and 'routes.route' to the Route (probably not useful). Pylons 1.0 apps have a ``singleton=False`` argument that suppresses initializing the deprecated ``url_for()`` function. Routes now puts a URL generator in the WSGI environment, which Pylons aliases to ``pylons.url``. * The innermost middleware calls the PylonsApp instance it was initialized with. Note: CacheMiddleware is no longer used in Pylons 1.0. Instead, ``app_globals`` creates the cache as an attribute, and a line in environment.py aliases ``pylons.cache`` to it. 10. Surprise! PylonsApp is itself middleware. Its .\_\_call\_\_() method does:: self.setup_app_env(environ, start_response) controller = self.resolve(environ, start_response) response = self.dispatch(controller, environ, start_response) return response ``.setup_app_env()`` registers all those special globals. ``.resolve()`` calculates the controller class based on the route chosen by the RoutesMiddleware, and returns the controller class. ``.dispatch`` instantiates the controller class and calls in the WSGI manner. If the controller does not exist (``.resolve()`` returned None), raise an Exception that tells you what controller did not have any content. This method also handles the special URL "/_test_vars", which is enabled if the application is running under a Nose test. This URL initializes Pylons' special globals, for tests that have to access them before making a regular request. 11. ``analysis.controllers.main.MainController`` does not have a ``.\_\_call\_\_()`` method, so control falls to its parent, ``analysis.lib.base.BaseController``. This trivially calls the grandparent, ``pylons.controllers.WSGIController``. It calls the action method ``MainController.page2()``. The action method may have any number of positional arguments as long as they correspond to variables in the routing match dict. (GET/POST variables are in the **request.params** dict.) If the method has a ``\*\*kwargs`` argument, all other match variables are put there. Any variables passed to the action method are also put on the **tmpl_context** object as attributes. If an action method name starts with "\_", it's private and HTTPNotFound is raised. 12. If the controller has .\_\_before\_\_() and/or .\_\_after\_\_() methods, they are called before and after the action, respectively. These can perform authorization, lock OS resources, etc. These methods can have arguments in the same manner as the action method. However, if the code is used by all controllers, most Pylons programmers prefer to it in the base controller's ``.\_\_call\_\_`` method instead. 13. The action method returns a string, unicode, Response object, or is a generator of strings. In this trivial case it returns a string. A typical Pylons action would set some *tmpl_context* attributes and 'return render('/some/template.html")' . In either case the global *response* object's body would be set to the string. 14. ``WSGIController.\_\_call\_\_()`` continues, converting the Response object to an appropriate WSGI return value. (First it calls the start_response callback to specify the HTTP headers, then it returns an iteration of strings. The Response object converts unicode to utf-8 encoded strings, or whatever encoding you've specified in the config file.) 15. The stack of middleware calls unwinds, each modifying the return value and headers if it desires. 16. The server receives the final return value. (We're way back in ``paste.httpserver.WSGIHandlerMixin.wsgi_execute()`` now.) The outermost middleware has called back to ``server.start_response()``, which has saved the status and HTTP headers in ``.wsgi_curr_headers``. ``.wsgi_execute()`` then iterates the application's return value, calling ``.wsgi_write_chunk(chunk)`` for each encoded string yielded. ``.wsgi_write_chunk('')`` formats the status and HTTP headers and sends them on the socket if they haven't been sent yet, then sends the chunk. The convoluted header behavior here is mandated by the WSGI spec. 17. Control returns to ``BaseHTTPRequestHandler.handle()``. ``.close_connection`` is true so this method returns. The call stack continues unwinding all the way to ``paste.httpserver.ThreadPoolMixIn.process_request_in_thread()``. This tries to finish the request first and then close it unless it finds errors in it to end raising an Exception. 18. The request lambda finishes and control returns to ``ThreadPool.worker_thread_callback()``. It waits for another request in the thread queue. If the next item in the queue is the shutdown sentinel value, thread #2 dies. Thus endeth our request's long journey, and this analysis is finished too. .. _beaker: http://beaker.groovie.org/ ================================================ FILE: pylons/docs/en/forms.rst ================================================ .. _forms: =========== Forms =========== The basics ========== When a user submits a form on a website the data is submitted to the URL specified in the `action` attribute of the `
` tag. The data can be submitted either via HTTP `GET` or `POST` as specified by the `method` attribute of the `` tag. If your form doesn't specify an `action`, then it's submitted to the current URL, generally you'll want to specify an `action`. When a file upload field such as `` is present, then the HTML `` tag must also specify `enctype="multipart/form-data"` and `method` must be `POST`. Getting Started =============== Add two actions that looks like this: .. code-block:: python # in the controller def form(self): return render('/form.mako') def email(self): return 'Your email is: %s' % request.params['email'] Add a new template called `form.mako` in the `templates` directory that contains the following: .. code-block:: html Email Address:
If the server is still running (see the :ref:`Getting Started Guide `) you can visit http://localhost:5000/hello/form and you will see the form. Try entering the email address `test@example.com` and clicking Submit. The URL should change to ``http://localhost:5000/hello/email?email=test%40example.com`` and you should see the text `Your email is test@example.com`. In Pylons all form variables can be accessed from the :data:`request.params` object which behaves like a dictionary. The keys are the names of the fields in the form and the value is a string with all the characters entity decoded. For example note how the `@` character was converted by the browser to `%40` in the URL and was converted back ready for use in :data:`request.params`. .. Note:: `request` and `response` are objects from the `WebOb` library. Full documentation on their attributes and methods is `here `_. If you have two fields with the same name in the form then using the dictionary interface will return the first string. You can get all the strings returned as a list by using the `.getall()` method. If you only expect one value and want to enforce this you should use `.getone()` which raises an error if more than one value with the same name is submitted. By default if a field is submitted without a value, the dictionary interface returns an empty string. This means that using `.get(key, default)` on `request.params` will only return a default if the value was not present in the form. POST vs GET and the Re-Submitted Data Problem --------------------------------------------- If you change the `form.mako` template so that the method is `POST` and you re-run the example you will see the same message is displayed as before. However, the URL displayed in the browser is simply http://localhost:5000/hello/email without the query string. The data is sent in the body of the request instead of the URL, but Pylons makes it available in the same way as for GET requests through the use of `request.params`. .. note:: If you are writing forms that contain password fields you should usually use POST to prevent the password being visible to anyone who might be looking at the user's screen. When writing form-based applications you will occasionally find users will press refresh immediately after submitting a form. This has the effect of repeating whatever actions were performed the first time the form was submitted but often the user will expect that the current page be shown again. If your form was submitted with a POST, most browsers will display a message to the user asking them if they wish to re-submit the data, this will not happen with a GET so POST is preferable to GET in those circumstances. Of course, the best way to solve this issue is to structure your code differently so: .. code-block:: python # in the controller def form(self): return render('/form.mako') def email(self): # Code to perform some action based on the form data # ... redirect(url(controller='home', action='result')) def result(self): return 'Your data was successfully submitted' In this case once the form is submitted the data is saved and an HTTP redirect occurs so that the browser redirects to http://localhost:5000/hello/result. If the user then refreshes the page, it simply redisplays the message rather than re-performing the action. Using the Helpers ================= Creating forms can also be done using WebHelpers, which comes with Pylons. Here is the same form created in the previous section but this time using the helpers: .. code-block:: html+mako ${h.form(h.url(action='email'), method='get')} Email Address: ${h.text('email')} ${h.submit('Submit')} ${h.end_form()} Before doing this you'll have to import the helpers you want to use into your project's `lib/helpers.py` file; then they'll be available under Pylons' ``h`` global. Most projects will want to import at least these: .. code-block:: python from webhelpers.html import escape, HTML, literal, url_escape from webhelpers.html.tags import * There are many other helpers for text formatting, container objects, statistics, and for dividing large query results into pages. See the :mod:`WebHelpers documentation ` to choose the helpers you'll need. .. _file_uploads: File Uploads ============ File upload fields are created by using the `file` input field type. The `file_field` helper provides a shortcut for creating these form fields: .. code-block:: mako ${h.file_field('myfile')} The HTML form must have its `enctype` attribute set to `multipart/form-data` to enable the browser to upload the file. The `form` helper's `multipart` keyword argument provides a shortcut for setting the appropriate `enctype` value: .. code-block:: html+mako ${h.form(h.url(action='upload'), multipart=True)} Upload file: ${h.file_field('myfile')}
File description: ${h.text_field('description')}
${h.submit('Submit')} ${h.end_form()} When a file upload has succeeded, the `request.POST` (or `request.params`) `MultiDict` will contain a `cgi.FieldStorage` object as the value of the field. `FieldStorage` objects have three important attributes for file uploads: `filename` The name of file uploaded as it appeared on the uploader's filesystem. `file` A file(-like) object from which the file's data can be read: A python `tempfile` or a `StringIO` object. `value` The content of the uploaded file, eagerly read directly from the file object. The easiest way to gain access to the file's data is via the `value` attribute: it returns the entire contents of the file as a string: .. code-block:: python def upload(self): myfile = request.POST['myfile'] return 'Successfully uploaded: %s, size: %i, description: %s' % \ (myfile.filename, len(myfile.value), request.POST['description']) However reading the entire contents of the file into memory is undesirable, especially for large file uploads. A common means of handling file uploads is to store the file somewhere on the filesystem. The `FieldStorage` typically reads the file onto filesystem, however to a non permanent location, via a python `tempfile` object (though for very small uploads it stores the file in a `StringIO` object instead). Python `tempfiles` are secure file objects that are automatically destroyed when they are closed (including an implicit close when the object is garbage collected). One of their security features is that their path cannot be determined: a simple `os.rename` from the `tempfile's` path isn't possible. Alternatively, `shutil.copyfileobj` can perform an efficient copy of the file's data to a permanent location: .. code-block:: python permanent_store = '/uploads/' class Uploader(BaseController): def upload(self): myfile = request.POST['myfile'] permanent_file = open(os.path.join(permanent_store, myfile.filename.lstrip(os.sep)), 'w') shutil.copyfileobj(myfile.file, permanent_file) myfile.file.close() permanent_file.close() return 'Successfully uploaded: %s, description: %s' % \ (myfile.filename, request.POST['description']) .. warning:: The previous basic example allows any file uploader to overwrite any file in the `permanent_store` directory that your web application has permissions to. Also note the use of `myfile.filename.lstrip(os.sep)` here: without it, `os.path.join` is unsafe. `os.path.join` won't join absolute paths (beginning with `os.sep`), i.e. `os.path.join('/uploads/', '/uploaded_file.txt')` == `'/uploaded_file.txt'`. Always check user submitted data to be used with `os.path.join`. Validating user input with FormEncode ===================================== Validation the Quick Way ------------------------ At the moment you could enter any value into the form and it would be displayed in the message, even if it wasn't a valid email address. In most cases this isn't acceptable since the user's input needs validating. The recommended tool for validating forms in Pylons is `FormEncode `_. For each form you create you also create a validation schema. In our case this is fairly easy: .. code-block:: python import formencode class EmailForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = True email = formencode.validators.Email(not_empty=True) .. note:: We usually recommend keeping form schemas together so that you have a single place you can go to update them. It's also convenient for inheritance since you can make new form schemas that build on existing ones. If you put your forms in a `models/form.py` file, you can easily use them throughout your controllers as `model.form.EmailForm` in the case shown. Our form actually has two fields, an email text field and a submit button. If extra fields are submitted FormEncode's default behavior is to consider the form invalid so we specify `allow_extra_fields = True`. Since we don't want to use the values of the extra fields we also specify `filter_extra_fields = True`. The final line specifies that the email field should be validated with an `Email()` validator. In creating the validator we also specify `not_empty=True` so that the email field will require input. Pylons comes with an easy to use `validate` decorator, if you wish to use it import it in your `lib/base.py` like this: .. code-block:: python # other imports from pylons.decorators import validate Using it in your controller is pretty straight-forward: .. code-block:: python # in the controller def form(self): return render('/form.mako') @validate(schema=EmailForm(), form='form') def email(self): return 'Your email is: %s' % self.form_result.get('email') Validation only occurs on POST requests so we need to alter our form definition so that the method is a POST: .. code-block:: mako ${h.form(h.url(action='email'), method='post')} If validation is successful, the valid result dict will be saved as `self.form_result` so it can be used in the action. Otherwise, the action will be re-run as if it was a GET request to the controller action specified in `form`, and the output will be filled by FormEncode's htmlfill to fill in the form field errors. For simple cases this is really handy because it also avoids having to write code in your templates to display error messages if they are present. This does exactly the same thing as the example above but works with the original form definition and in fact will work with any HTML form regardless of how it is generated because the validate decorator uses `formencode.htmlfill` to find HTML fields and replace them with the values were originally submitted. .. note:: Python 2.3 doesn't support decorators so rather than using the `@validate()` syntax you need to put `email = validate(schema=EmailForm(), form='form')(email)` after the email function's declaration. Validation the Long Way ----------------------- The `validate` decorator covers up a bit of work, and depending on your needs it's possible you could need direct access to FormEncode abilities it smoothes over. Here's the longer way to use the `EmailForm` schema: .. code-block:: python # in the controller def email(self): schema = EmailForm() try: form_result = schema.to_python(request.params) except formencode.validators.Invalid, error: return 'Invalid: %s' % error else: return 'Your email is: %s' % form_result.get('email') If the values entered are valid, the schema's `to_python()` method returns a dictionary of the validated and coerced `form_result`. This means that you can guarantee that the `form_result` dictionary contains values that are valid and correct Python objects for the data types desired. In this case the email address is a string so `request.params['email']` happens to be the same as `form_result['email']`. If our form contained a field for age in years and we had used a `formencode.validators.Int()` validator, the value in `form_result` for the age would also be the correct type; in this case a Python integer. FormEncode comes with a useful set of validators but you can also easily create your own. If you do create your own validators you will find it very useful that all FormEncode schemas' `.to_python()` methods take a second argument named `state`. This means you can pass the Pylons `c` object into your validators so that you can set any variables that your validators need in order to validate a particular field as an attribute of the `c` object. It can then be passed as the `c` object to the schema as follows: .. code-block:: python c.domain = 'example.com' form_result = schema.to_python(request.params, c) The schema passes `c` to each validator in turn so that you can do things like this: .. code-block:: python class SimpleEmail(formencode.validators.Email): def _to_python(self, value, c): if not value.endswith(c.domain): raise formencode.validators.Invalid( 'Email addresses must end in: %s' % \ c.domain, value, c) return formencode.validators.Email._to_python(self, value, c) For this to work, make sure to change the `EmailForm` schema you've defined to use the new `SimpleEmail` validator. In other words, .. code-block:: python email = formencode.validators.Email(not_empty=True) # becomes: email = SimpleEmail(not_empty=True) In reality the invalid error message we get if we don't enter a valid email address isn't very useful. We really want to be able to redisplay the form with the value entered and the error message produced. Replace the line: .. code-block:: python return 'Invalid: %s' % error with the lines: .. code-block:: python c.form_result = error.value c.form_errors = error.error_dict or {} return render('/form.mako') Now we will need to make some tweaks to `form.mako`. Make it look like this: .. code-block:: html+mako ${h.form(h.url(action='email'), method='get')} % if c.form_errors:

Please correct the errors

% else:

Enter Email Address

% endif % if c.form_errors: Email Address: ${h.text_field('email', value=c.form_result['email'] or '')}

${c.form_errors['email']}

% else: Email Address: ${h.text_field('email')} % endif ${h.submit('Submit')} ${h.end_form()} Now when the form is invalid the `form.mako` template is re-rendered with the error messages. Other Form Tools ================ If you are going to be creating a lot of forms you may wish to consider using `FormBuild `_ to help create your forms. To use it you create a custom Form object and use that object to build all your forms. You can then use the API to modify all aspects of the generation and use of all forms built with your custom Form by modifying its definition without any need to change the form templates. Here is an one example of how you might use it in a controller to handle a form submission: .. code-block:: python # in the controller def form(self): results, errors, response = formbuild.handle( schema=Schema(), # Your FormEncode schema for the form # to be validated template='form.mako', # The template containg the code # that builds your form form=Form # The FormBuild Form definition you wish to use ) if response: # The form validation failed so re-display # the form with the auto-generted response # containing submitted values and errors or # do something with the errors return response else: # The form validated, do something useful with results. ... Full documentation of all features is available in the `FormBuild manual `_ which you should read before looking at `Using FormBuild in Pylons `_ Looking forward it is likely Pylons will soon be able to use the TurboGears widgets system which will probably become the recommended way to build forms in Pylons. ================================================ FILE: pylons/docs/en/gettingstarted.rst ================================================ .. _getting_started: =============== Getting Started =============== This section is intended to get Pylons up and running as fast as possible and provide a quick overview of the project. Links are provided throughout to encourage exploration of the various aspects of Pylons. ************ Requirements ************ * Python 2 series above and including 2.4 (Python 3 or later not supported at this time) .. _installing_pylons: ********** Installing ********** To avoid conflicts with system-installed Python libraries, Pylons comes with a boot-strap Python script that sets up a "virtual" Python environment. Pylons will then be installed under the virtual environment. .. admonition:: By the Way :term:`virtualenv` is a useful tool to create isolated Python environments. In addition to isolating packages from possible system conflicts, it makes it easy to install Python libraries using :term:`easy_install` without dumping lots of packages into the system-wide Python. The other great benefit is that no root access is required since all modules are kept under the desired directory. This makes it easy to setup a working Pylons install on shared hosting providers and other systems where system-wide access is unavailable. 1. Download the `go-pylons.py `_ script. 2. Run the script and specify a directory for the virtual environment to be created under: .. code-block:: bash $ python go-pylons.py mydevenv .. admonition:: Tip The two steps can be combined on unix systems with curl using the following short-cut: .. code-block:: bash $ curl https://raw.githubusercontent.com/Pylons/pylons/master/scripts/go-pylons.py | python - mydevenv To isolate further from additional system-wide Python libraries, run with the --no-site-packages option: .. code-block:: bash $ python go-pylons.py --no-site-packages mydevenv | **How it Works** The ``go-pylons.py`` script is little more than a basic :term:`virtualenv` bootstrap script, that then does ``easy_install Pylons==1.0``. You could do the equivilant steps by manually fetching the ``virtualenv.py`` script and then installing Pylons like so: .. code-block:: bash curl -O http://bitbucket.org/ianb/virtualenv/raw/8dd7663d9811/virtualenv.py python virtualenv.py mydevenv mydevenv/bin/easy_install Pylons==1.0 This will leave a functional virtualenv and Pylons installation. Activate the virtual environment (scripts may also be run by specifying the full path to the mydevenv/bin dir): .. code-block:: bash $ source mydevenv/bin/activate Or on Window to activate: .. code-block:: text > mydevenv\Scripts\activate.bat .. note:: If you get an error such as:: ImportError: No module named _md5 during the install. It is likely that your Python installation is missing standard libraries needed to run Pylons. Debian and other systems using debian packages most frequently encounter this, make sure to install the ``python-dev`` packages and ``python-hashlib`` packages. Working Directly From the Source Code ===================================== `Mercurial `_ must be installed to retrieve the latest development source for Pylons. `Mercurial packages `_ are also available for Windows, MacOSX, and other OS's. Check out the latest code: .. code-block:: bash $ hg clone http://bitbucket.org/bbangert/pylons/ To tell setuptools to use the version in the ``Pylons`` directory: .. code-block:: bash $ cd pylons $ python setup.py develop The active version of Pylons is now the copy in this directory, and changes made there will be reflected for Pylons apps running. ************************* Creating a Pylons Project ************************* Create a new project named ``helloworld`` with the following command: .. code-block:: bash $ paster create -t pylons helloworld .. note:: Windows users must configure their ``PATH`` as described in :ref:`windows_notes`, otherwise they must specify the full path to the ``paster`` command (including the virtual environment bin directory). Running this will prompt for two choices: 1. which templating engine to use 2. whether to include :term:`SQLAlchemy` support Hit enter at each prompt to accept the defaults (Mako templating, no :term:`SQLAlchemy`). Here is the created directory structure with links to more information: - helloworld - MANIFEST.in - README.txt - development.ini - :ref:`run-config` - docs - ez_setup.py - helloworld (See the nested :ref:`helloworld directory `) - helloworld.egg-info - setup.cfg - setup.py - :ref:`setup-config` - test.ini .. _helloworld_dir: The nested ``helloworld directory`` looks like this: - helloworld - __init__.py - config - environment.py - :ref:`environment-config` - middleware.py - :ref:`middleware-config` - routing.py - :ref:`url-config` - controllers - :ref:`controllers` - lib - app_globals.py - :term:`app_globals` - base.py - helpers.py - :ref:`helpers` - model - :ref:`models` - public - templates - :ref:`templates` - tests - :ref:`testing` - websetup.py - :ref:`run-config` *********************** Running the application *********************** Run the web application: .. code-block:: bash $ cd helloworld $ paster serve --reload development.ini The command loads the project's server configuration file in :file:`development.ini` and serves the Pylons application. .. note:: The ``--reload`` option ensures that the server is automatically reloaded if changes are made to Python files or the :file:`development.ini` config file. This is very useful during development. To stop the server press :command:`Ctrl+c` or the platform's equivalent. The paster serve command can be run anywhere, as long as the development.ini path is properly specified. Generally during development it's run in the root directory of the project. Visiting http://127.0.0.1:5000/ when the server is running will show the welcome page. *********** Hello World *********** To create the basic hello world application, first create a :term:`controller` in the project to handle requests: .. code-block:: bash $ paster controller hello Open the :file:`helloworld/controllers/hello.py` module that was created. The default controller will return just the string 'Hello World': .. code-block:: python import logging from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect from helloworld.lib.base import BaseController, render log = logging.getLogger(__name__) class HelloController(BaseController): def index(self): # Return a rendered template #return render('/hello.mako') # or, Return a response return 'Hello World' At the top of the module, some commonly used objects are imported automatically. Navigate to http://127.0.0.1:5000/hello/index where there should be a short text string saying "Hello World" (start up the app if needed): .. image:: _static/helloworld.png .. admonition:: Tip :ref:`url-config` explains how URL's get mapped to controllers and their methods. Add a template to render some of the information that's in the :term:`environ`. First, create a :file:`hello.mako` file in the :file:`templates` directory with the following contents: .. code-block:: mako Hello World, the environ variable looks like:
${request.environ} The :term:`request` variable in templates is used to get information about the current request. :ref:`Template globals ` lists all the variables Pylons makes available for use in templates. Next, update the :file:`controllers/hello.py` module so that the index method is as follows: .. code-block:: python class HelloController(BaseController): def index(self): return render('/hello.mako') Refreshing the page in the browser will now look similar to this: .. image:: _static/hellotemplate.png ================================================ FILE: pylons/docs/en/glossary.rst ================================================ .. _glossary: Glossary ======== .. glossary:: action The class method in a Pylons applications' controller that handles a request. API Application Programming Interface. The means of communication between a programmer and a software program or operating system. app_globals The ``app_globals`` object is created on application instantiation by the :class:`Globals` class in a projects :file:`lib/app_globals.py` module. This object is created once when the application is loaded by the projects :file:`config/environment.py` module (See :ref:`environment-config`). It remains persistent during the lifecycle of the web application, and is *not* thread-safe which means that it is best used for global options that should be *read-only*, or as an object to attach db connections or other objects which ensure their own access is thread-safe. c Commonly used alias for :term:`tmpl_context` to save on the typing when using lots of controller populated variables in templates. caching The storage of the results of expensive or length computations for later re-use at a point more quickly accessed by the end user. CDN Content Delivery Networks (CDN's) are generally globally distributed content delivery networks optimized for low latency for static file distribution. They can significantly increase page-load times by ensuring that the static resources on a page are delivered by servers geographically close to the client in addition to lightening the load placed on the application server. ColdFusion Components CFCs represent an attempt by Macromedia to bring ColdFusion closer to an Object Oriented Programming (OOP) language. ColdFusion is in no way an OOP language, but thanks in part to CFCs, it does boast some of the attributes that make OOP languages so popular. config The :class:`~pylons.configuration.PylonsConfig` instance for a given application. This can be accessed as ``pylons.config`` after an Pylons application has been loaded. controller The 'C' in MVC. The controller is given a request, does the necessary logic to prepare data for display, then renders a template with the data and returns it to the user. See :ref:`controllers`. easy_install A tool that lets you download, build, install and manage Python packages and their dependencies. `easy_install`_ is the end-user facing component of :term:`setuptools`. Pylons can be installed with ``easy_install``, and applications built with Pylons can easily be deployed this way as well. .. seealso:: Pylons :ref:`deployment` .. _easy_install: http://peak.telecommunity.com/DevCenter/EasyInstall dotted name string A reference to a Python module by name using a string to identify it, e.g. ``pylons.controllers.util``. These strings are evaluated to import the module being referenced without having to import it in the code used. This is generally used to avoid import-time side-effects. egg Python egg's are bundled Python packages, generally installed by a package called :term:`setuptools`. Unlike normal Python package installs, egg's allow a few additional features, such as package dependencies, and dynamic discovery. .. seealso:: `The Quick Guide to Python Eggs `_ EJBs Enterprise JavaBeans (EJB) technology is the server-side component architecture for Java Platform, Enterprise Edition (Java EE). EJB technology enables rapid and simplified development of distributed, transactional, secure and portable applications based on Java technology. environ environ is a dictionary passed into all :term:`WSGI` application. It generally contains unparsed header information, CGI style variables and other objects inserted by :term:`WSGI Middleware`. ETag An ETag (entity tag) is an HTTP response header returned by an HTTP/1.1 compliant web server used to determine change in content at a given URL. See http://wikipedia.org/wiki/HTTP_ETag g Alias used in prior versions of Pylons for :term:`app_globals`. Google App Engine A cloud computing platform for hosting web applications implemented in Python. Building Pylons applications for App Engine is facilitated by Ian Bicking's `appengine-monkey project `_. .. seealso:: `What is Google App Engine? - Official Doc `_ h The helpers reference, ``h``, is made available for use inside templates to assist with common rendering tasks. ``h`` is just a reference to the :file:`lib/helpers.py` module and can be used in the same manner as any other module import. Model-View-Controller An architectural pattern used in software engineering. In Pylons, the MVC paradigm is extended slightly with a pipeline that may transform and extend the data available to a controller, as well as the Pylons :term:`WSGI` app itself that determines the appropriate Controller to call. .. seealso:: `MVC at Wikipedia `_ MVC See :term:`Model-View-Controller` ORM (Object-Relational Mapper) Maps relational databases such as MySQL, Postgres, Oracle to objects providing a cleaner API. Most ORM's also make it easier to prevent SQL Injection attacks by binding variables, and can handle generating sometimes extensive SQL. Pylons A Python-based WSGI oriented web framework. Rails Abbreviated as RoR, Ruby on Rails (also referred to as just Rails) is an open source Web application framework, written in Ruby request Refers to the current request being processed. Available to import from :mod:`pylons` and is available for use in templates by the same name. See :class:`~pylons.controllers.util.Request`. response Refers to the response to the current request. Available to import from :mod:`pylons` and is available for use in template by the same name. See :class:`~pylons.controllers.util.Response`. route Routes determine how the URL's are mapped to the controllers and which URL is generated. See :ref:`url-config` setuptools An extension to the basic distutils, setuptools allows packages to specify package dependencies and have dynamic discovery of other installed Python packages. .. seealso:: `Building and Distributing Packages with setuptools `_ SQLAlchemy One of the most popular Python database object-relational mappers (:term:`ORM`). `SQLAlchemy `_ is the default ORM recommended in Pylons. SQLAlchemy at the ORM level can look similar to Rails ActiveRecord, but uses the `DataMapper `_ pattern for additional flexibility with the ability to map simple to extremely complex databases. tmpl_context The ``tmpl_context`` is available in the :mod:`pylons` module, and refers to the template context. Objects attached to it are available in the template namespace as either ``tmpl_context`` or ``c`` for convenience. UI User interface. The means of communication between a person and a software program or operating system. virtualenv A tool to create isolated Python environments, designed to supersede the ``workingenv`` package and `virtual python`_ configurations. In addition to isolating packages from possible system conflicts, `virtualenv`_ makes it easy to install Python libraries using :term:`easy_install` without dumping lots of packages into the system-wide Python. The other great benefit is that no root access is required since all modules are kept under the desired directory. This makes it easy to setup a working Pylons install on shared hosting providers and other systems where system-wide access is unavailable. ``virtualenv`` is employed automatically by the ``go-pylons.py`` script described in :ref:`getting_started`. The Pylons wiki has more information on `working with virtualenv`_. .. _virtual python: http://peak.telecommunity.com/DevCenter/EasyInstall#creating-a-virtual-python .. _virtualenv: http://pypi.python.org/pypi/virtualenv .. _working with virtualenv: http://wiki.pylonshq.com/display/pylonscookbook/Using+a+Virtualenv+Sandbox web server gateway interface A specification for web servers and application servers to communicate with web applications. Also referred to by its initials, as :term:`WSGI`. WSGI The `WSGI Specification `_, also commonly referred to as PEP 333 and described by :pep:`333`. WSGI Middleware :term:`WSGI` Middleware refers to the ability of WSGI applications to modify the environ, and/or the content of other WSGI applications by being placed in between the request and the other WSGI application. .. seealso:: :ref:`WSGI Middleware in Concepts of Pylons ` :ref:`WSGI Middleware Configuration ` ================================================ FILE: pylons/docs/en/helpers.rst ================================================ .. _helpers: ======= Helpers ======= Helpers are functions intended for usage in templates, to assist with common HTML and text manipulation, higher level constructs like a HTML tag builder (that safely escapes variables), and advanced functionality like Pagination of data sets. The majority of the helpers available in Pylons are provided by the :mod:`webhelpers` package. Some of these helpers are also used in controllers to prepare data for use in the template by other helpers, such as the :func:`~webhelpers.rails.secure_form_tag` function which has a corresponding :func:`~pylons.decorators.secure.authenticate_form`. To make individual helpers available for use in templates under :term:`h`, the appropriate functions need to be imported in :file:`lib/helpers.py`. All the functions available in this file are then available under :term:`h` just like any other module reference. By customizing the :file:`lib/helpers.py` module you can quickly add custom functions and classes for use in your templates. Helper functions are organized into modules by theme. All HTML generators are under the ``webhelpers_html`` package, except for a few third-party modules which are directly under ``webhelpers``. The webhelpers modules are separately documented, see :mod:`webhelpers`. .. _pagination: Pagination ========== .. note:: The `paginate` module is not compatible to the deprecated `pagination` module that was provided with former versions of the Webhelpers package. Purpose of a paginator ---------------------- When you display large amounts of data like a result from an SQL query then usually you cannot display all the results on a single page. It would simply be too much. So you divide the data into smaller chunks. This is what a paginator does. It shows one page of chunk of data at a time. Imagine you are providing a company phonebook through the web and let the user search the entries. Assume the search result contains 23 entries. You may decide to display no more than 10 entries per page. The first page contains entries 1-10, the second 11-20 and the third 21-23. And you also show a navigational element like ``Page 1 of 3: [1] 2 3`` that allows the user to switch between the available pages. The ``Page`` class ------------------ The :mod:`webhelpers` package provides a *paginate* module that can be used for this purpose. It can create pages from simple Python lists as well as SQLAlchemy queries and SQLAlchemy select objects. The module provides a ``Page`` object that represents a single page of items from a larger result set. Such a ``Page`` mainly behaves like a list of items on that page. Let's take the above example of 23 items spread across 3 pages: .. code-block :: pycon # Create a list of items from 1 to 23 >>> items = range(1,24) # Import the paginate module >>> import webhelpers.paginate # Create a Page object from the 'items' for the second page >>> page2 = webhelpers.paginate.Page(items, page=2, items_per_page=10) # The Page object can be printed (__repr__) to show details on the page >>> page2 Page: Collection type: (Current) page: 2 First item: 11 Last item: 20 First page: 1 Last page: 3 Previous page: 1 Next page: 3 Items per page: 10 Number of items: 23 Number of pages: 3 # Show the items on this page >>> list(page2) [11, 12, 13, 14, 15, 16, 17, 18, 19, 20] # Print the items in a for loop >>> for i in page2: print "This is entry", i This is entry 11 This is entry 12 This is entry 13 This is entry 14 This is entry 15 This is entry 16 This is entry 17 This is entry 18 This is entry 19 This is entry 20 There are further parameters to invoking a ``Page`` object. Please see :class:`webhelpers.paginate.Page` .. note:: Page numbers and item numbers start from 1. If you are accessing the items on the page by their index please note that the first item is ``item[1]`` instead of ``item[0]``. Switching between pages using a `pager` --------------------------------------- The user needs a way to get to another page. This is usually done with a list of links like ``Page 3 of 41 - 1 2 [3] 4 5 .. 41``. Such a list can be created by the Page's :meth:`~webhelpers.paginate.Page.pager` method. Take the above example again: .. code-block:: pycon >>> page2.pager() 1 2 3 Without the HTML tags it looks like ``1 [2] 3``. The links point to a URL where the respective page is found. And the current page (2) is highlighted. The appearance of a pager can be customized. By default the format string is ``~2~`` which means it shows adjacent pages from the current page with a maximal radius of 2. In a larger set this would look like ``1 .. 34 35 [36] 37 38 .. 176``. The radius of 2 means that two pages before and after the current page 36 are shown. Several special variables can be used in the format string. See :meth:`~webhelpers.paginate.Page.pager` for a complete list. Some examples for a pager of 20 pages while being on page 10 currently: .. code-block:: pycon >>> page.pager() 1 .. 8 9 [10] 11 12 .. 20 >>> page.pager('~4~') 1 .. 6 7 8 9 [10] 11 12 13 14 .. 20 >>> page.pager('Page $page of $page_count - ~3~') Page 10 of 20 - 1 .. 7 8 9 [10] 11 12 13 .. 20 >>> page.pager('$link_previous $link_next ~2~') < > 1 .. 8 9 [10] 11 12 .. 20 >>> page.pager('Items $first_item - $last_item / ~2~') Items 91 - 100 / 1 .. 8 9 [10] 11 12 .. 20 Paging over an SQLAlchemy query ------------------------------- If the data to page over comes from a database via SQLAlchemy then the ``paginate`` module can access a ``query`` object directly. This is useful when using ORM-mapped models. Example: .. code-block:: pycon >>> employee_query = Session.query(Employee) >>> page2 = webhelpers.paginate.Page( employee_query, page=2, items_per_page=10) >>> for employee in page2: print employee.first_name John Jack Joseph Kay Lars Lynn Pamela Sandra Thomas Tim The `paginate` module is smart enough to only query the database for the objects that are needed on this page. E.g. if a page consists of the items 11-20 then SQLAlchemy will be asked to fetch exactly that 10 rows through `LIMIT` and `OFFSET` in the actual SQL query. So you must not load the complete result set into memory and pass that. Instead always pass a `query` when creating a `Page`. Paging over an SQLAlchemy select -------------------------------- SQLAlchemy also allows to run arbitrary SELECTs on database tables. This is useful for non-ORM queries. `paginate` can use such select objects, too. Example: .. code-block:: pycon >>> selection = sqlalchemy.select([Employee.c.first_name]) >>> page2 = webhelpers.paginate.Page( selection, page=2, items_per_page=10, sqlalchemy_session=model.Session) >>> for first_name in page2: print first_name John Jack Joseph Kay Lars Lynn Pamela Sandra Thomas Tim The only difference to using SQLAlchemy *query* objects is that you need to pass an SQLAlchemy *session* via the ``sqlalchemy_session`` parameter. A bare ``select`` does not have a database connection assigned. But the session has. Usage in a Pylons controller and template ----------------------------------------- A simple example to begin with. Controller: .. code-block:: python def list(self): c.employees = webhelpers.paginate.Page( model.Session.query(model.Employee), page = int(request.params['page']), items_per_page = 5) return render('/employees/list.mako') Template: .. code-block:: mako ${c.employees.pager('Page $page: $link_previous $link_next ~4~')}
    % for employee in c.employees:
  • ${employee.first_name} ${employee.last_name}
  • % endfor
The `pager()` creates links to the previous URL and just sets the *page* parameter appropriately. That's why you need to pass the requested page number (``request.params['page']``) when you create a `Page`. Partial updates with AJAX ------------------------- Updating a page partially is easy. All it takes is a little Javascript that - instead of loading the complete page - updates just the part of the page containing the paginated items. The ``pager()`` method accepts an ``onclick`` parameter for that purpose. This value is added as an ``onclick`` parameter to the A-HREF tags. So the ``href`` parameter points to a URL that loads the complete page while the ``onclick`` parameter provides Javascript that loads a partial page. An example (using the jQuery Javascript library for simplification) may help explain that. Controller: .. code-block:: python def list(self): c.employees = webhelpers.paginate.Page( model.Session.query(model.Employee), page = int(request.params['page']), items_per_page = 5) if 'partial' in request.params: # Render the partial page return render('/employees/list-partial.mako') else: # Render the full page return render('/employees/list-full.mako') Template ``list-full.mako``: .. code-block:: mako ${webhelpers.html.tags.javascript_link('/public/jQuery.js')}
<%include file="list-partial.mako"/>
Template ``list-partial.mako``: .. code-block:: mako ${c.employees.pager( 'Page $page: $link_previous $link_next ~4~', onclick="$('#my-page-area').load('%s'); return false;")}
    % for employee in c.employees:
  • ${employee.first_name} ${employee.last_name}
  • % endfor
To avoid code duplication in the template the full template includes the partial template. If a partial page load is requested then just the ``list-partial.mako`` gets rendered. And if a full page load is requested then the ``list-full.mako`` is rendered which in turn includes the ``list-partial.mako``. The ``%s`` variable in the ``onclick`` string gets replaced with a URL pointing to the respective page with a ``partial=1`` added (the name of the parameter can be customized through the ``partial_param`` parameter). Example: * ``href`` parameter points to ``/employees/list?page=3`` * ``onclick`` parameter contains Javascript loading ``/employees/list?page=3&partial=1`` jQuery's syntax to load a URL into a certain DOM object (e.g. a DIV) is simply: .. code-block:: javascript $('#some-id').load('/the/url') The advantage of this technique is that it degrades gracefully. If the user does not have Javascript enabled then a full page is loaded. And if Javascript works then a partial load is done through the ``onclick`` action. .. _secure-forms: Secure Form Tag Helpers ======================= For prevention of Cross-site request forgery (CSRF) attacks. Generates form tags that include client-specific authorization tokens to be verified by the destined web app. Authorization tokens are stored in the client's session. The web app can then verify the request's submitted authorization token with the value in the client's session. This ensures the request came from the originating page. See the wikipedia entry for `Cross-site request forgery`__ for more information. .. __: http://en.wikipedia.org/wiki/Cross-site_request_forgery Pylons provides an ``authenticate_form`` decorator that does this verification on the behalf of controllers. These helpers depend on Pylons' ``session`` object. Most of them can be easily ported to another framework by changing the API calls. The helpers are implemented in such a way that it should be easy for developers to create their own helpers if using helpers for AJAX calls. :func:`authentication_token` returns the current authentication token, creating one and storing it in the session if it doesn't already exist. :func:`auth_token_hidden_field` creates a hidden field containing the authentication token. :func:`secure_form` is :func:`form` plus :func:`auth_token_hidden_field`. ================================================ FILE: pylons/docs/en/i18n.rst ================================================ .. _i18n: ===================================== Internationalization and Localization ===================================== Introduction ============ Internationalization and localization are means of adapting software for non-native environments, especially for other nations and cultures. Parts of an application which might need to be localized might include: * Language * Date/time format * Formatting of numbers e.g. decimal points, positioning of separators, character used as separator * Time zones (UTC in internationalized environments) * Currency * Weights and measures The distinction between internationalization and localization is subtle but important. Internationalization is the adaptation of products for potential use virtually everywhere, while localization is the addition of special features for use in a specific locale. For example, in terms of language used in software, internationalization is the process of marking up all strings that might need to be translated whilst localization is the process of producing translations for a particular locale. Pylons provides built-in support to enable you to internationalize language but leaves you to handle any other aspects of internationalization which might be appropriate to your application. .. note:: Internationalization is often abbreviated as I18N (or i18n or I18n) where the number 18 refers to the number of letters omitted. Localization is often abbreviated L10n or l10n in the same manner. These abbreviations also avoid picking one spelling (internationalisation vs. internationalization, etc.) over the other. In order to represent characters from multiple languages, you will need to utilize Unicode. This document assumes you have read the :ref:`unicode`. By now you should have a good idea of what Unicode is, how to use it in Python and which areas of you application need to pay specific attention to decoding and encoding Unicode data. This final section will look at the issue of making your application work with multiple languages. Pylons uses the `Python gettext module `_ for internationalization. It is based off the `GNU gettext API `_. Getting Started =============== Everywhere in your code where you want strings to be available in different languages you wrap them in the ``_()`` function. There are also a number of other translation functions which are documented in the API reference at http://pylonshq.com/docs/module-pylons.i18n.translation.html .. note:: The ``_()`` function is a reference to the ``ugettext()`` function. ``_()`` is a convention for marking text to be translated and saves on keystrokes. ``ugettext()`` is the Unicode version of ``gettext()``; it returns unicode strings. In our example we want the string ``'Hello'`` to appear in three different languages: English, French and Spanish. We also want to display the word ``'Hello'`` in the default language. We'll then go on to use some plural words too. Lets call our project ``translate_demo``: .. code-block:: bash $ paster create -t pylons translate_demo Now lets add a friendly controller that says hello: .. code-block:: bash $ cd translate_demo $ paster controller hello Edit ``controllers/hello.py`` to make use of the ``_()`` function everywhere where the string ``Hello`` appears: .. code-block:: python import logging from pylons.i18n import get_lang, set_lang from translate_demo.lib.base import * log = logging.getLogger(__name__) class HelloController(BaseController): def index(self): response.write('Default: %s
' % _('Hello')) for lang in ['fr','en','es']: set_lang(lang) response.write("%s: %s
" % (get_lang(), _('Hello'))) When writing wrapping strings in the gettext functions, it is important not to piece sentences together manually; certain languages might need to invert the grammars. Don't do this: .. code-block:: python # BAD! msg = _("He told her ") msg += _("not to go outside.") but this is perfectly acceptable: .. code-block:: python # GOOD msg = _("He told her not to go outside") The controller has now been internationalized, but it will raise a ``LanguageError`` until we have setup the alternative language catalogs. GNU gettext use three types of files in the translation framework. POT (Portable Object Template) files ------------------------------------ The first step in the localization process. A program is used to search through your project's source code and pick out every string passed to one of the translation functions, such as ``_()``. This list is put together in a specially-formatted template file that will form the basis of all translations. This is the ``.pot`` file. PO (Portable Object) files -------------------------- The second step in the localization process. Using the POT file as a template, the list of messages are translated and saved as a ``.po`` file. MO (Machine Object) files ------------------------- The final step in the localization process. The PO file is run through a program that turns it into an optimized machine-readable binary file, which is the ``.mo`` file. Compiling the translations to machine code makes the localized program much faster in retrieving the translations while it is running. GNU gettext provides a suite of command line programs for extracting messages from source code and working with the associated gettext catalogs. The `Babel `_ project provides pure Python alternative versions of these tools. Unlike the GNU gettext tool `xgettext`, Babel supports extracting translatable strings from Python templating languages (currently Mako and Genshi). Using Babel =========== .. image:: _static/babel_logo.png To use Babel, you must first install it via easy_install. Run the command: .. code-block:: bash $ easy_install Babel Pylons (as of 0.9.6) includes some sane defaults for Babel's distutils commands in the setup.cfg file. It also includes an extraction method mapping in the setup.py file. It is commented out by default, to avoid distutils warning about it being an unrecognized option when Babel is not installed. These lines should be uncommented before proceeding with the rest of this walk through: .. code-block :: python message_extractors = {'translate_demo': [ ('**.py', 'python', None), ('templates/**.mako', 'mako', None), ('public/**', 'ignore', None)]}, We'll use Babel to extract messages to a ``.pot`` file in your project's ``i18n`` directory. First, the directory needs to be created. Don't forget to add it to your revision control system if one is in use: .. code-block:: bash $ cd translate_demo $ mkdir translate_demo/i18n $ svn add translate_demo/i18n Next we can extract all messages from the project with the following command: .. code-block:: bash $ python setup.py extract_messages running extract_messages extracting messages from translate_demo/__init__.py extracting messages from translate_demo/websetup.py ... extracting messages from translate_demo/tests/functional/test_hello.py writing PO template file to translate_demo/i18n/translate_demo.pot This will create a ``.pot`` file in the ``i18n`` directory that looks something like this: .. code-block:: pot # Translations template for translate_demo. # Copyright (C) 2007 ORGANIZATION # This file is distributed under the same license as the translate_demo project. # FIRST AUTHOR , 2007. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: translate_demo 0.0.0\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2007-08-02 18:01-0700\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9dev-r215\n" #: translate_demo/controllers/hello.py:10 translate_demo/controllers/hello.py:13 msgid "Hello" msgstr "" The ``.pot`` details that appear here can be customized via the ``extract_messages`` configuration in your project's ``setup.cfg`` (See the `Babel Command-Line Interface Documentation `_ for all configuration options). Next, we'll initialize a catalog (``.po`` file) for the Spanish language: .. code-block:: bash $ python setup.py init_catalog -l es running init_catalog creating catalog 'translate_demo/i18n/es/LC_MESSAGES/translate_demo.po' based on 'translate_demo/i18n/translate_demo.pot' Then we can edit the last line of the new Spanish ``.po`` file to add a translation of ``"Hello"``: .. code-block:: bash msgid "Hello" msgstr "¡Hola!" Finally, to utilize these translations in our application, we need to compile the ``.po`` file to a ``.mo`` file: .. code-block:: bash $ python setup.py compile_catalog running compile_catalog 1 of 1 messages (100%) translated in 'translate_demo/i18n/es/LC_MESSAGES/translate_demo.po' compiling catalog 'translate_demo/i18n/es/LC_MESSAGES/translate_demo.po' to 'translate_demo/i18n/es/LC_MESSAGES/translate_demo.mo' We can also use the ``update_catalog`` command to merge new messages from the ``.pot`` to the ``.po`` files. For example, if we later added the following line of code to the end of HelloController's index method: .. code-block :: python response.write('Goodbye: %s' % _('Goodbye')) We'd then need to re-extract the messages from the project, then run the ``update_catalog`` command: .. code-block:: bash $ python setup.py extract_messages running extract_messages extracting messages from translate_demo/__init__.py extracting messages from translate_demo/websetup.py ... extracting messages from translate_demo/tests/functional/test_hello.py writing PO template file to translate_demo/i18n/translate_demo.pot $ python setup.py update_catalog running update_catalog updating catalog 'translate_demo/i18n/es/LC_MESSAGES/translate_demo.po' based on 'translate_demo/i18n/translate_demo.pot' We'd then edit our catalog to add a translation for "Goodbye", and recompile the ``.po`` file as we did above. For more information, see the `Babel documentation `_ and the `GNU Gettext Manual `_. Back To Work ============ Next we'll need to repeat the process of creating a ``.mo`` file for the ``en`` and ``fr`` locales: .. code-block:: bash $ python setup.py init_catalog -l en running init_catalog creating catalog 'translate_demo/i18n/en/LC_MESSAGES/translate_demo.po' based on 'translate_demo/i18n/translate_demo.pot' $ python setup.py init_catalog -l fr running init_catalog creating catalog 'translate_demo/i18n/fr/LC_MESSAGES/translate_demo.po' based on 'translate_demo/i18n/translate_demo.pot' Modify the last line of the ``fr`` catalog to look like this: .. code-block:: po #: translate_demo/controllers/hello.py:10 translate_demo/controllers/hello.py:13 msgid "Hello" msgstr "Bonjour" Since our original messages are already in English, the ``en`` catalog can stay blank; gettext will fallback to the original. Once you've edited these new ``.po`` files and compiled them to ``.mo`` files, you'll end up with an ``i18n`` directory containing: .. code-block:: text i18n/translate_demo.pot i18n/en/LC_MESSAGES/translate_demo.po i18n/en/LC_MESSAGES/translate_demo.mo i18n/es/LC_MESSAGES/translate_demo.po i18n/es/LC_MESSAGES/translate_demo.mo i18n/fr/LC_MESSAGES/translate_demo.po i18n/fr/LC_MESSAGES/translate_demo.mo Testing the Application ======================= Start the server with the following command: .. code-block:: bash $ paster serve --reload development.ini Test your controller by visiting http://localhost:5000/hello. You should see the following output: .. code-block:: text Default: Hello fr: Bonjour en: Hello es: ¡Hola! You can now set the language used in a controller on the fly. For example this could be used to allow a user to set which language they wanted your application to work in. You could save the value to the session object: .. code-block:: python session['lang'] = 'en' session.save() then on each controller call the language to be used could be read from the session and set in your controller's ``__before__()`` method so that the pages remained in the same language that was previously set: .. code-block:: python def __before__(self): if 'lang' in session: set_lang(session['lang']) Pylons also supports defining the default language to be used in the configuration file. Set a ``lang`` variable to the desired default language in your ``development.ini`` file, and Pylons will automatically call ``set_lang`` with that language at the beginning of every request. E.g. to set the default language to Spanish, you would add ``lang = es`` to your ``development.ini``: .. code-block:: ini [app:main] use = egg:translate_demo lang = es If you are running the server with the ``--reload`` option the server will automatically restart if you change the ``development.ini`` file. Otherwise restart the server manually and the output would this time be as follows: .. code-block:: text Default: ¡Hola! fr: Bonjour en: Hello es: ¡Hola! Fallback Languages ================== If your code calls ``_()`` with a string that doesn't exist at all in your language catalog, the string passed to ``_()`` is returned instead. Modify the last line of the hello controller to look like this: .. code-block:: python response.write("%s %s, %s" % (_('Hello'), _('World'), _('Hi!'))) .. warning :: Of course, in real life breaking up sentences in this way is very dangerous because some grammars might require the order of the words to be different. If you run the example again the output will be: .. code-block:: text Default: ¡Hola! fr: Bonjour World! en: Hello World! es: ¡Hola! World! This is because we never provided a translation for the string ``'World!'`` so the string itself is used. Pylons also provides a mechanism for fallback languages, so that you can specify other languages to be used if the word is omitted from the main language's catalog. In this example we choose ``fr`` as the main language but ``es`` as a fallback: .. code-block:: python import logging from pylons.i18n import set_lang from translate_demo.lib.base import * log = logging.getLogger(__name__) class HelloController(BaseController): def index(self): set_lang(['fr', 'es']) return "%s %s, %s" % (_('Hello'), _('World'), _('Hi!')) If ``Hello`` is in the ``fr`` ``.mo`` file as ``Bonjour``, ``World`` is only in ``es`` as ``Mundo`` and none of the catalogs contain ``Hi!``, you'll get the multilingual message: ``Bonjour Mundo, Hi!``. This is a combination of the French, Spanish and original (English in this case, as defined in our source code) words. You can also add fallback languages after calling ``set_lang`` via the ``pylons.i18n.add_fallback`` function. Translations will be tested in the order you add them. .. note:: Fallbacks are reset after calling ``set_lang(lang)`` -- that is, fallbacks are associated with the currently selected language. One case where using fallbacks in this way is particularly useful is when you wish to display content based on the languages requested by the browser in the ``HTTP_ACCEPT_LANGUAGE`` header. Typically the browser may submit a number of languages so it is useful to be add fallbacks in the order specified by the browser so that you always try to display words in the language of preference and search the other languages in order if a translation cannot be found. The languages defined in the ``HTTP_ACCEPT_LANGUAGE`` header are available in Pylons as ``request.languages`` and can be used like this: .. code-block:: python for lang in request.languages: add_fallback(lang) Translations Within Templates ============================= You can also use the ``_()`` function within templates in exactly the same way you do in code. For example, in a Mako template: .. code-block:: mako ${_('Hello')} would produce the string ``'Hello'`` in the language you had set. Babel currently supports extracting gettext messages from Mako and Genshi templates. The Mako extractor also provides support for translator comments. Babel can be extended to extract messages from other sources via a `custom extraction method plugin `_. Pylons (as of 0.9.6) automatically configures a Babel extraction mapping for your Python source code and Mako templates. This is defined in your project's setup.py file: .. code-block:: python message_extractors = {'translate_demo': [ ('**.py', 'python', None), ('templates/**.mako', 'mako', None), ('public/**', 'ignore', None)]}, For a project using Genshi instead of Mako, the Mako line might be replaced with: .. code-block:: python ('templates/**.html, 'genshi', None), See `Babel's documentation on Message Extraction `_ for more information. Lazy Translations ================= Occasionally you might come across a situation when you need to translate a string when it is accessed, not when the ``_()`` or other functions are called. Consider this example: .. code-block:: python import logging from pylons.i18n import get_lang, set_lang from translate_demo.lib.base import * log = logging.getLogger(__name__) text = _('Hello') class HelloController(BaseController): def index(self): response.write('Default: %s
' % _('Hello')) for lang in ['fr','en','es']: set_lang(lang) response.write("%s: %s
" % (get_lang(), _('Hello'))) response.write('Text: %s
' % text) If we run this we get the following output: .. code-block:: text Default: Hello ['fr']: Bonjour ['en']: Good morning ['es']: Hola Text: Hello This is because the function ``_('Hello')`` just after the imports is called when the default language is ``en`` so the variable ``text`` gets the value of the English translation even though when the string was used the default language was Spanish. The rule of thumb in these situations is to try to avoid using the translation functions in situations where they are not executed on each request. For situations where this isn't possible, perhaps because you are working with legacy code or with a library which doesn't support internationalization, you need to use lazy translations. If we modify the above example so that the import statements and assignment to ``text`` look like this: .. code-block:: python from pylons.i18n import get_lang, lazy_gettext, set_lang from helloworld.lib.base import * log = logging.getLogger(__name__) text = lazy_gettext('Hello') then we get the output we expected: .. code-block:: text Default: Hello ['fr']: Bonjour ['en']: Good morning ['es']: Hola Text: Hola There are lazy versions of all the standard Pylons `translation functions `_. There is one drawback to be aware of when using the lazy translation functions: they are not actually strings. This means that if our example had used the following code it would have failed with an error ``cannot concatenate 'str' and 'LazyString' objects``: .. code-block:: python response.write('Text: ' + text + '
') For this reason you should only use the lazy translations where absolutely necessary and should always ensure they are converted to strings by calling ``str()`` or ``repr()`` before they are used in operations with real strings. Producing a Python Egg ====================== Finally you can produce an egg of your project which includes the translation files like this: .. code-block:: bash $ python setup.py bdist_egg The ``setup.py`` automatically includes the ``.mo`` language catalogs your application needs so that your application can be distributed as an egg. This is done with the following line in your ``setup.py`` file: .. code-block:: python package_data={'translate_demo': ['i18n/*/LC_MESSAGES/*.mo']}, Plural Forms ============ Pylons also provides the ``ungettext()`` function. It's designed for internationalizing plural words, and can be used as follows: .. code-block:: python ungettext('There is %(num)d file here', 'There are %(num)d files here', n) % {'num': n} Plural forms have a different type of entry in ``.pot``/``.po`` files, as described in `The Format of PO Files `_ in `GNU Gettext's Manual `_: .. code-block:: pot #: translate_demo/controllers/hello.py:12 #, python-format msgid "There is %(num)d file here" msgid_plural "There are %(num)d files here" msgstr[0] "" msgstr[1] "" One thing to keep in mind is that other languages don't have the same plural forms as English. While English only has 2 plural forms, singular and plural, Slovenian has 4! That means that you *must* use ugettext for proper pluralization. Specifically, the following will not work: .. code-block:: python # BAD! if n == 1: msg = _("There was no dog.") else: msg = _("There were no dogs.") Summary ======= This document only covers the basics of internationalizing and localizing a web application. GNU Gettext is an extensive library, and the GNU Gettext Manual is highly recommended for more information. Babel also provides support for interfacing to the CLDR (Common Locale Data Repository), providing access to various locale display names, localized number and date formatting, etc. You should also be able to internationalize and then localize your application using Pylons' support for GNU gettext. Further Reading =============== http://en.wikipedia.org/wiki/Internationalization Please feel free to report any mistakes to the Pylons mailing list or to the author. Any corrections or clarifications would be gratefully received. .. note:: This is a work in progress. We hope the internationalization, localization and Unicode support in Pylons is now robust and flexible but we would appreciate hearing about any issues we have. Just drop a line to the pylons-discuss mailing list on Google Groups. :mod:`babel.core` -- Babel core classes =================================================== .. module:: babel.core .. automodule:: babel Module Contents --------------- .. autoclass:: Locale :members: .. autofunction:: default_locale .. autofunction:: negotiate_locale .. autofunction:: parse_locale :mod:`babel.localedata` --- Babel locale data ==================================================== .. automodule:: babel.localedata .. autofunction:: exists .. autofunction:: exists :mod:`babel.dates` -- Babel date classes =================================================== .. automodule:: babel.dates Module Contents --------------- .. autoclass:: DateTimeFormat :members: .. autoclass:: DateTimePattern :members: :mod:`babel.numbers` -- Babel number classes =================================================== .. automodule:: babel.numbers Module Contents --------------- .. autoclass:: NumberFormatError :members: .. autoclass:: NumberPattern :members: __init__, apply .. autofunction:: format_number .. autofunction:: format_decimal .. autofunction:: format_percent .. autofunction:: format_scientific .. autofunction:: parse_number .. autofunction:: parse_decimal .. autofunction: format_currency ================================================ FILE: pylons/docs/en/index.rst ================================================ Pylons Reference Documentation ============================== .. image:: _static/pylon1.jpg :alt: The First Pylon of the Ramesseum, Thebes is approximately 69m long and 22m high, and marks the entrance to the Main Temple and the First Courtyard. :align: center :height: 255 :width: 780 Getting Started with Pylons --------------------------- .. toctree:: :maxdepth: 2 gettingstarted concepts MVC Reference ------------- .. toctree:: :maxdepth: 2 controllers views models advanced_models Project Configuration and Logging --------------------------------- .. toctree:: :maxdepth: 2 configuration logging Forms, Validation, and Helpers ------------------------------ .. toctree:: :maxdepth: 2 helpers forms Internationalization, Sessions, and Caching ------------------------------------------- .. toctree:: :maxdepth: 2 i18n sessions caching Testing, Upgrading, and Deploying --------------------------------- .. toctree:: :maxdepth: 2 testing debugging upgrading deployment Installation for Windows / Python 2.3 ------------------------------------- .. toctree:: :maxdepth: 2 python23_install windowsnotes Pylons on Jython ---------------- .. toctree:: :maxdepth: 2 jython Advanced Pylons --------------- .. toctree:: :maxdepth: 2 security_policy_for_bugs wsgi_support advanced_pylons/index execution Module Listing -------------- .. toctree:: :maxdepth: 2 modules/index thirdparty/index glossary For further information, indices are available: Indices ======= * :ref:`genindex` * :ref:`modindex` * :ref:`search` * :ref:`glossary` ================================================ FILE: pylons/docs/en/jython.rst ================================================ .. _jython: ================ Pylons on Jython ================ Pylons supports `Jython `_ as of v0.9.7. Installation ============ The installation process is the same as CPython, as described in :ref:`getting_started`. At least Jython 2.5b2 is required. .. _java_deployment: Deploying to Java Web servers ============================= The Java platform defines the `Servlet API`_ for creating web applications. The `modjy`_ library included with Jython provides a gateway between Java Servlets and WSGI applications. The `snakefight`_ tool can create a `WAR file`_ from a Pylons application (and modjy) that's suitable for deployment to the various `Servlet containers`_ (such as `Apache Tomcat`_ or `Sun's Glassfish`_). Creating .wars with snakefight ------------------------------ First, install snakefight: .. code-block :: bash $ easy_install snakefight This adds an additional command to distutils: :command:`bdist_war`. Pylons applications are loaded from Paste, via its ``paste.app_factory`` entry point and a Paste style configuration file. :command:`bdist_war` knows how to setup Paste apps for deployment when specified the :option:`--paste-config` option: .. code-block :: bash $ paster make-config MyApp production.ini $ jython setup.py bdist_war --paste-config production.ini As with any distutils command the preferred options can instead be added to the :file:`setup.cfg` in the root directory of the project: .. code-block :: ini [bdist_war] paste-config = production.ini Then we can simply run: .. code-block :: bash $ jython setup.py bdist_war :command:`bdist_war` creates a :file:`.war` with the following: - Jython's :file:`jar` files in :file:`WEB-INF/lib` - Jython's stdlib in :file:`WEB-INF/lib-python` - Your application's required eggs in :file:`WEB-INF/lib-python` With the :option:`--paste-config` option, it also: - Creates a simple loader for the application/config - Generates a :file:`web.xml` deployment descriptor configuring modjy to load the application with the simple loader For further information/usages, see `snakefight's documentation`_. .. _`Servlet API`: http://en.wikipedia.org/wiki/Java_Servlet .. _`modjy`: http://modjy.xhaus.com/ .. _`snakefight`: http://pypi.python.org/pypi/snakefight .. _`snakefight's documentation`: http://pypi.python.org/pypi/snakefight .. _`WAR file`: http://en.wikipedia.org/wiki/Sun_WAR_(file_format) .. _`Servlet containers`: http://en.wikipedia.org/wiki/Servlet_container .. _`Apache Tomcat`: http://tomcat.apache.org/ .. _`Sun's Glassfish`: http://glassfish.org/ ================================================ FILE: pylons/docs/en/logging.rst ================================================ .. _logging: ======= Logging ======= Logging messages ---------------- As of Pylons 0.9.6, Pylons controllers (created via ``paster controller/restcontroller``) and ``websetup.py`` create their own Logger objects via `Python's logging module `_. For example, in the helloworld project's hello controller (``helloworld/controllers/hello.py``): .. code-block:: python import logging from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect log = logging.getLogger(__name__) class HelloController(BaseController): def index(self): ... Python's special ``__name__`` variable refers to the current module's fully qualified name; in this case, ``helloworld.controllers.hello``. To log messages, simply use methods available on that Logger object: .. code-block:: python import logging from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect log = logging.getLogger(__name__) class HelloController(BaseController): def index(self): content_type = 'text/plain' content = 'Hello World!' log.debug('Returning: %s (content-type: %s)', content, content_type) response.content_type = content_type return content Which will result in the following printed to the console, on stderr: .. code-block:: text 16:20:20,440 DEBUG [helloworld.controllers.hello] Returning: Hello World! (content-type: text/plain) Basic Logging configuration --------------------------- As of Pylons 0.9.6, the default ini files include a basic configuration for the logging module. Paste ini files use the Python standard `ConfigParser format `_; the same format used for the Python `logging module's Configuration file format `_. ``paster``, when loading an application via the ``paster`` ``serve``, ``shell`` or ``setup-app`` commands, calls the `logging.fileConfig function `_ on that specified ini file if it contains a 'loggers' entry. ``logging.fileConfig`` reads the logging configuration from a ``ConfigParser`` file. Logging configuration is provided in both the default ``development.ini`` and the production ini file (created via ``paster make-config ``). The production ini's logging setup is a little simpler than the ``development.ini``'s, and is as follows: .. code-block:: ini # Logging configuration [loggers] keys = root [handlers] keys = console [formatters] keys = generic [logger_root] level = INFO handlers = console [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s One root Logger is created that logs only messages at a level above or equal to the ``INFO`` level to stderr, with the following format: .. code-block:: text 2007-08-17 15:04:08,704 INFO [helloworld.controllers.hello] Loading resource, id: 86 For those familiar with the ``logging.basicConfig`` function, this configuration is equivalent to the code: .. code-block:: python logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)-5.5s [%(name)s] %(message)s') The default ``development.ini``'s logging section has a couple of differences: it uses a less verbose timestamp, and defaults your application's log messages to the ``DEBUG`` level (described in the next section). Pylons and many other libraries (such as Beaker, SQLAlchemy, Paste) log a number of messages for debugging purposes. Switching the root Logger level to ``DEBUG`` reveals them: .. code-block:: ini [logger_root] #level = INFO level = DEBUG handlers = console Filtering log messages ---------------------- Often there's too much log output to sift through, such as when switching the root Logger's level to ``DEBUG``. An example: you're diagnosing database connection issues in your application and only want to see SQLAlchemy's ``DEBUG`` messages in relation to database connection pooling. You can leave the root Logger's level at the less verbose ``INFO`` level and set that particular SQLAlchemy Logger to ``DEBUG`` on its own, apart from the root Logger: .. code-block:: ini [logger_sqlalchemy.pool] level = DEBUG handlers = qualname = sqlalchemy.pool then add it to the list of Loggers: .. code-block:: ini [loggers] keys = root, sqlalchemy.pool No Handlers need to be configured for this Logger as by default non root Loggers will propagate their log records up to their parent Logger's Handlers. The root Logger is the top level parent of all Loggers. This technique is used in the default ``development.ini``. The root Logger's level is set to ``INFO``, whereas the application's log level is set to ``DEBUG``: .. code-block:: ini # Logging configuration [loggers] keys = root, helloworld .. code-block:: ini [logger_helloworld] level = DEBUG handlers = qualname = helloworld All of the child Loggers of the helloworld Logger will inherit the ``DEBUG`` level unless they're explicitly set differently. Meaning the ``helloworld.controllers.hello``, ``helloworld.websetup`` (and all your app's modules') Loggers by default have an effective level of ``DEBUG`` too. For more advanced filtering, the logging module provides a `Filter `_ object; however it cannot be used directly from the configuration file. Advanced Configuration ---------------------- To capture log output to a separate file, use a `FileHandler `_ (or a `RotatingFileHandler `_): .. code-block:: ini [handler_accesslog] class = FileHandler args = ('access.log','a') level = INFO formatter = generic Before it's recognized, it needs to be added to the list of Handlers: .. code-block:: ini [handlers] keys = console, accesslog and finally utilized by a Logger. .. code-block:: ini [logger_root] level = INFO handlers = console, accesslog These final 3 lines of configuration directs all of the root Logger's output to the access.log as well as the console; we'll want to disable this for the next section. Request logging with Paste's TransLogger ---------------------------------------- Paste provides the `TransLogger `_ middleware for logging requests using the `Apache Combined Log Format `_. TransLogger combined with a FileHandler can be used to create an ``access.log`` file similar to Apache's. Like any standard middleware with a Paste entry point, TransLogger can be configured to wrap your application in the ``[app:main]`` section of the ini file: .. code-block:: ini filter-with = translogger [filter:translogger] use = egg:Paste#translogger setup_console_handler = False This is equivalent to wrapping your app in a TransLogger instance via the bottom of your project's ``config/middleware.py`` file: .. code-block:: python from paste.translogger import TransLogger app = TransLogger(app, setup_console_handler=False) return app TransLogger will automatically setup a logging Handler to the console when called with no arguments, so it 'just works' in environments that don't configure logging. Since we've configured our own logging Handlers, we need to disable that option via ``setup_console_handler = False``. With the filter in place, TransLogger's Logger (named the 'wsgi' Logger) will propagate its log messages to the parent Logger (the root Logger), sending its output to the console when we request a page: .. code-block:: text 00:50:53,694 INFO [helloworld.controllers.hello] Returning: Hello World! (content-type: text/plain) 00:50:53,695 INFO [wsgi] 192.168.1.111 - - [11/Aug/2007:20:09:33 -0700] "GET /hello HTTP/1.1" 404 - "-" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.6) Gecko/20070725 Firefox/2.0.0.6" To direct TransLogger to the ``access.log`` FileHandler defined above, we need to add that FileHandler to the wsgi Logger's list of Handlers: .. code-block:: ini # Logging configuration [loggers] keys = root, wsgi .. code-block:: ini [logger_wsgi] level = INFO handlers = handler_accesslog qualname = wsgi propagate = 0 As mentioned above, non-root Loggers by default propagate their log Records to the root Logger's Handlers (currently the console Handler). Setting ``propagate`` to 0 (false) here disables this; so the ``wsgi`` Logger directs its records only to the ``accesslog`` Handler. Finally, there's no need to use the ``generic`` Formatter with TransLogger as TransLogger itself provides all the information we need. We'll use a Formatter that passes-through the log messages as is: .. code-block:: ini [formatters] keys = generic, accesslog .. code-block:: ini [formatter_accesslog] format = %(message)s Then wire this new ``accesslog`` Formatter into the FileHandler: .. code-block:: ini [handler_accesslog] class = FileHandler args = ('access.log','a') level = INFO formatter = accesslog Logging to wsgi.errors ---------------------- Pylons provides a custom logging Handler class, `pylons.log.WSGIErrorsHandler `_, for logging output to ``environ['wsgi.errors']``: the WSGI server's error stream (see the `WSGI Spefification, PEP 333 `_ for more information). ``wsgi.errors`` can be useful to log to in certain situations, such as when deployed under Apache mod_wsgi/mod_python, where the ``wsgi.errors`` stream is the Apache error log. To configure logging of only ``ERROR`` (and ``CRITICAL``) messages to ``wsgi.errors``, add the following to the ini file: .. code-block:: ini [handlers] keys = console, wsgierrors .. code-block:: ini [handler_wsgierrors] class = pylons.log.WSGIErrorsHandler args = () level = ERROR format = generic then add the new Handler name to the list of Handlers used by the root Logger: .. code-block:: ini [logger_root] level = INFO handlers = console, wsgierrors .. warning :: ``WSGIErrorsHandler`` does not receive log messages created during application startup. This is due to the ``wsgi.errors`` stream only being available through the ``environ`` dictionary; which isn't available until a request is made. Lumberjacking with log4j's Chainsaw =================================== Java's ``log4j`` project provides the Java GUI application `Chainsaw `_ for viewing and managing log messages. Among its features are the ability to filter log messages on the fly, and customizable color highlighting of log messages. We can configure Python's logging module to output to a format parsable by Chainsaw, ``log4j``'s `XMLLayout `_ format. To do so, we first need to install the `Python XMLLayout package `_: .. code-block:: bash $ easy_install XMLLayout It provides a log Formatter that generates ``XMLLayout`` XML. It also provides ``RawSocketHandler``; like the logging module's ``SocketHandler``, it sends log messages across the network, but does not pickle them. The following is an example configuration for sending ``XMLLayout`` log messages across the network to Chainsaw, if it were listening on `localhost` port `4448`: .. code-block:: ini [handlers] keys = console, chainsaw [formatters] keys = generic, xmllayout [logger_root] level = INFO handlers = console, chainsaw .. code-block:: ini [handler_chainsaw] class = xmllayout.RawSocketHandler args = ('localhost', 4448) level = NOTSET formatter = xmllayout .. code-block:: ini [formatter_xmllayout] class = xmllayout.XMLLayout This configures any log messages handled by the root Logger to also be sent to Chainsaw. The default ``development.ini`` configures the root Logger to the ``INFO`` level, however in the case of using Chainsaw, it is preferable to configure the root Logger to ``NOTSET`` so *all* log messages are sent to Chainsaw. Instead, we can restrict the console handler to the ``INFO`` level: .. code-block:: ini [logger_root] level = NOTSET handlers = console [handler_console] class = StreamHandler args = (sys.stderr,) level = INFO formatter = generic Chainsaw can be downloaded from its `home page `_, but can also be launched directly from a Java-enabled browser via the link: `Chainsaw web start `_. It can be configured from the GUI, but it also supports reading its configuration from a ``log4j.xml`` file. The following ``log4j.xml`` file configures Chainsaw to listen on port `4448` for ``XMLLayout`` style log messages. It also hides Chainsaw's own logging messages under the ``WARN`` level, so only your app's log messages are displayed: .. code-block:: xml Chainsaw will prompt for a configuration file upon startup. The configuration can also be loaded later by clicking `File`/`Load Log4J File...`. You should see an XMLSocketReceiver instance loaded in Chainsaw's Receiver list, configured at port `4448`, ready to receive log messages. Here's how the Pylons stack's log messages can look with colors defined (using Chainsaw on OS X): .. image:: _static/Pylons_Stack-Chainsaw-OSX.png :width: 750px :height: 469px Alternate Logging Configuration style ===================================== Pylons' default ini files include a basic configuration for Python's logging module. Its format matches the standard Python :mod:`logging` module's `config file format `_ . If a more concise format is preferred, here is Max Ischenko's demonstration of an alternative style to setup logging. The following function is called at the application start up (e.g. Global ctor): .. code-block:: python def setup_logging(): logfile = config['logfile'] if logfile == 'STDOUT': # special value, used for unit testing logging.basicConfig(stream=sys.stdout, level=logging.DEBUG, #format='%(name)s %(levelname)s %(message)s', #format='%(asctime)s,%(msecs)d %(levelname)s %(message)s', format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', datefmt='%H:%M:%S') else: logdir = os.path.dirname(os.path.abspath(logfile)) if not os.path.exists(logdir): os.makedirs(logdir) logging.basicConfig(filename=logfile, mode='at+', level=logging.DEBUG, format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', datefmt='%Y-%b-%d %H:%M:%S') setup_thirdparty_logging() The setup_thirdparty_logging function searches through the certain keys of the application ``.ini`` file which specify logging level for a particular logger (module). .. code-block:: python def setup_thirdparty_logging(): for key in config: if not key.endswith('logging'): continue value = config.get(key) key = key.rstrip('.logging') loglevel = logging.getLevelName(value) log.info('Set %s logging for %s', logging.getLevelName(loglevel), key) logging.getLogger(key).setLevel(loglevel) Relevant section of the .ini file (example): .. code-block:: ini sqlalchemy.logging = WARNING sqlalchemy.orm.unitofwork.logging = INFO sqlalchemy.engine.logging = DEBUG sqlalchemy.orm.logging = INFO routes.logging = WARNING This means that routes logger (and all sub-loggers such as routes.mapper) only passes through messages of at least WARNING level; sqlalachemy defaults to WARNING level but some loggers are configured with more verbose level to aid debugging. ================================================ FILE: pylons/docs/en/models.rst ================================================ .. _models: ====== Models ====== About the model =============== .. image:: _static/pylon3.jpg :alt: :align: left :height: 450px :width: 368px In the MVC paradigm the *model* manages the behavior and data of the application domain, responds to requests for information about its state and responds to instructions to change state. The model represents enterprise data and business rules. It is where most of the processing takes place when using the MVC design pattern. Databases are in the remit of the model, as are component objects such as :term:`EJBs` and :term:`ColdFusion Components`. The data returned by the model is display-neutral, i.e. the model applies no formatting. A single model can provide data for any number of display interfaces. This reduces code duplication as model code is written only once and is then reused by all of the views. Because the model returns data without applying any formatting, the same components can be used with any interface. For example, most data is typically formatted with HTML but it could also be formatted with Macromedia Flash or WAP. The model also isolates and handles state management and data persistence. For example, a Flash site or a wireless application can both rely on the same session-based shopping cart and e-commerce processes. Because the model is self-contained and separate from the controller and the view, changing the data layer or business rules is less painful. If it proves necessary to switch databases, e.g. from MySQL to Oracle, or change a data source from an RDBMS to LDAP, the only required task is that of altering the model. If the view is written correctly, it won’t care at all whether a list of users came from a database or an LDAP server. This freedom arises from the way that the three parts of an MVC-based application act as `black boxes`, the inner workings of each one are hidden from, and are independent of, the other two. The approach promotes well-defined interfaces and self-contained components. .. note:: *adapted from an Oct 2002 TechRepublic article by by Brian Kotek: "MVC design pattern brings about better organization and code reuse"* - http://articles.techrepublic.com.com/5100-10878_11-1049862.html Model Basics ============ Pylons provides a :data:`model` package to put your database code in but does not offer a database engine or API. Instead there are several third-party APIs to choose from. The recommended and most commonly-adopted approach used in Pylons applications is to use SQLAlchemy with the declarative configuration style and develop with a relational database (Postgres, MySQL, etc). **This is the documented and recommended approach for creating a Pylons project with a SQL database**. Install SQLAlchemy ------------------ We'll assume you've already installed Pylons and have the `easy_install` command. At the command line, run: .. code-block:: bash easy_install SQLAlchemy Next you'll have to install a database engine and its Python bindings. If you don't know which one to choose, SQLite is a good one to start with. It's small and easy to install, and Python 2.5 includes bindings for it. Installing the database engine is beyond the scope of this article, but here are the Python bindings you'll need for the most popular engines: .. code-block:: bash easy_install pysqlite # If you use SQLite and Python 2.4 (not needed for Python 2.5) easy_install MySQL-python # If you use MySQL easy_install psycopg2 # If you use PostgreSQL See the `Python Package Index `_ (formerly the Cheeseshop) for other database drivers. .. tip:: Checking Your Version To see which version of SQLAlchemy you have, go to a Python shell and look at ``sqlalchemy.__version__`` : .. code-block:: pycon >>> import sqlalchemy >>> sqlalchemy.__version__ 0.5.8 Create a Pylons Project with SQLAlchemy --------------------------------------- When creating a Pylons project, one of the questions asked as part of the project creation dialogue is whether the project should be configured with SQLAlchemy. Before continuing, ensure that the project was created with this option, if it's missing the :file:`model/meta.py` file, then the project should be re-created with this option. .. tip:: The project doesn't need to be deleted to add this option, just re-run the `paster` command in the project's parent directory and answer "yes" to the SQLAlchemy prompt. The files will then be added and existing files will present a prompt on whether to replace them or leave the current file. Configure SQLAlchemy -------------------- When your Pylons application runs, it needs to know which database to connect to. Normally you put this information in *development.ini* and activate the model in *environment.py*: put the following in *development.ini* in the `\[app:main\]` section, depending on your database, For SQLite ^^^^^^^^^^ .. code-block:: ini sqlalchemy.url = sqlite:///%(here)s/mydatabasefilename.sqlite Where `mydatabasefilename.db` is the path to your SQLite database file. "%(here)s" represents the directory containing the development.ini file. If you're using an absolute path, use four slashes after the colon: "sqlite:////var/lib/myapp/database.sqlite". Don't use a relative path (three slashes) because the current directory could be anything. The example has three slashes because the value of "%(here)s" always starts with a slash (or the platform equivalent; e.g., "C:\\foo" on Windows). For MySQL ^^^^^^^^^ .. code-block:: ini sqlalchemy.url = mysql://username:password@host:port/database sqlalchemy.pool_recycle = 3600 Enter your username, password, host (localhost if it is on your machine), port number (usually 3306) and the name of your database. The second line is an example of setting `engine options `_. It's important to set "pool_recycle" for MySQL to prevent "MySQL server has gone away" errors. This is because MySQL automatically closes idle database connections without informing the application. Setting the connection lifetime to 3600 seconds (1 hour) ensures that the connections will be expired and recreated before MySQL notices they're idle. Don't be tempted to use the ".echo" option to enable SQL logging because it may cause duplicate log output. Instead see the `Logging`_ section below to integrate MySQL logging into Paste's logging system. For PostgreSQL ^^^^^^^^^^^^^^ .. code-block:: ini sqlalchemy.url = postgres://username:password@host:port/database Enter your username, password, host (localhost if it is on your machine), port number (usually 5432) and the name of your database. Organizing ========== When you answer "yes" to the SQLAlchemy question when creating a Pylons project, it configures a simple default model. The model consists of two files: :file:`model/__init__.py` and :file:`model/meta.py`. :file:`model/__init__.py` ------------------------- The file :file:`model/__init__.py` contains the table definitions, the ORM classes and an :func:`init_model` function. This :func:`init_model` function must be called at application startup. In the Pylons default project template this call is made in the :func:`load_environment` function (in the file :file:`config/environment.py`). :file:`model/meta.py` --------------------- :file:`model/meta.py` is merely a container for a few housekeeping objects required by SQLAlchemy such as :class:`Session`, ``metadata`` and ``engine`` to avoid import issues. In the context of the default Pylons application, only the :class:`Session` object is instantiated. The objects are optional in the context of other applications that do not make use of them and so if you answer "no" to the SQLAlchemy question when creating a Pylons project, the creation of :file:`model/meta.py` is simply skipped. It is recommended that, for each model, a new module inside the ``model/`` directory should be created. This keeps the models tidy when they get larger as more domain specific code is added to each one. Creating a Model ================ SQLAlchemy 0.5 has an optional `Declarative` syntax which offers the convenience of defining the table and the ORM class in one step. This is the recommended usage of SQLAlchemy. Create a :file:`model/person.py` module:: """Person model""" from sqlalchemy import Column from sqlalchemy.types import Integer, String from myapp.model.meta import Base class Person(Base): __tablename__ = "person" id = Column(Integer, primary_key=True) name = Column(String(100)) email = Column(String(100)) def __init__(self, name='', email=''): self.name = name self.email = email def __repr__(self): return "`_ and `SQLAlchemy manual`_ Creating the Database ===================== To actually create the tables in the database, you call the metadata's `.create_all()` method. You can do this interactively or use `paster`'s application initialization feature. To do this, put the code in :file:`myapp/websetup.py`. After the `load_environment()` call, put: .. code-block:: python from myapp.model.meta import Base, Session log.info("Creating tables") Base.metadata.drop_all(checkfirst=True, bind=Session.bind) Base.metadata.create_all(bind=Session.bind) log.info("Successfully setup") Then run the following on the command line: .. code-block:: bash $ paster setup-app development.ini A brief guide to using model objects in the Controller ====================================================== In which we: query a model, update a model entity, create a model entity and delete several model entities, all inside a Pylons controller. To illustrate some typical ways of handling model objects in the Controller, we will draw from the example :class:`PagesController` code of the :ref:`QuickWiki Tutorial`. The :class:`Session` -------------------- The SQLAlchemy-provided :class:`Session` object is a crucially important facet when working with models and model object entities. The SQLAlchemy documentation describes the :class:`Session` thus: "In the most general sense, the Session establishes all conversations with the database and represents a "holding zone" for all the mapped instances which you’ve loaded or created during its lifespan." All of the model access that takes place in a Pylons controller is done in the context of a :class:`Session` providing a database connection reference that is created at the start of the processing of each request and destroyed at the end of the processing of the request. These creation and destruction operations are performed automatically by the :class:`BaseController` instantiated in :file:`MYAPP/lib/base.py` which is in turn subclassed for each standard Pylons controller, ensuring that subclassed controllers can access the database only in a request-specific context which, in turn, protects against data accidentally leaking across requests. .. seeAlso:: SQLAlchemy documentation for the `Session object `_ The net effect of this is that a fully-instantiated :class:`Session` object is available for import and immediate use in the controller for, e.g. querying the model. Querying the model ------------------ The :class:`Session` object provides a :func:`query` function that, when applied to a class of mapped model object, returns a SQLAlchemy :class:`Query` object that can be passed around and repeatedly consulted. .. seealso:: SQLAlchemy documentation for the `Query object `_ Standard usage is illustrated in this code for the :func:`__before__` function of the QuickWiki :class:`PagesController` in which ``self.page_q`` is bound to the :class:`Query` object returned by ``Session.query(Page)`` - where :class:`Page` is the class of mapped model object that will be the subject of the queries. .. code-block:: python from MYAPP.lib.base import Session from MYAPP.model import Page class PagesController(BaseController): def __before__(self): self.page_q = Session.query(Page) # [ ... ] The :class:`Query` object that is bound to ``self.page_q`` is now specialised to perform queries of the :class:`Page` declarative base entity / mapped model entity. .. seeAlso:: SQLAlchemy documentation for the `Querying the database `_ Here, in the context of a controller's :func:`index` action, it is used in a very straighforward manner - :func:`self.page_q.all` - to fuel a list comprehension that returns a list containing the ``title`` of every :class:`Page` object in the database: .. code-block:: python def index(self): c.titles = [page.title for page in self.page_q.all()] return render('/pages/index.mako') and ``self.page_q`` is used in similarly direct manner for the :func:`show` action that retrieves a Page with a given value of ``title`` and then calls the Page's :func:`get_wiki_content` class method. .. code-block:: python def show(self, title): page = self.page_q.filter_by(title=title).first() if page: c.content = page.get_wiki_content() return render('/pages/show.mako') elif wikiwords.match(title): return render('/pages/new.mako') abort(404) .. note:: the ``title`` argument to the function is bound when the request is dispatched by the Routes map, typically of the form: .. code-block:: python map.connect('show_page', '/page/show/{title}', controller='page', action='show') The :class:`Query` object has many other features, including filtering on conditions, ordering the results, grouping, etc. These are excellently described in the `SQLAlchemy manual`_. See especially the `Data Mapping `_ and `Session / Unit of Work `_ chapters. Creating, updating and deleting model entities ---------------------------------------------- When performing operations that change the state of the database, the recommended approach is for Pylons users to take full advantage of the abstraction provided by the SQLAlchemy ORM and simply treat the retrieved or created model entities as Python objects, make changes to them in a conventional Pythonic way, add them to or delete them from the :class:`Session` "holding zone" and call :func:`Session.commit` to commit the changes to the database. The three examples shown below are condensed illustrations of how these operations are typically performed in controller actions. Creating a model entity ^^^^^^^^^^^^^^^^^^^^^^^ SQLAlchemy's Declarative Base syntax allows model entity classes to act as constructors, accepting keyworded args and values. In this example, a new Page is created with the given title, the created model entity object is then added to the :class:`Session` and then the change is committed. .. code-block:: python def create(self, title): page = Page(title=title) Session.add(page) Session.commit() redirect_to('show_page', title=title) Updating a model entity ^^^^^^^^^^^^^^^^^^^^^^^ Perhaps the most straighforward use - a model entity object is retrieved from the database, a field value is updated and the change committed. (Note, this example is considerably abbreviated as a controller action - preliminary content checking has been omitted, as has exception handling for the database query.) .. code-block:: python def save(self, title): page = self.page_q.filter_by(title=title).first() page.content=escape(request.POST.getone('content')) Session.commit() redirect_to('show_page', title=title) Deleting a model entity ^^^^^^^^^^^^^^^^^^^^^^^ This example of shows the freedom that the Pylons user has to make repeated changes to the model (in this instance, repeatedly deleting entities from the database) before finally committing those changes by calling :func:`Session.commit`. .. code-block:: python def delete(self): titles = request.POST.getall('title') pages = self.page_q.filter(Page.title.in_(titles)) for page in pages: Session.delete(page) Session.commit() redirect_to('pages') The `Object Relational tutorial `_ in the SQLAlchemy documentation covers a basic SQLAlchemy object-relational mapping scenario in much more detail and the `SQL Expression tutorial `_ covers the details of manipulating and marshalling the model entity objects. Using multiple databases ------------------------ In order to use multiple databases, in :file:`MYAPP/model/meta.py` create as many instances of :class:`Base` as there are databases to connect to: .. code-block:: python """SQLAlchemy Metadata and Session object""" from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker __all__ = ['Base','Base2', 'Session'] # SQLAlchemy session manager. Updated by model.init_model() Session = scoped_session(sessionmaker()) # The declarative Base Base = declarative_base() Base2 = declarative_base() Declare the different database URLs in :file:`development.ini`, appending an integer to the ``sqlalchemy`` keyword in order to differentiate between them. .. code-block:: ini sqlalchemy.url = sqlite:///%(here)s/database_one.sqlite sqlalchemy.echo = true sqlalchemy2.url = sqlite:///%(here)s/database_two.sqlite sqlalchemy2.echo = false In :file:`MYAPP/config/environment.py`, pick up those db URL declarations by using the different keywords (in this example: `sqlalchemy` and `sqlalchemy2`). Create the engines and call :func:`model.init_model`, passing through both engines as parameters. .. code-block:: python # Setup the SQLAlchemy database engine # Engine 0 engine = engine_from_config(config, 'sqlalchemy.') engine2 = engine_from_config(config, 'sqlalchemy2.') model.init_model(engine, engine2) Bind the engines appropriately to the :class:`Base`-specific metadata in :file:`MYAPP/model/\_\_init\_\_.py` - note :func:`init_model` is expecting both engines to be supplied as formal parameters. .. code-block:: python def init_model(engine, engine2): meta.Base.metadata.bind = engine meta.Base2.metadata.bind = engine2 Then import :class:`Base` and/or :class:`Base2` .. code-block:: python from MYAPP.model.meta import Base, Base2 and use as required, e.g. .. code-block:: python class Author(Base2): __tablename__ = 'authors' id = Column(Integer, primary_key=True) keywords = relation("Keyword", secondary=keywords) Avoiding the "circular imports" problem of model interdependency ---------------------------------------------------------------- Closely-interdependent models can sometimes cause "circular import" problems, where importing one model file causes a dependent model file to be imported, which then cause the first model file to be imported, and so on round and round in circles. In order to break the circle, define the model entities as globals in :file:`MYAPP/model/meta.py` .. code-block:: python """The application's model objects""" import sqlalchemy as sa from MYAPP.model import meta from sqlalchemy.orm import scoped_session, sessionmaker def init_model(engine): """Call me before using any of the tables or classes in the model""" meta.Base.metadata.bind = engine import MYAPP.model.user User = MYAPP.model.user.User global User import MYAPP.model.newsletter Newsletter = MYAPP.model.newsletter.Newsletter global Newsletter import MYAPP.model.submission Submission = MYAPP.model.submission.Submission global Submission Testing the Models ------------------ Normal model usage works fine in model tests, however to use the metadata you must specify an engine connection for it. To have your tables created for every unit test in your project, use a :file:`test_models.py` such as: .. code-block:: python from myapp.tests import * from myapp import model from myapp.model import meta class TestModels(TestController): def setUp(self): meta.Session.remove() meta.Base.metadata.create_all(meta.engine) def test_index(self): # test your models pass .. note:: Notice that the tests inherit from TestController. This is to ensure that the application is setup so that the models will work. "nosetests --with-pylons=/path/to/test.ini ..." is another way to ensure that your model is properly initialized before the tests are run. This can be used when running non-controller tests. Logging ======= SQLAlchemy has several loggers that chat about the various aspects of its operation. To log all SQL statements executed along with their parameter values, put the following in :file:`development.ini`: .. code-block:: ini [logger_sqlalchemy] level = INFO handlers = qualname = sqlalchemy.engine Then modify the "[loggers]" section to enable your new logger: .. code-block:: ini [loggers] keys = root, myapp, sqlalchemy To log the results along with the SQL statements, set the level to DEBUG. This can cause a lot of output! To stop logging the SQL, set the level to WARN or ERROR. SQLAlchemy has several other loggers you can configure in the same way. "sqlalchemy.pool" level INFO tells when connections are checked out from the engine's connection pool and when they're returned. "sqlalchemy.orm" and buddies log various ORM operations. See "Configuring Logging" in the `SQLAlchemy manual`_. About SQLAlchemy ================ `SQLAlchemy `_ is by far the most common approach for Pylons databases. It provides a connection pool, a SQL statement builder, an object-relational mapper (ORM), and transaction support. SQLAlchemy works with several database engines (MySQL, PostgreSQL, SQLite, Oracle, Firebird, MS-SQL, Access via ODBC, etc) and understands the peculiar SQL dialect of each, making it possible to port a program from one engine to another by simply changing the connection string. Although its API is still changing gradually, SQLAlchemy is well tested, widely deployed, has excellent documentation, and its mailing list is quick with answers. SQLAlchemy lets you work at three different levels, and you can even use multiple levels in the same program: * The object-relational mapper (ORM) lets you interact with the database using your own object classes rather than writing SQL code. * The SQL expression language has many methods to create customized SQL statements, and the result cursor is more friendly than DBAPI's. * The low-level execute methods accept literal SQL strings if you find something the SQL builder can't do, such as adding a column to an existing table or modifying the column's type. If they return results, you still get the benefit of SQLAlchemy's result cursor. The first two levels are *database neutral*, meaning they hide the differences between the databases' SQL dialects. Changing to a different database is merely a matter of supplying a new connection URL. Of course there are limits to this, but SQLAlchemy is 90% easier than rewriting all your SQL queries. The `SQLAlchemy manual`_ should be your next stop for questions not covered here. It's very well written and thorough. SQLAlchemy add-ons ------------------ Most of these provide a higher-level ORM, either by combining the table definition and ORM class definition into one step, or supporting an "active record" style of access. *Please take the time to learn how to do things "the regular way" before using these shortcuts in a production application*. Understanding what these add-ons do behind the scenes will help if you have to troubleshoot a database error or work around a limitation in the add-on later. `SQLSoup `_, an extension to SQLAlchemy, provides a quick way to generate ORM classes based on existing database tables. If you're familiar with ActiveRecord, used in Ruby on Rails, then you may want to use the `Elixir `_ layer on top of SQLAlchemy. This approach is less common since the introduction of the declarative extension, but has other features the declarative does not. .. _`SQLAlchemy manual`: http://www.sqlalchemy.org/docs/ ================================================ FILE: pylons/docs/en/modules/commands.rst ================================================ :mod:`pylons.commands` -- Command line functions ================================================ .. automodule:: pylons.commands Module Contents --------------- .. autoclass:: ControllerCommand .. autoclass:: RestControllerCommand .. autoclass:: ShellCommand ================================================ FILE: pylons/docs/en/modules/configuration.rst ================================================ :mod:`pylons.configuration` -- Configuration object and defaults setup ====================================================================== .. automodule:: pylons.configuration Module Contents --------------- .. autoclass:: PylonsConfig :members: init_app ================================================ FILE: pylons/docs/en/modules/controllers.rst ================================================ :mod:`pylons.controllers` -- Controllers ======================================== .. module:: pylons.controllers This module makes available the :class:`~pylons.controllers.core.WSGIController` and :class:`~pylons.controllers.xmlrpc.XMLRPCController` for easier importing. ================================================ FILE: pylons/docs/en/modules/controllers_core.rst ================================================ :mod:`pylons.controllers.core` -- WSGIController Class ====================================================== .. automodule:: pylons.controllers.core Module Contents --------------- .. autoclass:: WSGIController :members: _perform_call, _inspect_call, _get_method_args, _dispatch_call, __call__ ================================================ FILE: pylons/docs/en/modules/controllers_util.rst ================================================ :mod:`pylons.controllers.util` -- Controller Utility functions ====================================================================== .. automodule:: pylons.controllers.util Module Contents --------------- .. autoclass:: Request :members: :undoc-members: :show-inheritance: .. autoclass:: Response :members: :undoc-members: :show-inheritance: .. autofunction:: abort .. autofunction:: etag_cache .. autofunction:: forward .. autofunction:: redirect ================================================ FILE: pylons/docs/en/modules/controllers_xmlrpc.rst ================================================ :mod:`pylons.controllers.xmlrpc` -- XMLRPCController Class ========================================================== .. automodule:: pylons.controllers.xmlrpc Module Contents --------------- .. autoclass:: XMLRPCController :members: __call__, system_listMethods, system_methodSignature, system_methodHelp ================================================ FILE: pylons/docs/en/modules/decorators.rst ================================================ :mod:`pylons.decorators` -- Decorators ====================================== .. automodule:: pylons.decorators Module Contents --------------- .. autofunction:: jsonify .. autofunction:: validate ================================================ FILE: pylons/docs/en/modules/decorators_cache.rst ================================================ :mod:`pylons.decorators.cache` -- Cache Decorators ====================================================================== .. automodule:: pylons.decorators.cache Module Contents --------------- .. autofunction:: beaker_cache ================================================ FILE: pylons/docs/en/modules/decorators_rest.rst ================================================ :mod:`pylons.decorators.rest` -- REST-ful Decorators ==================================================== .. automodule:: pylons.decorators.rest Module Contents --------------- .. autofunction:: dispatch_on .. autofunction:: restrict ================================================ FILE: pylons/docs/en/modules/decorators_secure.rst ================================================ :mod:`pylons.decorators.secure` -- Secure Decorators ==================================================== .. automodule:: pylons.decorators.secure Module Contents --------------- .. autofunction:: authenticate_form .. autofunction:: https ================================================ FILE: pylons/docs/en/modules/error.rst ================================================ :mod:`pylons.error` -- Error handling support ============================================= .. automodule:: pylons.error ================================================ FILE: pylons/docs/en/modules/i18n_translation.rst ================================================ :mod:`pylons.i18n.translation` -- Translation/Localization functions ==================================================================== .. automodule:: pylons.i18n.translation Module Contents --------------- .. autoexception:: LanguageError .. autoclass:: LazyString .. autofunction:: lazify .. autofunction:: gettext_noop .. autofunction:: gettext .. autofunction:: ugettext .. autofunction:: ngettext .. autofunction:: ungettext .. autofunction:: set_lang .. autofunction:: get_lang .. autofunction:: add_fallback ================================================ FILE: pylons/docs/en/modules/index.rst ================================================ .. _modules: ============== Pylons Modules ============== .. toctree:: :maxdepth: 2 commands configuration controllers controllers_core controllers_util controllers_xmlrpc decorators decorators_cache decorators_rest decorators_secure error i18n_translation log middleware templating test util wsgiapp ================================================ FILE: pylons/docs/en/modules/log.rst ================================================ :mod:`pylons.log` -- Logging for WSGI errors ============================================ .. automodule:: pylons.log Module Contents --------------- .. autoclass:: WSGIErrorsHandler :members: ================================================ FILE: pylons/docs/en/modules/middleware.rst ================================================ :mod:`pylons.middleware` -- WSGI Middleware =========================================== .. automodule:: pylons.middleware Module Contents --------------- .. autoclass:: StatusCodeRedirect :members: __init__ .. autoclass:: StaticJavascripts .. autofunction:: ErrorHandler .. note:: The :data:`errorware` dictionary is constructed from the settings in the `DEFAULT` section of development.ini. the recognised keys and settings at initialization are: * :data:`error_email` = conf.get('email_to') * :data:`error_log` = conf.get('error_log', None) * :data:`smtp_server` = conf.get('smtp_server','localhost') * :data:`error_subject_prefix` = conf.get('error_subject_prefix', 'WebApp Error: ') * :data:`from_address` = conf.get('from_address', conf.get('error_email_from', 'pylons@yourapp.com')) * :data:`error_message` = conf.get('error_message', 'An internal server error occurred') Referenced classes ------------------ Pylons middleware uses :class:`WebError` to effect the error-handling. The two classes implicated are: ErrorMiddleware ^^^^^^^^^^^^^^^ :mod:`weberror.errormiddleware` :class:`weberror.errormiddleware.ErrorMiddleware` EvalException ^^^^^^^^^^^^^ :mod:`weberror.evalexception` :class:`weberror.evalexception.EvalException` ================================================ FILE: pylons/docs/en/modules/templating.rst ================================================ :mod:`pylons.templating` -- Render functions and helpers ======================================================== .. automodule:: pylons.templating Module Contents --------------- .. autofunction:: pylons_globals .. autofunction:: cached_template .. autofunction:: render_mako .. autofunction:: render_mako_def .. autofunction:: render_genshi ================================================ FILE: pylons/docs/en/modules/test.rst ================================================ :mod:`pylons.test` -- Test related functionality ================================================ .. automodule:: pylons.test Module Contents --------------- .. autoclass:: PylonsPlugin ================================================ FILE: pylons/docs/en/modules/util.rst ================================================ :mod:`pylons.util` -- Paste Template and Pylons utility functions ================================================================= .. automodule:: pylons.util Module Contents --------------- .. autoclass:: PylonsContext .. autoclass:: ContextObj .. autoclass:: AttribSafeContextObj ================================================ FILE: pylons/docs/en/modules/wsgiapp.rst ================================================ :mod:`pylons.wsgiapp` -- PylonsWSGI App Creator =============================================== .. automodule:: pylons.wsgiapp Module Contents --------------- .. autoclass:: PylonsApp :members: .. automethod:: PylonsApp.__call__ ================================================ FILE: pylons/docs/en/objects.inv ================================================ ================================================ FILE: pylons/docs/en/python23_install.rst ================================================ .. _python23_installation: ==================================== Python 2.3 Installation Instructions ==================================== Advice of **end of support for Python 2.3** ------------------------------------------- .. warning:: **END OF SUPPORT FOR PYTHON 2.3** This is the **LAST** version to support Python 2.3 **BEGIN UPGRADING OR DIE** Preparation ----------- First, please note that Python 2.3 users on Windows will need to install `subprocess.exe`__ before beginning the installation (whereas Python 2.4 users on Windows do not). All windows users also should read the section :ref:`windows_notes` after installation. Users of Ubuntu/debian will also likely need to install the python-dev package. System-wide Install ------------------- To install Pylons so it can be used by everyone (you'll need root access). If you already have easy install: .. code-block:: bash $ easy_install Pylons==0.9.7 .. note:: On rare occasions, the python.org Cheeseshop goes down. It is still possible to install Pylons and its dependencies however by specifying our local package directory for installation with: .. code-block:: bash $ easy_install -f http://pylonshq.com/download/ Pylons==0.9.7 Which will use the packages necessary for the latest release. If you're using an older version of Pylons, you can get the packages that went with it by specifying the version desired: .. code-block:: bash $ easy_install -f http://pylonshq.com/download/0.9.7/ Pylons==0.9.7 Otherwise: #. Download the easy install setup file from http://peak.telecommunity.com/dist/ez_setup.py #. Run: .. code-block:: bash $ python ez_setup.py Pylons==0.9.7 .. __: http://www.pylonshq.com/download/subprocess-0.1-20041012.win32-py2.3.exe .. warning:: **END OF SUPPORT FOR PYTHON 2.3** This is the **LAST** version to support Python 2.3 **BEGIN UPGRADING OR DIE** ================================================ FILE: pylons/docs/en/security_policy_for_bugs.rst ================================================ .. _security_policy_for_bugs: ======================== Security policy for bugs ======================== Receiving Security Updates ========================== The Pylons team have set up a mailing list at wsgi-security-announce@googlegroups.com to which any security vulnerabilities that affect Pylons will be announced. Anyone wishing to be notified of vulnerabilities in Pylons should subscribe to this list. Security announcements will only be made once a solution to the problem has been discovered. Reporting Security Issues ========================= Please report security issues by email to both the lead developers of Pylons at the following addresses: ben\ |at|\ groovie.org security\ |at|\ 3aims.com Please DO NOT announce the vulnerability to any mailing lists or on the ticket system because we would not want any malicious person to be aware of the problem before a solution is available. In the event of a confirmed vulnerability in Pylons itself, we will take the following actions: * Acknowledge to the reporter that we've received the report and that a fix is forthcoming. We'll give a rough timeline and ask the reporter to keep the issue confidential until we announce it. * Halt all other development as long as is needed to develop a fix, including patches against the current release. * Publicly announce the vulnerability and the fix as soon as it is available to the WSGI security list at wsgi-security-announce@googlegroups.com. This will probably mean a new release of Pylons, but in some cases it may simply be the release of documentation explaining how to avoid the vulnerability. In the event of a confirmed vulnerability in one of the components that Pylons uses, we will take the following actions: * Acknowledge to the reporter that we've received the report and ask the reporter to keep the issue confidential until we announce it. * Contact the developer or maintainer of the package containing the vulnerability. * If the developer or maintainer fails to release a new version in a reasonable time-scale and the vulnerability is serious we will either create documentation explaining how to avoid the problem or as a last resort, create a patched version. * Publicly announce the vulnerability and the fix as soon as it is available to the WSGI security list at wsgi-security-announce@googlegroups.com. Minimising Risk =============== * Only use official production versions of Pylons released publicly on the `Python Package Index `_. * Only use stable releases of third party software not development, alpha, beta or release candidate code. * Do not assume that related software is of the same quality as Pylons itself, even if Pylons users frequently make use of it. * Subscribe to the wsgi-security-announce@googlegroups.com mailing list to be informed of security issues and their solutions. .. |at| image:: _static/at.png ================================================ FILE: pylons/docs/en/sessions.rst ================================================ .. _sessions: ======== Sessions ======== Sessions ======== Pylons includes a session object: a session is a server-side, semi-permanent storage for data associated with a client. The session object is provided by the `Beaker library`_ which also provides caching functionality as described in :ref:`caching`. The Session Object ================== The Pylons session object is available at :data:`pylons.session`. Controller modules created via :command:`paster controller/restcontroller` import the session object by default. The basic session API is simple, it implements a dict-like interface with a few additional methods. The following is an example of using the session to store a token identifying if a client is logged in. .. code-block :: python class LoginController(BaseController): def authenicate(self): name = request.POST['name'] password = request.POST['password'] user = Session.query(User).filter_by(name=name, password=password).first() if user: msg = 'Successfully logged in as %s' % name location = url('index') session['logged_in'] = True session.save() else: msg = 'Invalid username/password' location = url('login') flash(msg) redirect(location) def logout(self): # Clear all values in the session associated with the client session.clear() session.save() Subsequent requests can then determine if a client is logged in or not by checking the session: .. code-block :: python if not session.get('logged_in'): flash('Please login') redirect(url('login')) The session object acts lazily: it does not load the session data (from disk or whichever backend is used) until the data is first accessed. This lazyness is facilitated via an intermediary :class:`beaker.session.SessionObject` that wraps the actual :class:`beaker.session.Session` object. Furthermore the session will not write changes to its backend without an explicit call to its :meth:`beaker.session.Session.save` method (unless configured with the ``auto`` option). Session data is generally serialized for storage via the Python :mod:`pickle` module, so anything stored in the session must be pickleable. The lightweight SessionObject wrapper is created by the: :class:`beaker.middleware.SessionMiddleware` WSGI middleware. SessionMiddleware stores the wrapper in the WSGI environ where Pylons sets a reference to it from pylons.session. Sessions are associated with a client via a client-side cookie. The WSGI middleware is also responsible for sending said cookie to the client. Configuring the Session ======================= The basic session defaults are: * File based sessions (session data is stored on disk) * Session cookies have no expiration date (cookies expire at the end of the browser session) * Session cookie domain/path matches the current host/path Pylons projects by default sets the following couple of session options via their .ini files. All Beaker specific session options in the ini file are prefixed with `beaker.session`: .. code-block :: ini cache_dir = %(here)s/data beaker.session.key = foo beaker.session.secret = somesecret ``cache_dir`` acts a base directory for both session and cache storage. Session data is stored in this location under a :file:`sessions/` sub-directory. ``session.key`` is the name attribute of the cookie sent to the browser. This defaults to your project's name. ``session.secret`` is the secret token used to hash the cookie data sent to the client. This should be a secret, ideally randomly generated value on production environments. :command:`paster make-config` will generate a random secret for you when creating a production ini file. Other Session Options --------------------- Some other commonly used session options are: * ``type`` The type of the back-end for storing session data. Beaker supports many different backends, see `Beaker Configuration Documentation`_ for the choices. Defaults to 'file'. * ``cookie_domain`` The domain name to use for the session Cookie. For example, when using sub-domains, set this to the parent domain name so that the cookie is valid for all sub-domains. To enable pure `Cookie-based Sessions`_ and force the cookie domain to be valid for all sub-domains of 'example.com', add the following to your Pylons ini file: .. code-block :: ini beaker.session.type = cookie beaker.session.cookie_domain = .example.com See the `Beaker Configuration Documentation`_ for an exhaustive list of Session options. Storing SQLAlchemy mapped objects in Beaker sessions ==================================================== Mapped objects from SQLAlchemy can be serialized into the beaker session, but care must be taken when retrieving these objects back from the beaker session. They will not be associated with the SQLAlchemy Unit-of-Work Session, however these objects can be reconciled via the SQLAlchemy Session's ``merge`` method, as follows: .. code-block:: python address = DBSession.query(Address).get(id) session[id] = address ... address = session.get(id) address = DBSession.merge(address) Custom and caching middleware ============================= Care should be taken when deciding in which layer to place custom middleware. In most cases middleware should be placed between the Pylons WSGI application instantiation and the Routes middleware; however, if the middleware should run *before* the session object or routing is handled: .. code-block:: python # Routing/Session Middleware app = RoutesMiddleware(app, config['routes.map']) app = SessionMiddleware(app, config) # MyMiddleware can only see the cache object, nothing *above* here app = MyMiddleware(app) app = CacheMiddleware(app, config) Some of the Pylons middleware layers such as the ``Session``, ``Routes``, and ``Cache`` middleware, only add objects to the `environ` dict, or add HTTP headers to the response (the Session middleware for example adds the session cookie header). Others, such as the ``Status Code Redirect``, and the ``Error Handler`` may fully intercept the request entirely, and change how its responded to. Using `Session` in Internationalization ======================================= How to set the language used in a controller on the fly. For example this could be used to allow a user to set which language they wanted your application to work in. Save the value to the session object: .. code-block:: python session['lang'] = 'en' session.save() then on each controller call the language to be used could be read from the session and set in the controller's ``__before__()`` method so that the pages remained in the same language that was previously set: .. code-block:: python def __before__(self): if 'lang' in session: set_lang(session['lang']) Using `Session` in Secure Forms =============================== Authorization tokens are stored in the client's session. The web app can then verify the request's submitted authorization token with the value in the client's session. This ensures the request came from the originating page. See the wikipedia entry for `Cross-site request forgery`__ for more information. .. __: http://en.wikipedia.org/wiki/Cross-site_request_forgery Pylons provides an ``authenticate_form`` decorator that does this verification on the behalf of controllers. These helpers depend on Pylons' ``session`` object. Most of them can be easily ported to another framework by changing the API calls. Hacking the session for no cookies ================================== (From a `paste #441 `_ baked by Ben Bangert) Set the session to not use cookies in the dev.ini file .. code-block:: ini beaker.session.use_cookies = False with this as the *mode d'emploi* in the controller action .. code-block:: python from beaker.session import Session as BeakerSession # Get the actual session object through the global proxy real_session = session._get_current_obj() # Duplicate the session init options to avoid screwing up other sessions in # other threads params = real_session.__dict__['_params'] # Now set the id param used to make a session to our session maker, # if id is None, a new id will be made automatically params['id'] = find_id_func() real_session.__dict__['_sess'] = BeakerSession({}, **params) # Now we can use the session as usual session['fred'] = 42 session.save() # At the end, we need to see if the session was used and handle its id if session.is_new: # do something with session.id to make sure its around next time pass Using middleware (Beaker) with a composite app ============================================== How to allow called WSGI apps to share a common session management utility. (From a `paste #616 `_ baked by Mark Luffel) .. code-block:: ini # Here's an example of configuring multiple apps to use a common # middleware filter # The [app:home] section is a standard pylons app # The ``/servicebroker`` and ``/proxy`` apps both want to be able # to use the same session management [server:main] use = egg:Paste#http host = 0.0.0.0 port = 5000 [filter-app:main] use = egg:Beaker#beaker_session next = sessioned beaker.session.key = my_project_key beaker.session.secret = i_wear_two_layers_of_socks [composite:sessioned] use = egg:Paste#urlmap / = home /servicebroker = servicebroker /proxy = cross_domain_proxy [app:servicebroker] use = egg:Appcelerator#service_broker [app:cross_domain_proxy] use = egg:Appcelerator#cross_domain_proxy [app:home] use = egg:my_project full_stack = true cache_dir = %(here)s/data .. _`Beaker library`: http://beaker.groovie.org .. _`Beaker Configuration Documentation`: http://beaker.groovie.org/configuration.html#session-options .. _`Cookie-based Sessions`: http://beaker.groovie.org/sessions.html#cookie-based ================================================ FILE: pylons/docs/en/testing.rst ================================================ .. _testing: =========================== Unit and functional testing =========================== Unit Testing with :mod:`webtest` ================================ Pylons provides powerful unit testing capabilities for your web application utilizing `webtest `_ to emulate requests to your web application. You can then ensure that the response was handled appropriately and that the controller set things up properly. To run the test suite for your web application, Pylons utilizes the `nose `_ test runner/discovery package. Running ``nosetests`` in your project directory will run all the tests you create in the tests directory. If you don't have nose installed on your system, it can be installed via setuptools with: .. code-block:: bash $ easy_install -U nose To avoid conflicts with your development setup, the tests use the `test.ini` configuration file when run. This means **you must configure any databases, etc. in your test.ini file or your tests will not be able to find the database configuration**. .. warning:: Nose can trigger errors during its attempt to search for doc tests since it will try and import all your modules one at a time *before* your app was loaded. This will cause files under models/ that rely on your app to be running, to fail. Pylons 0.9.6.1 and later includes a plugin for nose that loads the app before the doctests scan your modules, allowing models to be doctested. You can use this option from the command line with nose: .. code-block:: bash nosetests --with-pylons=test.ini Or by setting up a `[nosetests]` block in your setup.cfg: .. code-block:: ini [nosetests] verbose=True verbosity=2 with-pylons=test.ini detailed-errors=1 with-doctest=True Then just run: .. code-block:: bash python setup.py nosetests to run the tests. Example: Testing a Controller ============================= First let's create a new project and controller for this example: .. code-block:: bash $ paster create -t pylons TestExample $ cd TestExample $ paster controller comments You'll see that it creates two files when you create a controller. The stub controller, and a test for it under ``testexample/tests/functional/``. Modify the ``testexample/controllers/comments.py`` file so it looks like this: .. code-block:: python from testexample.lib.base import * class CommentsController(BaseController): def index(self): return 'Basic output' def sess(self): session['name'] = 'Joe Smith' session.save() return 'Saved a session' Then write a basic set of tests to ensure that the controller actions are functioning properly, modify ``testexample/tests/functional/test_comments.py`` to match the following: .. code-block:: python from testexample.tests import * class TestCommentsController(TestController): def test_index(self): response = self.app.get(url(controller='/comments')) assert 'Basic output' in response def test_sess(self): response = self.app.get(url(controller='/comments', action='sess')) assert response.session['name'] == 'Joe Smith' assert 'Saved a session' in response Run ``nosetests`` in your main project directory and you should see them all pass: .. code-block:: pycon .. ---------------------------------------------------------------------- Ran 2 tests in 2.999s OK Unfortunately, a plain assert does not provide detailed information about the results of an assertion should it fail, unless you specify it a second argument. For example, add the following test to the ``test_sess`` function: .. code-block:: python assert response.session.has_key('address') == True When you run ``nosetests`` you will get the following, not-very-helpful result: .. code-block:: pycon .F ====================================================================== FAIL: test_sess (testexample.tests.functional.test_comments.TestCommentsController) ---------------------------------------------------------------------- Traceback (most recent call last): File "~/TestExample/testexample/tests/functional/test_comments.py", line 12, in test_sess assert response.session.has_key('address') == True AssertionError: ---------------------------------------------------------------------- Ran 2 tests in 1.417s FAILED (failures=1) You can augment this result by doing the following: .. code-block:: python assert response.session.has_key('address') == True, "address not found in session" Which results in: .. code-block:: pycon .F ====================================================================== FAIL: test_sess (testexample.tests.functional.test_comments.TestCommentsController) ---------------------------------------------------------------------- Traceback (most recent call last): File "~/TestExample/testexample/tests/functional/test_comments.py", line 12, in test_sess assert response.session.has_key('address') == True AssertionError: address not found in session ---------------------------------------------------------------------- Ran 2 tests in 1.417s FAILED (failures=1) But detailing every assert statement could be time consuming. Our TestController subclasses the standard Python ``unittest.TestCase`` class, so we can use utilize its helper methods, such as ``assertEqual``, that can automatically provide a more detailed AssertionError. The new test line looks like this: .. code-block:: python self.assertEqual(response.session.has_key('address'), True) Which provides the more useful failure message: .. code-block:: pycon .F ====================================================================== FAIL: test_sess (testexample.tests.functional.test_comments.TestCommentsController) ---------------------------------------------------------------------- Traceback (most recent call last): File "~/TestExample/testexample/tests/functional/test_comments.py", line 12, in test_sess self.assertEqual(response.session.has_key('address'), True) AssertionError: False != True Testing Pylons Objects ====================== Pylons will provide several additional attributes for the :mod:`webtest` :class:`webtest.TestResponse` object that let you access various objects that were created during the web request: ``config`` The configured Pylons applications. ``session`` Session object ``req`` Request object ``tmpl_context`` Object containing variables passed to templates ``app_globals`` Globals object To use them, merely access the attributes of the response *after* you've used a get/post command: .. code-block:: python response = app.get('/some/url') assert response.session['var'] == 4 assert 'REQUEST_METHOD' in response.req.environ .. note:: The :class:`response ` object already has a TestRequest object assigned to it, therefore Pylons assigns its ``request`` object to the response as ``req``. Accessing Special Globals ------------------------- Sometimes, you might wish to modify or check a global Pylons variable such as :term:`app_globals` before running the rest of your unit tests. The non-request specific variables are available from a special URL that will respond only in unit testing situations. For example, to get the :term:`app_globals` object without sending a request to your actual applications:: response = app.get('/_test_vars') app_globals = response.app_globals Testing Your Own Objects ======================== WebTest's fixture testing allows you to designate your own objects that you'd like to access in your tests. This powerful functionality makes it easy to test the value of objects that are normally only retained for the duration of a single request. Before making objects available for testing, its useful to know when your application is being tested. WebTest will provide an environ variable called ``paste.testing`` that you can test for the presence and truth of so that your application only populates the testing objects when it has to. Populating the :mod:`webtest` response object with your objects is done by adding them to the environ dict under the key ``paste.testing_variables``. Pylons creates this dict before calling your application, so testing for its existence and adding new values to it is recommended. All variables assigned to the ``paste.testing_variables`` dict will be available on the response object with the key being the attribute name. .. note:: WebTest is an extracted stand-alone version of a Paste component called paste.fixture. For backwards compatibility, WebTest continues to honor the ``paste.testing_variables`` key in the environ. Example: .. code-block:: python # testexample/lib/base.py from pylons import request from pylons.controllers import WSGIController from pylons.templating import render_mako as render class BaseController(WSGIController): def __call__(self, environ, start_response): # Create a custom email object email = MyCustomEmailObj() email.name = 'Fred Smith' if 'paste.testing_variables' in request.environ: request.environ['paste.testing_variables']['email'] = email return WSGIController.__call__(self, environ, start_response) # testexample/tests/functional/test_controller.py from testexample.tests import * class TestCommentsController(TestController): def test_index(self): response = self.app.get(url(controller='/')) assert response.email.name == 'Fred Smith' .. seealso:: `WebTest Documentation `_ Documentation covering webtest and its usage :mod:`WebTest Module docs ` Module API reference for methods available for use when testing the application .. _unit_testing: Unit Testing ============ XXX: Describe unit testing an applications models, libraries .. _functional_testing: Functional Testing ================== XXX: Describe functional/integrated testing, WebTest ================================================ FILE: pylons/docs/en/thirdparty/formencode_api.rst ================================================ .. _formencode: ========== FormEncode ========== FormEncode is a validation and form generation package. The validation can be used separately from the form generation. The validation works on compound data structures, with all parts being nestable. It is separate from HTTP or any other input mechanism. These module API docs are divided into section by category. Core API ======== :mod:`formencode.api` --------------------- These functions are used mostly internally by FormEncode. .. automodule:: formencode.api .. function:: is_validator(obj) Returns whether ``obj`` is a validator object or not. .. autoclass:: Invalid :members: .. automethod:: __init__ .. autoclass:: Validator :members: .. autoclass:: FancyValidator :members: :mod:`formencode.schema` ------------------------ The FormEncode schema is one of the most important parts of using FormEncode, as it lets you organize validators into parts that can be re-used between schemas. Generally, a single schema will represent an entire form, but may inherit other schemas for re-usable validation parts (ie, maybe multiple forms all requires first and last name). .. module:: formencode.schema .. autoclass:: Schema .. autoclass:: SimpleFormValidator Validators ========== .. automodule:: formencode.validators .. autoclass:: Bool .. autoclass:: CIDR .. autoclass:: CreditCardValidator .. autoclass:: CreditCardExpires .. autoclass:: CreditCardSecurityCode .. autoclass:: DateConverter .. autoclass:: DateValidator .. autoclass:: DictConverter .. autoclass:: Email .. autoclass:: Empty .. autoclass:: FieldsMatch .. autoclass:: FieldStorageUploadConverter .. autoclass:: FileUploadKeeper .. autoclass:: FormValidator .. autoclass:: IndexListConverter .. autoclass:: Int .. autoclass:: IPhoneNumberValidator .. autoclass:: MACAddress .. autoclass:: MaxLength .. autoclass:: MinLength .. autoclass:: Number .. autoclass:: NotEmpty .. autoclass:: OneOf .. autoclass:: PhoneNumber .. autoclass:: PlainText .. autoclass:: PostalCode .. autoclass:: Regex .. autoclass:: RequireIfMissing .. autoclass:: Set .. autoclass:: SignedString .. autoclass:: StateProvince .. autoclass:: String .. autoclass:: StringBool .. autoclass:: StripField .. autoclass:: TimeConverter .. autoclass:: UnicodeString .. autoclass:: URL Wrapper Validators ------------------ .. autoclass:: ConfirmType .. autoclass:: Wrapper .. autoclass:: Constant Validator Modifiers =================== :mod:`formencode.compound` -------------------------- .. automodule:: formencode.compound .. autoclass:: Any .. autoclass:: All :mod:`formencode.foreach` ------------------------- .. automodule:: formencode.foreach .. autoclass:: ForEach HTML Parsing and Form Filling ============================= :mod:`formencode.htmlfill` -------------------------- .. automodule:: formencode.htmlfill .. autofunction:: render .. autofunction:: default_formatter .. autofunction:: none_formatter .. autofunction:: escape_formatter .. autofunction:: escapenl_formatter .. autoclass:: FillingParser ================================================ FILE: pylons/docs/en/thirdparty/index.rst ================================================ .. _third_party_components: ====================== Third-party components ====================== .. toctree:: :maxdepth: 1 formencode_api weberror webtest webob ================================================ FILE: pylons/docs/en/thirdparty/weberror.rst ================================================ :mod:`weberror` -- Weberror =========================== .. automodule:: weberror :mod:`weberror.errormiddleware` ------------------------------- .. currentmodule:: weberror.errormiddleware .. automodule:: weberror.errormiddleware .. autoclass:: ErrorMiddleware :members: :mod:`weberror.evalcontext` --------------------------- .. currentmodule:: weberror.evalcontext .. automodule:: weberror.evalcontext .. autoclass:: weberror.evalcontext.EvalContext :members: :mod:`weberror.evalexception` ----------------------------- .. currentmodule:: weberror.evalexception .. automodule:: weberror.evalexception .. autoclass:: weberror.evalexception.EvalException :members: :mod:`weberror.formatter` ------------------------- .. currentmodule:: weberror.formatter .. automodule:: weberror.formatter .. autoclass:: AbstractFormatter :members: .. autoclass:: TextFormatter :members: .. autoclass:: HTMLFormatter :members: .. autoclass:: XMLFormatter :members: .. autofunction:: create_text_node .. autofunction:: html_quote .. autofunction:: format_html .. autofunction:: format_text .. autofunction:: format_xml .. autofunction:: str2html .. autofunction:: _str2html .. autofunction:: truncate .. autofunction:: make_wrappable .. autofunction:: make_pre_wrappable :mod:`weberror.reporter` ------------------------ .. currentmodule:: weberror.reporter .. automodule:: weberror.reporter .. autoclass:: Reporter :members: .. autoclass:: EmailReporter :members: .. autoclass:: LogReporter :members: .. autoclass:: FileReporter :members: .. autoclass:: WSGIAppReporter :members: :mod:`weberror.collector` ------------------------- .. currentmodule:: weberror.collector .. automodule:: weberror.collector .. autoclass:: ExceptionCollector :members: .. autoclass:: ExceptionFrame :members: .. autofunction:: collect_exception ================================================ FILE: pylons/docs/en/thirdparty/webob.rst ================================================ :mod:`webob` -- Request/Response objects ======================================== .. automodule:: webob Request ------- .. autoclass:: Request .. automodule:: webob.acceptparse .. autoclass:: Accept .. autoclass:: MIMEAccept .. automodule:: webob.byterange .. autoclass:: Range .. automodule:: webob.cachecontrol .. autoclass:: CacheControl .. automodule:: webob.datastruct .. autoclass:: EnvironHeaders .. automodule:: webob.etag .. autoclass:: ETagMatcher .. autoclass:: IfRange Response -------- .. autoclass:: webob.Response .. autoclass:: webob.byterange.ContentRange .. autoclass:: webob.cachecontrol.CacheControl .. automodule:: webob.headerdict .. autoclass:: HeaderDict Misc Functions -------------- .. autofunction:: webob.html_escape .. comment: not sure what to do with these constants; not autoclass .. autoclass:: webob.day .. autoclass:: webob.week .. autoclass:: webob.hour .. autoclass:: webob.minute .. autoclass:: webob.second .. autoclass:: webob.month .. autoclass:: webob.year .. autoclass:: webob.response.AppIterRange .. automodule:: webob.multidict .. autoclass:: MultiDict .. autoclass:: UnicodeMultiDict .. autoclass:: NestedMultiDict .. autoclass:: NoVars .. automodule:: webob.updatedict .. autoclass:: webob.updatedict.UpdateDict Descriptors ----------- .. autoclass:: webob.descriptors.environ_getter .. autoclass:: webob.descriptors.header_getter .. autoclass:: webob.descriptors.converter .. autoclass:: webob.descriptors.deprecated_property ================================================ FILE: pylons/docs/en/thirdparty/webtest.rst ================================================ :mod:`webtest` -- WebTest ========================= .. currentmodule:: webtest .. automodule:: webtest .. autoclass:: TestApp :members: .. autoclass:: TestResponse :members: .. autoclass:: Form :members: ================================================ FILE: pylons/docs/en/tutorials/index.rst ================================================ .. _tutorials: Pylons Tutorials ================ A small collection of relevant tutorials. .. toctree:: :maxdepth: 2 quickwiki_tutorial understanding_unicode ================================================ FILE: pylons/docs/en/tutorials/quickwiki_tutorial.rst ================================================ .. _quickwiki_tutorial: ================== Quickwiki tutorial ================== Introduction ============ If you haven't done so already, please first read the :ref:`getting_started` guide. In this tutorial we are going to create a working wiki from scratch using Pylons 1.0 and `SQLAlchemy`_. Our wiki will allow visitors to add, edit or delete formatted wiki pages. Starting at the End =================== Pylons is designed to be easy for everyone, not just developers, so let's start by downloading and installing the finished QuickWiki in exactly the same way that end users of QuickWiki might do. Once we have explored its features we will set about writing it from scratch. After you have :ref:`installed Pylons `, install the QuickWiki project: .. code-block :: bash $ easy_install QuickWiki==0.1.8 $ paster make-config QuickWiki test.ini Next, ensure that the ``sqlalchemy.url`` variable in the ``[app:main]`` section of the configuration file (``development.ini``) specifies a value that is suitable for your setup. The data source name points to the database you wish to use. .. note :: The default ``sqlite:///%(here)s/quickwiki.db`` uses a (file-based) SQLite database named ``quickwiki.db`` in the ini's top-level directory. This SQLite database will be created for you when running the :command:`paster setup-app` command below, but you could also use MySQL, Oracle or PostgreSQL. Firebird and MS-SQL may also work. See the `SQLAlchemy documentation `_ for more information on how to connect to different databases. SQLite for example requires additional forward slashes in its URI, where the client/server databases should only use two. You will also need to make sure you have the appropriate Python driver for the database you wish to use. If you're using Python 2.5, a version of the `pysqlite adapter `_ is already included, so you can jump right in with the tutorial. You may need to get `SQLite itself `_. Finally create the database tables and serve the finished application: .. code-block :: bash $ paster setup-app test.ini $ paster serve test.ini That's it! Now you can visit http://127.0.0.1:5000 and experiment with the finished Wiki. When you've finished, stop the server with :kbd:`Control-C` so we can start developing our own version. If you are interested in looking at the latest version of the QuickWiki source code it can be browsed online at http://bitbucket.org/bbangert/quickwiki/src/ or can be checked out using `Mercurial `_: .. code-block :: bash $ hg clone http://bitbucket.org/bbangert/quickwiki .. Note:: To run the QuickWiki checked out from the repository, you'll need to first run :command:`python setup.py develop` from the project's root directory. This will install its dependencies and generate `Python Egg `_ metadata in a :file:`QuickWiki.egg-info` directory. The latter is required for the :command:`paster` command (among other things) . .. code-block :: bash $ cd QuickWiki $ python setup.py develop Developing QuickWiki ==================== If you skipped the "Starting at the End" section you will need to assure yourself that you have Pylons installed. See the :ref:`getting_started`. Then create your project: .. code-block :: bash $ paster create -t pylons QuickWiki When prompted for which templating engine to use, simply hit enter for the default (Mako). When prompted for SQLAlchemy configuration, enter ``True``. Now let's start the server and see what we have: .. code-block :: bash $ cd QuickWiki $ paster serve --reload development.ini .. note :: We have started :command:`paster serve` with the :option:`--reload` option. This means any changes that we make to code will cause the server to restart (if necessary); your changes are immediately reflected on the live site. Visit http://127.0.0.1:5000 where you will see the introduction page. Now delete the file :file:`public/index.html` so we can see the front page of the wiki instead of this welcome page. If you now refresh the page, the Pylons built-in error document support will kick in and display an ``Error 404`` page, indicating the file could not be found. We'll setup a controller to handle this location later. The Model ========= Pylons uses a Model-View-Controller architecture; we'll start by creating the model. We could use any system we like for the model, including `SQLAlchemy`_ or `SQLObject `_. Optional SQLAlchemy integration is provided for new Pylons projects, which we enabled when creating the project, and thus we'll be using SQLAlchemy for the QuickWiki. .. note :: `SQLAlchemy`_ is a powerful Python SQL toolkit and Object Relational Mapper (ORM) that is widely used by the Python community. SQLAlchemy provides a full suite of well known enterprise-level persistence patterns, designed for efficient and high-performance database access, adapted into a simple and Pythonic domain language. It has full and detailed documentation available on the SQLAlchemy website: http://sqlalchemy.org/docs/. The most basic way of using SQLAlchemy is with explicit sessions where you create :class:`Session` objects as needed. Pylons applications typically employ a slightly more sophisticated setup, using SQLAlchemy's "contextual" thread-local sessions created via the :meth:`sqlalchemy.orm.scoped_session` function. With this configuration, the application can use a single :class:`Session` instance per web request, avoiding the need to pass it around explicitly. Instantiating a new scoped :class:`Session` will actually find an existing one in the current thread if available. Pylons has setup a :class:`Session` for us in the :file:`model/meta.py` file. For further details, refer to the `SQLAlchemy documentation on the Session `_. .. note :: It is important to recognize the difference between SQLAlchemy's (or possibly another DB abstraction layer's) :class:`Session` object and Pylons' standard :dfn:`session` (with a lowercase 's') for web requests. See :mod:`beaker` for more on the latter. It is customary to reference the database session by :class:`model.Session` or (more recently) :class:`Session` outside of model classes. The :file:`model/__init__.py` file starts out rather bare-bones. It initializes the SQLAlchemy database engine, and imports the Session object. At the top, add the following imports:: from sqlalchemy import orm, Column, Unicode, UnicodeText from quickwiki.model.meta import Session, Base Then add the following to the end of the :file:`model/__init__.py` file: .. code-block :: python class Page(Base): __tablename__ = 'pages' title = Column(Unicode(40), primary_key=True) content = Column(UnicodeText(), default=u'') We've defined a table called ``pages`` which has two columns: ``title`` (the primary key), a Unicode VARCHAR of 40 characters, and ``content`` a Unicode TEXT column of variable sized length. .. note :: A primary key is a unique ID for each row in a database table. In the example above we are using the page title as a natural primary key. Some prefer to integer primary keys for all tables, so-called surrogate primary keys. The author of this tutorial uses both methods in his own code and is not advocating one method over the other, what's important is to choose the best database structure for your application. See the Pylons Cookbook for `a quick general overview of relational databases `_ if you're not familiar with these concepts. A core philosophy of ORMs is that tables and domain classes are different beasts. So next we'll create the Python class that represents the pages of our wiki, and map these domain objects to rows in the ``pages`` table via the :func:`sqlalchemy.orm.mapper` function. In a more complex application, you could break out model classes into separate ``.py`` files in your :file:`model` directory, but for sake of simplicity in this case, we'll just stick to :file:`__init__.py`. Add this to the bottom of ``model/__init__.py``: .. code-block :: python class Page(object): def __init__(self, title, content=None): self.title = title self.content = content def __unicode__(self): return self.title __str__ = __unicode__ orm.mapper(Page, pages_table) A :class:`Page` object represents a row in the ``pages`` table, so ``self.title`` and ``self.content`` will be the values of the ``title`` and ``content`` columns. Looking ahead, our wiki could use a way of marking up the ``content`` field into HTML. Also, any 'WikiWords' (words made by joining together two or more capitalized words) should be converted to hyperlinks to wiki pages. We can use Python's `docutils `_ library to allow marking up ``content`` as `reStructuredText`_. So next we'll add a method to our :class:`Page` class that formats ``content`` as HTML and converts the WikiWords to hyperlinks. Add the following at the top of the :file:`model/__init__.py` file: .. code-block :: python import logging import re import sets from docutils.core import publish_parts from pylons import url from quickwiki.lib.helpers import link_to from quickwiki.model import meta log = logging.getLogger(__name__) # disable docutils security hazards: # http://docutils.sourceforge.net/docs/howto/security.html SAFE_DOCUTILS = dict(file_insertion_enabled=False, raw_enabled=False) wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)", re.UNICODE) then add a :meth:`get_wiki_content` method to the :class:`Page` class: .. code-block :: python class Page(object): def __init__(self, title, content=None): self.title = title self.content = content def get_wiki_content(self): """Convert reStructuredText content to HTML for display, and create links for WikiWords """ content = publish_parts(self.content, writer_name='html', settings_overrides=SAFE_DOCUTILS)['html_body'] titles = sets.Set(wikiwords.findall(content)) for title in titles: title_url = url(controller='pages', action='show', title=title) content = content.replace(title, link_to(title, title_url)) return content def __unicode__(self): return self.title __str__ = __unicode__ The :class:`Set` object provides us with only unique WikiWord names, so we don't try replacing them more than once (a "wikiword" is of course defined by the regular expression set globally). .. note :: Pylons uses a **Model View Controller** architecture and so the formatting of objects into HTML should properly be handled in the View, i.e. in a template. However in this example, converting `reStructuredText`_ into HTML in a template is inappropriate so we are treating the HTML representation of the content as part of the model. It also gives us the chance to demonstrate that SQLAlchemy domain classes are real Python classes that can have their own methods. The :func:`link_to` and :func:`url` functions referenced in the controller code are respectively: a helper imported from the :mod:`webhelpers.html` module indirectly via :file:`lib/helpers.py`, and a utility function imported directly from the :mod:`pylons` module. They are utilities for creating links to specific controller actions. In this case we have decided that all WikiWords should link to the :meth:`show` action of the ``pages`` controller which we'll create later. However, we need to ensure that the :func:`link_to` function is made available as a helper by adding an import statement to :file:`lib/helpers.py`: .. code-block :: python """Helper functions Consists of functions to typically be used within templates, but also available to Controllers. This module is available to templates as 'h'. """ from webhelpers.html.tags import * Since we have used docutils and SQLAlchemy, both third party packages, we need to edit our :file:`setup.py` file so that anyone installing QuickWiki with `Easy Install `_ will automatically have these dependencies installed too. Edit your :file:`setup.py` in your project root directory and add a docutils entry to the ``install_requires`` line (there will already be one for SQLAlchemy): .. code-block :: python install_requires=[ "Pylons>=0.9.7", "SQLAlchemy>=0.5", "docutils==0.4", ], While we are we are making changes to :file:`setup.py` we might want to complete some of the other sections too. Set the version number to 0.1.6 and add a description and URL which will be used on PyPi when we release it: .. code-block :: python version='0.1.6', description='QuickWiki - Pylons 0.9.7 Tutorial application', url='http://docs.pylonshq.com/tutorials/quickwiki_tutorial.html', We might also want to make a full release rather than a development release in which case we would remove the following lines from :file:`setup.cfg`: .. code-block :: ini [egg_info] tag_build = dev tag_svn_revision = true To test the automatic installation of the dependencies, run the following command which will also install docutils and SQLAlchemy if you don't already have them: .. code-block :: bash $ python setup.py develop .. note :: The command :command:`python setup.py develop` installs your application in a special mode so that it behaves exactly as if it had been installed as an egg file by an end user. This is really useful when you are developing an application because it saves you having to create an egg and install it every time you want to test a change. Application Setup ================= Edit :file:`websetup.py`, used by the :command:`paster setup-app` command, to look like this: .. code-block :: python """Setup the QuickWiki application""" import logging from quickwiki import model from quickwiki.config.environment import load_environment from quickwiki.model import meta log = logging.getLogger(__name__) def setup_app(command, conf, vars): """Place any commands to setup quickwiki here""" load_environment(conf.global_conf, conf.local_conf) # Create the tables if they don't already exist log.info("Creating tables...") meta.metadata.create_all(bind=meta.engine) log.info("Successfully set up.") log.info("Adding front page data...") page = model.Page(title=u'FrontPage', content=u'**Welcome** to the QuickWiki front page!') meta.Session.add(page) meta.Session.commit() log.info("Successfully set up.") You can see that :file:`config/environment.py`'s :func:`load_environment` function is called (which calls :file:`model/__init__.py`'s :func:`init_model` function), so our engine is ready for binding and we can import the model. A SQLAlchemy :class:`MetaData` object -- which provides some utility methods for operating on database schema -- usually needs to be connected to an engine, so the line .. code-block :: python meta.metadata.bind = meta.engine does exactly that and then .. code-block :: python model.metadata.create_all(checkfirst=True) uses the connection we've just set up and, creates the table(s) we've defined ... if they don't already exist. After the tables are created, the other lines add some data for the simple front page to our wiki. By default, SQLAlchemy specifies ``autocommit=False`` when creating the :class:`Session`, which means that operations will be wrapped in a transaction and :func:`commit`'ed atomically (unless your DB doesn't support transactions, like MySQL's default MyISAM tables -- but that's beyond the scope of this tutorial). The database SQLAlchemy will use is specified in the ``ini`` file, under the ``[app:main]`` section, as ``sqlalchemy.url``. We'll customize the ``sqlalchemy.url`` value to point to a SQLite database named :file:`quickwiki.db` that will reside in your project's root directory. Edit the :file:`development.ini` file in the root directory of your project: .. note :: If you've decided to use a different database other than SQLite, see the SQLAlchemy note in the `Starting at the End`_ section for information on supported database URIs. .. code-block :: ini [app:main] use = egg:QuickWiki #... # Specify the database for SQLAlchemy to use. # SQLAlchemy database URL sqlalchemy.url = sqlite:///%(here)s/quickwiki.db You can now run the :command:`paster setup-app` command to setup your tables in the same way an end user would, remembering to drop and recreate the database if the version tested earlier has already created the tables: .. code-block :: bash $ paster setup-app development.ini You should see the SQL sent to the database as the default :file:`development.ini` is setup to log SQLAlchemy's SQL statements. At this stage you will need to ensure you have the appropriate Python database drivers for the database you chose, otherwise you might find SQLAlchemy complains it can't get the DBAPI module for the dialect it needs. You should also edit :file:`quickwiki/config/deployment.ini_tmpl` so that when users run :command:`paster make-config` the configuration file that is produced for them will also use :file:`quickwiki.db`. In the ``[app:main]`` section: .. code-block :: ini # Specify the database for SQLAlchemy to use. sqlalchemy.url = sqlite:///%(here)s/quickwiki.db Templates ========= .. note :: Pylons uses the `Mako templating engine `_ by default, although as is the case with most aspects of Pylons, you are free to deviate from the default if you prefer. In our project we will make use of the `Mako inheritance feature `_. Add the main page template in :file:`templates/base.mako`: .. code-block :: html+mako QuickWiki ${h.stylesheet_link('/quick.css')}

${self.header()}

${next.body()}\
We'll setup all our other templates to inherit from this one: they will be automatically inserted into the ``${next.body()}`` line. Thus the whole page will be returned when we call the :func:`render` global from our controller. This lets us easily apply a consistent theme to all our templates. If you are interested in learning some of the features of Mako templates have a look at the comprehensive `Mako Documentation `_. For now we just need to understand that :func:`next.body` is replaced with the child template and that anything within ``${...}`` brackets is executed and replaced with the result. By default, the replacement content is HTML-escaped in order to meet modern standards of basic protection from accidentally making the app vulnerable to XSS exploit. This :file:`base.mako` also makes use of various helper functions attached to the ``h`` object. These are described in the `WebHelpers documentation `_. We need to add some helpers to the ``h`` by importing them in the :file:`lib/helpers.py` module (some are for later use): .. code-block :: python """Helper functions Consists of functions to typically be used within templates, but also available to Controllers. This module is available to templates as 'h'. """ from webhelpers.html import literal from webhelpers.html.tags import * from webhelpers.html.secure_form import secure_form Note that the :file:`helpers` module is available to templates as 'h', this is a good place to import or define directly any convenience functions that you want to make available to all templates. Routing ======= Before we can add the actions we want to be able to route the requests to them correctly. Edit :file:`config/routing.py` and adjust the 'Custom Routes' section to look like this: .. code-block :: python # CUSTOM ROUTES HERE map.connect('home', '/', controller='pages', action='show', title='FrontPage') map.connect('pages', '/pages', controller='pages', action='index') map.connect('show_page', '/pages/show/{title}', controller='pages', action='show') map.connect('edit_page', '/pages/edit/{title}', controller='pages', action='edit') map.connect('save_page', '/pages/save/{title}', controller='pages', action='save', conditions=dict(method='POST')) map.connect('delete_page', '/pages/delete', controller='pages', action='delete') # A bonus example - the specified defaults allow visiting # example.com/FrontPage to view the page titled 'FrontPage': map.connect('/{title}', controller='pages', action='show') return map Note that the default route has been replaced. This tells Pylons to route the root URL ``/`` to the :meth:`show()` method of the :class:`PageController` class in :file:`controllers/pages.py` and specify the ``title`` argument as ``'FrontPage'``. It also says that any URL of the form ``/SomePage`` should be routed to the same method but the ``title`` argument will contain the value of the first part of the URL, in this case ``SomePage``. Any other URLs that can't be matched by these maps are routed to the error controller as usual where they will result in a 404 error page being displayed. One of the main benefits of using the Routes system is that you can also create URLs automatically, simply by specifying the routing arguments. For example if I want the URL for the page ``FrontPage`` I can create it with this code: .. code-block :: python url(title='FrontPage') Although the URL would be fairly simple to create manually, with complicated URLs this approach is much quicker. It also has the significant advantage that if you ever deploy your Pylons application at a URL other than ``/``, all the URLs will be automatically adjusted for the new path without you needing to make any manual modifications. This flexibility is a real advantage. Full information on the powerful things you can do to route requests to controllers and actions can be found in the `Routes manual `_. Controllers =========== Quick Recap: We've setup the model, configured the application, added the routes and setup the base template in :file:`base.mako`, now we need to write the application logic and we do this with controllers. In your project's root directory, add a controller called ``pages`` to your project with this command: .. code-block :: bash $ paster controller pages If you are using Subversion, this will automatically be detected and the new controller and tests will be automatically added to your subversion repository. We are going to need the following actions: ``show(self, title)`` displays a page based on the title ``edit(self, title)`` displays a from for editing the page ``title`` ``save(self, title)`` save the page ``title`` and show it with a saved message ``index(self)`` lists all of the titles of the pages in the database ``delete(self, title)`` deletes a page :meth:`show` --------------- Let's get to work on the new controller in :file:`controllers/pages.py`. First we'll import the :class:`Page` class from our :mod:`model`, and the :class:`Session` class from the :mod:`model.meta` module. We'll also import the ``wikiwords`` regular expression object, which we'll use in the :meth:`show` method. Add this line with the imports at the top of the file: .. code-block :: python from quickwiki.model import Page, wikiwords from quickwiki.model.meta import Session Next we'll add the convenience method :meth:`__before__` to the :class:`PagesController`, which is a special method Pylons always calls before calling the actual action method. We'll have :meth:`__before__` obtain and make available the relevant query object from the database, ready to be queried. Our other action methods will need this query object, so we might as well create it one place. .. code-block :: python class PagesController(BaseController): def __before__(self): self.page_q = Session.query(Page) Now we can query the database using the query expression language provided by SQLAlchemy. Add the following :meth:`show` method to :class:`PagesController`: .. code-block :: python def show(self, title): page = self.page_q.filter_by(title=title).first() if page: c.content = page.get_wiki_content() return render('/pages/show.mako') elif wikiwords.match(title): return render('/pages/new.mako') abort(404) Add a template called :file:`templates/pages/show.mako` that looks like this: .. code-block :: html+mako <%inherit file="/base.mako"/>\ <%def name="header()">${c.title} ${h.literal(c.content)} This template simply displays the page title and content. .. note :: Pylons automatically assigns all the action parameters to the Pylons context object ``c`` so that you don't have to assign them yourself. In this case, the value of ``title`` will be automatically assigned to ``c.title`` so that it can be used in the templates. We assign ``c.content`` manually in the controller. We also need a template for pages that don't already exist. The template needs to display a message and link to the :meth:`edit` action so that they can be created. Add a template called :file:`templates/new.mako` that looks like this: .. code-block :: html+mako <%inherit file="/base.mako"/>\ <%def name="header()">${c.title}

This page doesn't exist yet. Create the page.

At this point we can test our QuickWiki to see how it looks. If you don't already have a server running, start it now with: .. code-block :: bash $ paster serve --reload development.ini We can spruce up the appearance of page a little by adding the stylesheet we linked to in the :file:`templates/base.mako` file earlier. Add the file :file:`public/quick.css` with the following content and refresh the page to reveal a better looking wiki: .. code-block :: css body { background-color: #888; margin: 25px; } div.content { margin: 0; margin-bottom: 10px; background-color: #d3e0ea; border: 5px solid #333; padding: 5px 25px 25px 25px; } h1.main { width: 100%; } p.footer{ width: 100%; padding-top: 8px; border-top: 1px solid #000; } a { text-decoration: none; } a:hover { text-decoration: underline; } When you run the example you will notice that the word ``QuickWiki`` has been turned into a hyperlink by the :func:`get_wiki_content` method we added to our :class:`Page` domain object earlier. You can click the link and will see an example of the new page screen from the :file:`new.mako` template. If you follow the ``Create the page`` link you will see the Pylons automatic error handler kick in to tell you ``Action edit is not implemented``. Well, we better write it next, but before we do, have a play with the :ref:`interactive_debugging`, try clicking on the ``+`` or ``>>`` arrows and you will be able to interactively debug your application. It is a tremendously useful tool. :meth:`edit` ------------ To edit the wiki page we need to get the content from the database without changing it to HTML to display it in a simple form for editing. Add the :meth:`edit` action: .. code-block :: python def edit(self, title): page = self.page_q.filter_by(title=title).first() if page: c.content = page.content return render('/pages/edit.mako') and then create the :file:`templates/edit.mako` file: .. code-block :: html+mako <%inherit file="/base.mako"/>\ <%def name="header()">Editing ${c.title} ${h.secure_form(url('save_page', title=c.title))} ${h.textarea(name='content', rows=7, cols=40, content=c.content)}
${h.submit(value='Save changes', name='commit')} ${h.end_form()} .. note :: You may have noticed that we only set ``c.content`` if the page exists but that it is accessed in :func:`h.text_area` even for pages that don't exist and yet it doesn't raise an :class:`AttributeError`. We are making use of the fact that the ``c`` object returns an empty string ``""`` for any attribute that is accessed which doesn't exist. This can be a very useful feature of the ``c`` object, but can catch you on occasions where you don't expect this behavior. It can be disabled by setting ``config['pylons.strict_c'] = True`` in your project's :file:`config/environment.py`. We are making use of the ``h`` object to create our form and field objects. This saves a bit of manual HTML writing. The form submits to the :meth:`save()` action to save the new or updated content so let's write that next. :meth:`save` -------------- The first thing the :meth:`save` action has to do is to see if the page being saved already exists. If not it creates it with ``page = model.Page(title)``. Next it needs the updated content. In Pylons you can get request parameters from form submissions via ``GET`` and ``POST`` requests from the appropriately named ``request`` object. For form submissions from *only* ``GET`` or ``POST`` requests, use ``request.GET`` or ``request.POST``. Only ``POST`` requests should generate side effects (like changing data), so the save action will only reference ``request.POST`` for the parameters. Then add the :meth:`save` action: .. code-block :: python @authenticate_form def save(self, title): page = self.page_q.filter_by(title=title).first() if not page: page = Page(title) # In a real application, you should validate and sanitize # submitted data throughly! escape is a minimal example here. page.content = escape(request.POST.getone('content')) Session.add(page) Session.commit() flash('Successfully saved %s!' % title) redirect_to('show_page', title=title) .. note :: ``request.POST`` is a MultiDict object: an ordered dictionary that may contain multiple values for each key. The MultiDict will always return one value for any existing key via the normal dict accessors ``request.POST[key]`` and :meth:`request.POST.get`. When multiple values are expected, use the :meth:`request.POST.getall` method to return all values in a list. :meth:`request.POST.getone` ensures one value for key was sent, raising a :class:`KeyError` when there are 0 or more than 1 values. The :func:`@authenticate_form` decorator that appears immediately before the :meth:`save` action checks the value of the hidden form field placed there by the :func:`secure_form` helper that we used in :file:`templates/edit.mako` to create the form. The hidden form field carries an authorization token for prevention of certain `Cross-site request forgery (CSRF) `_ attacks. Upon a successful save, we want to redirect back to the :meth:`show` action and 'flash' a ``Successfully saved`` message at the top of the page. 'Flashing' a status message immediately after an action is a common requirement, and the `WebHelpers` package provides the :class:`webhelpers.pylonslib.Flash` class that makes it easy. To utilize it, we'll create a flash object at the bottom of our :file:`lib/helpers.py` module: .. code-block :: python from webhelpers.pylonslib import Flash as _Flash flash = _Flash() And import it into our :file:`controllers/pages.py`. Our new :meth:`show` method is escaping the content via Python's :func:`cgi.escape` function, so we need to import that too, and also :func:`@authenticate_form`. .. code-block :: python from cgi import escape from pylons.decorators.secure import authenticate_form from quickwiki.lib.helpers import flash And finally utilize the ``flash`` object in our :file:`templates/base.mako` template: .. code-block :: html+mako QuickWiki ${h.stylesheet_link('/quick.css')}

${self.header()}

<% flashes = h.flash.pop_messages() %> % if flashes: % for flash in flashes:
${flash}
% endfor % endif ${next.body()}\
And add the following to the :file:`public/quick.css` file: .. code-block :: css div#flash .message { color: orangered; } The ``%`` syntax is used for control structures in mako -- conditionals and loops. You must 'close' them with an 'end' tag as shown here. At this point we have a fully functioning wiki that lets you create and edit pages and can be installed and deployed by an end user with just a few simple commands. Visit http://127.0.0.1:5000 and have a play. It would be nice to get a title list and to be able to delete pages, so that's what we'll do next! :meth:`index` ------------- Add the :meth:`index` action: .. code-block :: python def index(self): c.titles = [page.title for page in self.page_q.all()] return render('/pages/index.mako') The :meth:`index` action simply gets all the pages from the database. Create the :file:`templates/index.mako` file to display the list: .. code-block:: html+mako <%inherit file="/base.mako"/>\ <%def name="header()">Title List ${h.secure_form(url('delete_page'))}
    % for title in c.titles:
  • ${h.link_to(title, url('show_page', title=title))} - ${h.checkbox('title', title)}
  • % endfor
${h.submit('delete', 'Delete')} ${h.end_form()} This displays a form listing a link to all pages along with a checkbox. When submitted, the selected titles will be sent to a :meth:`delete` action we'll create in the next step. We need to edit :file:`templates/base.mako` to add a link to the title list in the footer, but while we're at it, let's introduce a Mako function to make the footer a little smarter. Edit :file:`base.mako` like this: .. code-block :: html+mako QuickWiki ${h.stylesheet_link('/quick.css')}

${self.header()}

<% flashes = h.flash.pop_messages() %> % if flashes: % for flash in flashes:
${flash}
% endfor % endif ${next.body()}\
## Don't show links that are redundant for particular pages <%def name="footer(action)">\ Return to the ${h.link_to('FrontPage', url('home'))} % if action == "index": <% return %> % endif % if action != 'edit': | ${h.link_to('Edit ' + c.title, url('edit_page', title=c.title))} % endif | ${h.link_to('Title List', url('pages'))} The ``<%def name="footer(action">`` creates a Mako function for display logic. As you can see, the function builds the HTML for the footer, but doesn't display the 'Edit' link when you're on the 'Title List' page or already on an edit page. It also won't show a 'Title List' link when you're already on that page. The ``<% ... %>`` tags shown on the :keyword:`return` statement are the final new piece of Mako syntax: they're used much like the ``${...}`` tags, but for arbitrary Python code that does not directly render HTML. Also, the double hash (``##``) denotes a single-line comment in Mako. So the :func:`footer` function is called in place of our old 'static' footer markup. We pass it a value from ``pylons.routes_dict`` which holds the name of the action for the current request. The trailing `\\` character just tells Mako not to render an extra newline. If you visit http://127.0.0.1:5000/pages you should see the full titles list and you should be able to visit each page. :meth:`delete` ---------------- We need to add a :meth:`delete` action that deletes pages submitted from :file:`templates/index.mako`, then returns us back to the list of titles (excluding those that were deleted): .. code-block :: python @authenticate_form def delete(self): titles = request.POST.getall('title') pages = self.page_q.filter(Page.title.in_(titles)) for page in pages: Session.delete(page) Session.commit() # flash only after a successful commit for title in titles: flash('Deleted %s.' % title) redirect_to('pages') Again we use the :func:`@authenticate_form` decorator along with :func:`secure_form` used in :file:`templates/index.mako`. We're expecting potentially multiple titles, so we use :meth:`request.POST.getall` to return a list of titles. The titles are used to identify and load the :class:`Page` objects, which are then deleted. We use the SQL ``IN`` operator to match multiple titles in one query. We can do this via the more flexible :meth:`filter` method which can accept an :meth:`in_` clause created via the title column's attribute. The :meth:`filter_by` method we used in previous methods is a shortcut for the most typical filtering clauses. For example, the :meth:`show` method's: .. code-block :: python self.page_q.filter_by(title=title) is equivalent to: .. code-block :: python self.page_q.filter(Page.title == title) After deleting the pages, the changes are committed, and only after successfully committing do we flash deletion messages. That way if there was a problem with the commit no flash messages are shown. Finally we redirect back to the index page, which re-renders the list of remaining titles. Visit http://127.0.0.1:5000/index and have a go at deleting some pages. You may need to go back to the FrontPage and create some more if you get carried away! That's it! A working, production-ready wiki in 20 mins. You can visit http://127.0.0.1:5000/ once more to admire your work. Publishing the Finished Product =============================== After all that hard work it would be good to distribute the finished package wouldn't it? Luckily this is really easy in Pylons too. In the project root directory run this command: .. code-block :: bash $ python setup.py bdist_egg This will create an egg file in the :file:`dist` directory which contains everything anyone needs to run your program. They can install it with: .. code-block :: bash $ easy_install QuickWiki-0.1.6-py2.5.egg You should probably make eggs for each version of Python your users might require by running the above commands with both Python 2.4 and 2.5 to create both versions of the eggs. If you want to register your project with PyPi at http://www.python.org/pypi you can run the command below. *Please only do this with your own projects though because QuickWiki has already been registered!* .. code-block :: bash $ python setup.py register .. warning:: The PyPi authentication is very weak and passwords are transmitted in plain text. Don't use any sign in details that you use for important applications as they could be easily intercepted. You will be asked a number of questions and then the information you entered in :file:`setup.py` will be used as a basis for the page that is created. Now visit http://www.python.org/pypi to see the new index with your new package listed. .. note :: A `CheeseShop Tutorial `_ has been written and `full documentation on setup.py `_ is available from the Python website. You can even use `reStructuredText`_ in the ``description`` and ``long_description`` areas of :file:`setup.py` to add formatting to the pages produced on PyPi (PyPi used to be called "the CheeseShop"). There is also `another tutorial here `_. Finally you can sign in to PyPi with the account details you used when you registered your application and upload the eggs you've created. If that seems too difficult you can even use this command which should be run for each version of Python supported to upload the eggs for you: .. code-block :: bash $ python setup.py bdist_egg upload Before this will work you will need to create a :file:`.pypirc` file in your home directory containing your username and password so that the :command:`upload` command knows who to sign in as. It should look similar to this: .. code-block :: ini [server-login] username: james password: password .. note :: This works on windows too but you will need to set your :envvar:`HOME` environment variable first. If your home directory is :file:`C:\Documents and Settings\James` you would put your :file:`.pypirc` file in that directory and set your :envvar:`HOME` environment variable with this command: .. code-block :: bash > SET HOME=C:\Documents and Settings\James You can now use the :command:`python setup.py bdist_egg upload` as normal. Now that the application is on PyPi anyone can install it with the :command:`easy_install` command exactly as we did right at the very start of this tutorial. Security ======== A final word about security. .. warning :: Always set ``debug = false`` in configuration files for production sites and make sure your users do too. You should NEVER run a production site accessible to the public with debug mode on. If there was a problem with your application and an interactive error page was shown, the visitor would be able to run any Python commands they liked in the same way you can when you are debugging. This would obviously allow them to do all sorts of malicious things so it is very important you turn off interactive debugging for production sites by setting ``debug = false`` in configuration files and also that you make users of your software do the same. Summary ======= We've gone through the whole cycle of creating and distributing a Pylons application looking at setup and configuration, routing, models, controllers and templates. Hopefully you have an idea of how powerful Pylons is and, once you get used to the concepts introduced in this tutorial, how easy it is to create sophisticated, distributable applications with Pylons. That's it, I hope you found the tutorial useful. You are encouraged to email any comments to the `Pylons mailing list `_ where they will be welcomed. Thanks ====== A big thanks to Ches Martin for updating this document and the QuickWiki project for Pylons 0.9.6 / Pylons 0.9.7 / QuickWiki 0.1.5 / QuickWiki 0.1.6, Graham Higgins, and others in the Pylons community who contributed bug fixes and suggestions. Todo ==== * Provide :command:`paster shell` examples * Incorporate testing into the tutorial * Explain Ches's :meth:`validate_title` method in the actual QuickWiki project * Provide snapshots of every file modified at each step, to help resolve mistakes .. _`SQLAlchemy`: http://www.sqlalchemy.org .. _`reStructuredText`: http://docutils.sourceforge.net/rst.html ================================================ FILE: pylons/docs/en/tutorials/understanding_unicode.rst ================================================ .. _unicode: ===================== Understanding Unicode ===================== If you've ever come across text in a foreign language that contains lots of ``????`` characters or have written some Python code and received a message such as ``UnicodeDecodeError: 'ascii' codec can't decode byte 0xff in position 6: ordinal not in range(128)`` then you have run into a problem with character sets, encodings, Unicode and the like. The truth is that many developers are put off by Unicode because most of the time it is possible to muddle through rather than take the time to learn the basics. To make the problem worse if you have a system that manages to fudge the issues and just about work and then start trying to do things properly with Unicode it often highlights problems in other parts of your code. The good news is that Python has great Unicode support, so the rest of this article will show you how to correctly use Unicode in Pylons to avoid unwanted ``?`` characters and ``UnicodeDecodeErrors``. What is Unicode? ---------------- When computers were first being used the characters that were most important were unaccented English letters. Each of these letters could be represented by a number between 32 and 127 and thus was born ASCII, a character set where space was 32, the letter "A" was 65 and everything could be stored in 7 bits. Most computers in those days were using 8-bit bytes so people quickly realized that they could use the codes 128-255 for their own purposes. Different people used the codes 128-255 to represent different characters and before long these different sets of characters were also standardized into *code pages*. This meant that if you needed some non-ASCII characters in a document you could also specify a codepage which would define which extra characters were available. For example Israel DOS used a code page called 862, while Greek users used 737. This just about worked for Western languages provided you didn't want to write an Israeli document with Greek characters but it didn't work at all for Asian languages where there are many more characters than can be represented in 8 bits. Unicode is a character set that solves these problems by uniquely defining *every* character that is used anywhere in the world. Rather than defining a character as a particular combination of bits in the way ASCII does, each character is assigned a *code point*. For example the word ``hello`` is made from code points ``U+0048 U+0065 U+006C U+006C U+006F``. The full list of code points can be found at http://www.unicode.org/charts/. There are lots of different ways of encoding Unicode code points into bits but the most popular encoding is UTF-8. Using UTF-8, every code point from 0-127 is stored in a single byte. Only code points 128 and above are stored using 2, 3, in fact, up to 6 bytes. This has the useful side effect that English text looks exactly the same in UTF-8 as it did in ASCII, because for every ASCII character with hexadecimal value 0xXY, the corresponding Unicode code point is U+00XY. This backwards compatibility is why if you are developing an application that is only used by English speakers you can often get away without handling characters properly and still expect things to work most of the time. Of course, if you use a different encoding such as UTF-16 this doesn't apply since none of the code points are encoded to 8 bits. The important things to note from the discussion so far are that: * Unicode can represent pretty much any character in any writing system in widespread use today * Unicode uses code points to represent characters and the way these map to bits in memory depends on the encoding * The most popular encoding is UTF-8 which has several convenient properties 1. It can handle any Unicode code point 2. A Unicode string is turned into a string of bytes containing no embedded zero bytes. This avoids byte-ordering issues, and means UTF-8 strings can be processed by C functions such as strcpy() and sent through protocols that can't handle zero bytes 3. A string of ASCII text is also valid UTF-8 text 4. UTF-8 is fairly compact; the majority of code points are turned into two bytes, and values less than 128 occupy only a single byte. 5. If bytes are corrupted or lost, it's possible to determine the start of the next UTF-8-encoded code point and resynchronize. .. note:: Since Unicode 3.1, some extensions have even been defined so that the defined range is now U+000000 to U+10FFFF (21 bits), and formally, the character set is defined as 31-bits to allow for future expansion. It is a myth that there are 65,536 Unicode code points and that every Unicode letter can really be squeezed into two bytes. It is also incorrect to think that UTF-8 can represent less characters than UTF-16. UTF-8 simply uses a variable number of bytes for a character, sometimes just one byte (8 bits). Unicode in Python ----------------- In Python Unicode strings are expressed as instances of the built-in ``unicode`` type. Under the hood, Python represents Unicode strings as either 16 or 32 bit integers, depending on how the Python interpreter was compiled. The ``unicode()`` constructor has the signature ``unicode(string[, encoding, errors])``. All of its arguments should be 8-bit strings. The first argument is converted to Unicode using the specified encoding; if you leave off the encoding argument, the ASCII encoding is used for the conversion, so characters greater than 127 will be treated as errors: .. code-block:: pycon >>> unicode('hello') u'hello' >>> s = unicode('hello') >>> type(s) >>> unicode('hello' + chr(255)) Traceback (most recent call last): File "", line 1, in ? UnicodeDecodeError: 'ascii' codec can't decode byte 0xff in position 6: ordinal not in range(128) The ``errors`` argument specifies what to do if the string can't be decoded to ascii. Legal values for this argument are ``'strict'`` (raise a ``UnicodeDecodeError`` exception), ``'replace'`` (replace the character that can't be decoded with another one), or ``'ignore'`` (just leave the character out of the Unicode result). .. code-block:: pycon >>> unicode('\x80abc', errors='strict') Traceback (most recent call last): File "", line 1, in ? UnicodeDecodeError: 'ascii' codec can't decode byte 0x80 in position 0: ordinal not in range(128) >>> unicode('\x80abc', errors='replace') u'\ufffdabc' >>> unicode('\x80abc', errors='ignore') u'abc' It is important to understand the difference between *encoding* and *decoding*. Unicode strings are considered to be the Unicode code points but any representation of the Unicode string has to be encoded to something else, for example UTF-8 or ASCII. So when you are converting an ASCII or UTF-8 string to Unicode you are *decoding* it and when you are converting from Unicode to UTF-8 or ASCII you are *encoding* it. This is why the error in the example above says that the ASCII codec cannot decode the byte ``0x80`` from ASCII to Unicode because it is not in the range(128) or 0-127. In fact ``0x80`` is hex for 128 which the first number outside the ASCII range. However if we tell Python that the character ``0x80`` is encoded with the ``'latin-1'``, ``'iso_8859_1'`` or ``'8859'`` character sets (which incidentally are different names for the same thing) we get the result we expected: .. code-block:: pycon >>> unicode('\x80', encoding='latin-1') u'\x80' .. note:: The character encodings Python supports are listed at http://docs.python.org/lib/standard-encodings.html Unicode objects in Python have most of the same methods that normal Python strings provide. Python will try to use the ``'ascii'`` codec to convert strings to Unicode if you do an operation on both types: .. code-block:: pycon >>> a = 'hello' >>> b = unicode(' world!') >>> print a + b u'hello world!' You can encode a Unicode string using a particular encoding like this: .. code-block:: pycon >>> u'Hello World!'.encode('utf-8') 'Hello World!' Unicode Literals in Python Source Code -------------------------------------- In Python source code, Unicode literals are written as strings prefixed with the 'u' or 'U' character: .. code-block:: pycon >>> u'abcdefghijk' >>> U'lmnopqrstuv' You can also use ``"``, ``"""``` or ``'''`` versions too. For example: .. code-block:: pycon >>> u"""This ... is a really long ... Unicode string""" Specific code points can be written using the ``\u`` escape sequence, which is followed by four hex digits giving the code point. If you use ``\U`` instead you specify 8 hex digits instead of 4. Unicode literals can also use the same escape sequences as 8-bit strings, including ``\x``, but ``\x`` only takes two hex digits so it can't express all the available code points. You can add characters to Unicode strings using the ``unichr()`` built-in function and find out what the ordinal is with ``ord()``. Here is an example demonstrating the different alternatives: .. code-block:: pycon >>> s = u"\x66\u0072\u0061\U0000006e" + unichr(231) + u"ais" >>> # ^^^^ two-digit hex escape >>> # ^^^^^^ four-digit Unicode escape >>> # ^^^^^^^^^^ eight-digit Unicode escape >>> for c in s: print ord(c), ... 97 102 114 97 110 231 97 105 115 >>> print s français Using escape sequences for code points greater than 127 is fine in small doses but Python 2.4 and above support writing Unicode literals in any encoding as long as you declare the encoding being used by including a special comment as either the first or second line of the source file: .. code-block:: python #!/usr/bin/env python # -*- coding: latin-1 -*- u = u'abcdé' print ord(u[-1]) If you don't include such a comment, the default encoding used will be ASCII. Versions of Python before 2.4 were Euro-centric and assumed Latin-1 as a default encoding for string literals; in Python 2.4, characters greater than 127 still work but result in a warning. For example, the following program has no encoding declaration: .. code-block:: python #!/usr/bin/env python u = u'abcdé' print ord(u[-1]) When you run it with Python 2.4, it will output the following warning: .. code-block:: pycon sys:1: DeprecationWarning: Non-ASCII character '\xe9' in file testas.py on line 2, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details and then the following output: .. code-block:: pycon 233 For real world use it is recommended that you use the UTF-8 encoding for your file but you must be sure that your text editor actually saves the file as UTF-8 otherwise the Python interpreter will try to parse UTF-8 characters but they will actually be stored as something else. .. note :: Windows users who use the `SciTE `_ editor can specify the encoding of their file from the menu using the ``File->Encoding``. .. note :: If you are working with Unicode in detail you might also be interested in the ``unicodedata`` module which can be used to find out Unicode properties such as a character's name, category, numeric value and the like. Input and Output ---------------- We now know how to use Unicode in Python source code but input and output can also be different using Unicode. Of course, some libraries natively support Unicode and if these libraries return Unicode objects you will not have to do anything special to support them. XML parsers and SQL databases frequently support Unicode for example. If you remember from the discussion earlier, Unicode data consists of code points. In order to send Unicode data via a socket or write it to a file you usually need to encode it to a series of bytes and then decode the data back to Unicode when reading it. You can of course perform the encoding manually reading a byte at the time but since encodings such as UTF-8 can have variable numbers of bytes per character it is usually much easier to use Python's built-in support in the form of the ``codecs`` module. The codecs module includes a version of the ``open()`` function that returns a file-like object that assumes the file's contents are in a specified encoding and accepts Unicode parameters for methods such as ``.read()`` and ``.write()``. The function's parameters are open(filename, mode='rb', encoding=None, errors='strict', buffering=1). ``mode`` can be 'r', 'w', or 'a', just like the corresponding parameter to the regular built-in ``open()`` function. You can add a ``+`` character to update the file. ``buffering`` is similar to the standard function's parameter. ``encoding`` is a string giving the encoding to use, if not specified or specified as ``None``, a regular Python file object that accepts 8-bit strings is returned. Otherwise, a wrapper object is returned, and data written to or read from the wrapper object will be converted as needed. ``errors`` specifies the action for encoding errors and can be one of the usual values of ``'strict'``, ``'ignore'``, or ``'replace'`` which we saw right at the begining of this document when we were encoding strings in Python source files. Here is an example of how to read Unicode from a UTF-8 encoded file: .. code-block:: python import codecs f = codecs.open('unicode.txt', encoding='utf-8') for line in f: print repr(line) It's also possible to open files in update mode, allowing both reading and writing: .. code-block:: python f = codecs.open('unicode.txt', encoding='utf-8', mode='w+') f.write(u"\x66\u0072\u0061\U0000006e" + unichr(231) + u"ais") f.seek(0) print repr(f.readline()[:1]) f.close() Notice that we used the ``repr()`` function to display the Unicode data. This is very useful because if you tried to print the Unicode data directly, Python would need to encode it before it could be sent the console and depending on which characters were present and the character set used by the console, an error might be raised. This is avoided if you use ``repr()``. The Unicode character ``U+FEFF`` is used as a byte-order mark or BOM, and is often written as the first character of a file in order to assist with auto-detection of the file's byte ordering. Some encodings, such as UTF-16, expect a BOM to be present at the start of a file, but with others such as UTF-8 it isn't necessary. When such an encoding is used, the BOM will be automatically written as the first character and will be silently dropped when the file is read. There are variants of these encodings, such as 'utf-16-le' and 'utf-16-be' for little-endian and big-endian encodings, that specify one particular byte ordering and don't skip the BOM. .. note :: Some editors including SciTE will put a byte order mark (BOM) in the text file when saved as UTF-8, which is strange because UTF-8 doesn't need BOMs. Unicode Filenames ----------------- Most modern operating systems support the use of Unicode filenames. The filenames are transparently converted to the underlying filesystem encoding. The type of encoding depends on the operating system. On Windows 9x, the encoding is ``mbcs``. On Mac OS X, the encoding is ``utf-8``. On Unix, the encoding is the user's preference according to the result of nl_langinfo(CODESET), or None if the nl_langinfo(CODESET) failed. On Windows NT+, file names are Unicode natively, so no conversion is performed. getfilesystemencoding still returns ``mbcs``, as this is the encoding that applications should use when they explicitly want to convert Unicode strings to byte strings that are equivalent when used as file names. ``mbcs`` is a special encoding for Windows that effectively means "use whichever encoding is appropriate". In Python 2.3 and above you can find out the system encoding with ``sys.getfilesystemencoding()``. Most file and directory functions and methods support Unicode. For example: .. code-block:: python filename = u"\x66\u0072\u0061\U0000006e" + unichr(231) + u"ais" f = open(filename, 'w') f.write('Some data\n') f.close() Other functions such as ``os.listdir()`` will return Unicode if you pass a Unicode argument and will try to return strings if you pass an ordinary 8 bit string. For example running this example as ``test.py``: .. code-block:: python filename = u"Sample " + unichar(5000) f = open(filename, 'w') f.close() import os print os.listdir('.') print os.listdir(u'.') will produce the following output: .. code-block:: python ['Sample?', 'test.py'] [u'Sample\u1388', u'test.py'] Applying this to Web Programming ================================ So far we've seen how to use encoding in source files and seen how to decode text to Unicode and encode it back to text. We've also seen that Unicode objects can be manipulated in similar ways to strings and we've seen how to perform input and output operations on files. Next we are going to look at how best to use Unicode in a web app. The main rule is this: **Your application should use Unicode for all strings internally, decoding any input to Unicode as soon as it enters the application and encoding the Unicode to UTF-8 or another encoding only on output.** If you fail to do this you will find that ``UnicodeDecodeError`` s will start popping up in unexpected places when Unicode strings are used with normal 8-bit strings because Python's default encoding is ASCII and it will try to decode the text to ASCII and fail. It is always better to do any encoding or decoding at the edges of your application otherwise you will end up patching lots of different parts of your application unnecessarily as and when errors pop up. Unless you have a very good reason not to it is wise to use UTF-8 as the default encoding since it is so widely supported. The second rule is: **Always test your application with characters above 127 and above 255 wherever possible.** If you fail to do this you might think your application is working fine, but as soon as your users do put in non-ASCII characters you will have problems. Using arabic is always a good test and www.google.ae is a good source of sample text. The third rule is: **Always do any checking of a string for illegal characters once it's in the form that will be used or stored, otherwise the illegal characters might be disguised.** For example, let's say you have a content management system that takes a Unicode filename, and you want to disallow paths with a '/' character. You might write this code: .. code-block:: python def read_file(filename, encoding): if '/' in filename: raise ValueError("'/' not allowed in filenames") unicode_name = filename.decode(encoding) f = open(unicode_name, 'r') # ... return contents of file ... This is INCORRECT. If an attacker could specify the 'base64' encoding, they could pass ``L2V0Yy9wYXNzd2Q=`` which is the base-64 encoded form of the string ``'/etc/passwd'`` which is a file you clearly don't want an attacker to get hold of. The above code looks for ``/`` characters in the encoded form and misses the dangerous character in the resulting decoded form. Those are the three basic rules so now we will look at some of the places you might want to perform Unicode decoding in a Pylons application. Request Parameters ------------------ Pylons automatically coerces incoming form parameters (``request.POST``, ``GET`` (quote GET) and ``params``) into unicode objects (as of Pylons 0.9.6). The request object contains a ``charset`` (encoding) attribute defining what the parameters should be decoded to (via value.decode(charset, errors)), and the decoding ``errors`` handler. The unicode conversion of parameters can be disabled when ``charset`` is set to None. .. code-block:: python def index(self): #request.charset = 'utf-8' # utf-8 is the default charset #request.errors = 'replace' # replace is the default error handler # a MultiDict-like object of string names and unicode values decoded_get = request.GET # The raw data is always still available when charset is None request.charset = None raw_get = request.GET raw_params = request.params Pylons can also be configured to not coerece parameters to unicode objects by default. This is done by setting the following in the Pylons config object (at the bottom of your project's ``config/environment.py``): .. code-block:: python # Don't coerce parameters to unicode config['pylons.request_options']['charset'] = None # You can also change the default error handler #config['pylons.request_options']['errors'] = 'strict' When the ``request`` object is instructed to always automatically decode to unicode via the ``request_settings`` dictionary, the dictionary's ``charset`` value acts as a fallback charset. If a ``charset`` was sent by the browser (via the ``Content-Type`` header), the browser's value will take precedent: this takes place when the ``request`` object is constructed. ``FieldStorage`` (file upload) objects will be handled specially for unicode parameters: what's provided is a copy of the original ``FieldStorage`` object with a unicode version of its ``filename`` attribute. See :ref:`file_uploads` for more information on working with file uploads/``FieldStorage`` objects. .. note:: Only parameter values (not their associated names) are decoded to unicode by default. Since parameter names commonly map directly to Python variable names (which are restricted to the ASCII character set), it's usually preferable to handle them as strings. For example, passing form parameters to a function as keyword arguments (e.g. \*\*request.params.mixed()) doesn't work with unicode keys. To make ``WSGIRequest`` decode parameter names anyway, enable the ``decode_param_names`` option on either the WSGIRequest object or the ``request_settings`` dictionary. ``FieldStorage's`` ``name`` attributes are also decoded to unicode when this option is enabled. Templating ---------- Pylons uses Mako as its default templating language. Mako handles all content as unicode internally. It only deals in raw strings upon the final rendering of the template (the Mako ``render()`` function, used by the Pylons ``render()`` function/Buffet plugin). The encoding of the rendered string can be configured; Pylons sets the default value to UTF-8. To change this value, edit your project's ``config/environment.py`` file and add the following option: .. code-block:: python # Customize templating options via this variable tmpl_options = config['buffet.template_options'] tmpl_options['mako.output_encoding'] = 'utf-8' replacing ``utf-8`` with the encoding you wish to use. More information can be found at `Mako's Unicode Chapter `_. Output Encoding --------------- Web pages should be generated with a specific encoding, most likely UTF-8. At the very least, that means you should specify the following in the ```` section: .. code-block:: html The charset should also be specified in the ``Content-Type`` header (which Pylons automatically does for you): .. code-block:: python response.headers['Content-type'] = 'text/html; charset=utf-8' Pylons has a notion of ``response_options``, complimenting the ``request_options`` mentioned in the `Request Parameters`_ section above. The default request charset can be changed by setting the following in the Pylons config object (at the bottom of your project's ``config/environment.py``): .. code-block:: python config['pylons.response_options']['charset'] = 'utf-8' replacing ``utf-8`` with the charset you wish to use. If you specify that your output is UTF-8, generally the web browser will give you UTF-8. If you want the browser to submit data using a different character set, you can set the encoding by adding the ``accept-encoding`` tag to your form. Here is an example: .. code-block:: html
However, be forewarned that if the user tries to give you non-ASCII text, then: * Firefox will translate the non-ASCII text into HTML entities. * IE will ignore your suggested encoding and give you UTF-8 anyway. The lesson to be learned is that if you output UTF-8, you had better be prepared to accept UTF-8 by decoding the data in ``request.params`` as described in the section above entitled `Request Parameters`_'. Another technique which is sometimes used to determine the character set is to use an algorithm to analyse the input and guess the encoding based on probabilities. For instance, if you get a file, and you don't know what encoding it is encoded in, you can often rename the file with a .txt extension and then try to open it in Firefox. Then you can use the "View->Character Encoding" menu to try to auto-detect the encoding. Databases --------- Your database driver should automatically convert from Unicode objects to a particular charset when writing and back again when reading. Again it is normal to use UTF-8 which is well supported. You should check your database's documentation for information on how it handles Unicode. For example MySQL's Unicode documentation is here http://dev.mysql.com/doc/refman/5.0/en/charset-unicode.html Also note that you need to consider both the encoding of the database and the encoding used by the database driver. If you're using MySQL together with SQLAlchemy, see the following, as there are some bugs in MySQLdb that you'll need to work around: http://www.mail-archive.com/sqlalchemy@googlegroups.com/msg00366.html Summary ======= Hopefully you now understand the history of Unicode, how to use it in Python and where to apply Unicode encoding and decoding in a Pylons application. You should also be able to use Unicode in your web app remembering the basic rule to use UTF-8 to talk to the world, do the encode and decode at the edge of your application. Further Reading =============== This information is based partly on the following articles which can be consulted for further information.: http://www.joelonsoftware.com/articles/Unicode.html http://www.amk.ca/python/howto/unicode Please feel free to report any mistakes to the Pylons mailing list or to the author. Any corrections or clarifications would be gratefully received. ================================================ FILE: pylons/docs/en/upgrading.rst ================================================ .. _upgrading: ========= Upgrading ========= 1.0 -> 1.0.1 ============ No changes are necessary, however to take advantage of MarkupSafe's faster HTML escaping, the default filter in ``environment.py`` that Mako is configured with should be changed from:: from webhelpers.html import escape To:: from markupsafe import escape MarkupSafe utilizes a C extension where available for faster escaping which can help on larger pages with substantial variable substitutions. 0.9.7 -> 1.0 ============ Upgrading your project is slightly different depending on which versions you're upgrading from and to. It's recommended that upgrades be done in minor revision steps, as deprecation warnings are added between revisions to help in the upgrade process. For any project prior to 0.9.7, you should first follow the applicable docs to upgrade to 0.9.7 before proceeding. To upgrade to 1.0, first upgrade your project to 0.10. This is a Pylons release that is fully backwards-compatible with 0.9.7. However under 0.10 a variety of warnings will be issued about the various things that need to be changed before upgrading to 1.0. .. tip:: Since Pylons 0.10 is only out as a beta at this point, upgrade using the actual URL, for example: .. code-block:: bash $ easy_install -U http://pylonshq.com/download/0.10/Pylons-0.10.tar.gz Beyond the warnings issued, you should also read the following list and ensure these changes have been applied. Pylons changes from 0.9.7 to 1.0: * The config object created in ``environment.py`` is now passed around explicitly. There are also some other minor updates as follows. Update config/environment.py to initialize and return the config:: # Add to the imports: from pylons.configuration import PylonsConfig # Add under 'def load_environment': config = PylonsConfig() # Replace the make_map / app globals line with config['routes.map'] = make_map(config) config['pylons.app_globals'] = app_globals.Globals(config) # Optionally, if removing the CacheMiddleware and using the # cache in the new 1.0 style, add under the previous lines: import pylons pylons.cache._push_object(config['pylons.app_globals'].cache) # Add at the end of the load_environment function: return config Update config/middleware.py to use the returned :term:`config`:: # modify the load_environment call: config = load_environment(global_conf, app_conf) # update the middleware calls # The Pylons WSGI app app = PylonsApp(config=config) # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map']) app = SessionMiddleware(app, config) # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) # Add right before 'return app': app.config = config .. note:: The CacheMiddleware is no longer setup by default through middleware, its now setup under :term:`app_globals` inside its instantiation in :file:`lib/app_globals.py`. Update config/routing.py to accept the :term:`config`:: # Replace the def line with def make_map(config): Update lib/app_globals.py to accept the :term:`config`:: # Replace the __init__ line with def __init__(self, config): # Optionally, if you decided to remove the CacheMiddleware # Add these imports from beaker.cache import CacheManager from beaker.util import parse_cache_config_options # and add this line in __init__: self.cache = CacheManager(**parse_cache_config_options(config)) Update tests/__init__.py as needed:: from unittest import TestCase from paste.deploy import loadapp from paste.script.appinstall import SetupCommand from pylons import url from routes.util import URLGenerator from webtest import TestApp import pylons.test __all__ = ['environ', 'url', 'TestController'] # Invoke websetup with the current config file SetupCommand('setup-app').run([pylons.test.pylonsapp.config['__file__']]) environ = {} class TestController(TestCase): def __init__(self, *args, **kwargs): wsgiapp = pylons.test.pylonsapp config = wsgiapp.config self.app = TestApp(wsgiapp) url._push_object(URLGenerator(config['routes.map'], environ)) TestCase.__init__(self, *args, **kwargs) .. note:: Change the use of ``url_for`` in your tests to use :class:`url `, which is imported from :file:`tests/__init__.py` in your unit tests. Finally, update websetup.py to avoid the duplicate app creation that previously could occur during the unit tests:: # Add to the imports import pylons.test # Add under the 'def setup_app': # Don't reload the app if it was loaded under the testing environment if not pylons.test.pylonsapp: load_environment(conf.global_conf, conf.local_conf) * Change all instances of ``redirect_to(...)`` -> ``redirect(url(...))`` ``redirect_to`` processed arguments in a slightly 'magical' manner in that some of them went to the ``url_for`` while sometimes... not. :func:`~pylons.controllers.util.redirect` issues a redirect and nothing more, so to generate a url, the :class:`url ` instance should be used (import: ``from pylons import url``). * Ensure that all use of ``g`` is switched to using the new name, :term:`app_globals` * Change all instances of ``url_for`` to :class:`url `. Note that ``url`` does not retain the current route memory like ``url_for`` did by default. To get a route generated using the current route, call :meth:`url.current `. For example:: # Rather than url_for() for the current route url.current() :class:`url ` can be imported from ``pylons``. * Change ``config`` import statement if needed Previously, the config object could be imported as if it was a module:: import pylons.config The config object is now an object in :file:`pylons/__init__.py` so the import needs to be changed to:: from pylons import config * Routes is now explicit by default This won't affect those already using :class:`url ` as it ignores route memory. This change does mean that some routes which relied on a default controller of 'content' and a default action of 'index' will not work. To restore the old behavior, in :file:`config/routing.py`, set the mapper to explicit:: map.explicit = True * By default, the :term:`tmpl_context` (a.k.a 'c'), is no longer a :class:`~pylons.util.AttribSafeContextObj`. This means accessing attributes that don't exist will raise an :exc:`AttributeError`. To use the attribute-safe :term:`tmpl_context`, add this line to the :file:`config/environment.py`:: config['pylons.strict_tmpl_context'] = False ================================================ FILE: pylons/docs/en/views.rst ================================================ .. _views: ===== Views ===== .. image:: _static/pylon4.jpg :alt: :align: left :height: 434px :width: 368px In the MVC paradigm the *view* manages the presentation of the model. The view is the interface the user sees and interacts with. For Web applications, this has historically been an HTML interface. HTML remains the dominant interface for Web apps but new view options are rapidly appearing. These include Macromedia Flash, JSON and views expressed in alternate markup languages like XHTML, XML/XSL, WML, and Web services. It is becoming increasingly common for web apps to provide specialised views in the form of a REST API that allows programmatic read/write access to the data model. More complex APIs are quite readily implemented via SOAP services, yet another type of view on to the data model. The growing adoption of RDF, the graph-based representation scheme that underpins the Semantic Web, brings a perspective that is strongly weighted towards machine-readability. .. NOTE: As much as I love RDF I think the following paragraph is too verbose for our intro docs, maybe we can put this elsewhere -pjenvey .. RDF model data is serialized into an undecorated, standardized format that can readily be processed and rendered by client applications of increasing sophistication, such as the MIT `Simile`__ project's "`Fresnel`__", "`Longwell`__" and "`Welkin`__" browser extensions. .. .. __: http://simile.mit.edu/ .. .. __: http://simile.mit.edu/fresnel/ .. .. __: http://simile.mit.edu/longwell/ .. .. __: http://simile.mit.edu/welkin/ Handling all of these interfaces in an application is becoming increasingly challenging. One big advantage of MVC is that it makes it easier to create these interfaces and develop a web app that supports many different views and thereby provides a broad range of services. Typically, no significant processing occurs in the view; it serves only as a means of outputting data and allowing the user (or the application) to act on that data, irrespective of whether it is an online store or an employee list. .. _templates: ********* Templates ********* Template rendering engines are a popular choice for handling the task of view presentation. To return a processed template, it must be rendered and returned by the controller:: from helloworld.lib.base import BaseController, render class HelloController(BaseController): def sample(self): return render('/sample.mako') Using the default Mako template engine, this will cause Mako to look in the :file:`helloworld/templates` directory (assuming the project is called 'helloworld') for a template filed called :file:`sample.mako`. The :func:`render` function used here is actually an alias defined in your projects' :file:`base.py` for Pylons' :func:`~pylons.templating.render_mako` function. Directly-supported template engines =================================== Pylons provides pre-configured options for using the `Mako`__, `Genshi`__ and `Jinja2`__ template rendering engines. They are setup automatically during the creation of a new Pylons project, or can be added later manually. .. __: http://www.makotemplates.org/ .. __: http://genshi.edgewall.org/ .. __: http://jinja.pocoo.org/ ****************************** Passing Variables to Templates ****************************** To pass objects to templates, the standard Pylons method is to attach them to the :term:`tmpl_context` (aliased as `c` in controllers and templates, by default) object in the :ref:`controllers`:: import logging from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect from helloworld.lib.base import BaseController, render log = logging.getLogger(__name__) class HelloController(BaseController): def index(self): c.name = "Fred Smith" return render('/sample.mako') Using the variable in the template: .. code-block:: html+mako Hi there ${c.name}! Strict vs Attribute-Safe tmpl_context objects ============================================= The :term:`tmpl_context` object is created at the beginning of every request, and by default is an instance of the :class:`~pylons.util.AttribSafeContextObj` class, which is an Attribute-Safe object. This means that accessing attributes on it that do **not** exist will return an empty string **instead** of raising an :exc:`AttributeError` error. This can be convenient for use in templates since it can act as a default: .. code-block:: html+mako Hi there ${c.name} That will work when `c.name` has not been set, and is a bit shorter than what would be needed with the strict :class:`~pylons.util.ContextObj` context object. Switching to the strict version of the :term:`tmpl_context` object can be done in the :file:`config/environment.py` by adding (after the config.init_app):: config['pylons.strict_c'] = True .. _template-globals: ************************** Default Template Variables ************************** By default, all templates have a set of variables present in them to make it easier to get to common objects. The full list of available names present in the templates global scope: - :term:`c` -- Template context object (Alias for :term:`tmpl_context`) - :term:`tmpl_context` -- Template context object - :data:`config` -- Pylons :class:`~pylons.configuration.PylonsConfig` object (acts as a dict) - :term:`g` -- Project application globals object (Alias for :term:`app_globals`) - :term:`app_globals` -- Project application globals object - :term:`h` -- Project helpers module reference - :data:`request` -- Pylons :class:`~pylons.controllers.util.Request` object for this request - :data:`response` -- Pylons :class:`~pylons.controllers.util.Response` object for this request - :class:`session` -- Pylons session object (unless Sessions are removed) - :class:`translator` -- Gettext translator object configured for current locale - :func:`ungettext` -- Unicode capable version of gettext's ngettext function (handles plural translations) - :func:`_` -- Unicode capable gettext translate function - :func:`N_` -- gettext no-op function to mark a string for translation, but doesn't actually translate - :class:`url ` -- An instance of the :class:`routes.util.URLGenerator` configured for this request. **************************** Configuring Template Engines **************************** A new Pylons project comes with the template engine setup inside the projects' :file:`config/environment.py` file. This section creates the Mako template lookup object and attaches it to the :term:`app_globals` object, for use by the template rendering function. .. code-block:: python # these imports are at the top from mako.lookup import TemplateLookup from pylons.error import handle_mako_error # this section is inside the load_environment function # Create the Mako TemplateLookup, with the default auto-escaping config['pylons.app_globals'].mako_lookup = TemplateLookup( directories=paths['templates'], error_handler=handle_mako_error, module_directory=os.path.join(app_conf['cache_dir'], 'templates'), input_encoding='utf-8', default_filters=['escape'], imports=['from webhelpers.html import escape']) Using Multiple Template Engines =============================== Since template engines are configured in the :file:`config/environment.py` section, then used by render functions, it's trivial to setup additional template engines, or even differently configured versions of a single template engine. However, custom render functions will frequently be needed to utilize the additional template engine objects. Example of additional Mako template loader for a different templates directory for admins, which falls back to the normal templates directory:: # Add the additional path for the admin template paths = dict(root=root, controllers=os.path.join(root, 'controllers'), static_files=os.path.join(root, 'public'), templates=[os.path.join(root, 'templates')], admintemplates=[os.path.join(root, 'admintemplates'), os.path.join(root, 'templates')]) config['pylons.app_globals'].mako_admin_lookup = TemplateLookup( directories=paths['admin_templates'], error_handler=handle_mako_error, module_directory=os.path.join(app_conf['cache_dir'], 'admintemplates'), input_encoding='utf-8', default_filters=['escape'], imports=['from webhelpers.html import escape']) That adds the additional template lookup instance, next a :ref:`custom render function ` is needed that utilizes it:: from pylons.templating import cached_template, pylons_globals def render_mako_admin(template_name, extra_vars=None, cache_key=None, cache_type=None, cache_expire=None): # Create a render callable for the cache function def render_template(): # Pull in extra vars if needed globs = extra_vars or {} # Second, get the globals globs.update(pylons_globals()) # Grab a template reference template = globs['app_globals'].mako_admin_lookup.get_template(template_name) return template.render(**globs) return cached_template(template_name, render_template, cache_key=cache_key, cache_type=cache_type, cache_expire=cache_expire) The only change from the :func:`~pylons.templating.render_mako` function that comes with Pylons is to use the `mako_admin_lookup` rather than the `mako_lookup` that is used by default. .. _custom-render: ******************************* Custom :func:`render` functions ******************************* Writing custom render functions can be used to access specific features in a template engine, such as Genshi, that go beyond the default :func:`~pylons.templating.render_genshi` functionality or to add support for additional template engines. Two helper functions for use with the render function are provided to make it easier to include the common Pylons globals that are useful in a template in addition to enabling easy use of cache capabilities. The :func:`pylons_globals` and :func:`cached_template` functions can be used if desired. Generally, the custom render function should reside in the project's ``lib/`` directory, probably in :file:`base.py`. Here's a sample Genshi render function as it would look in a project's ``lib/base.py`` that doesn't fully render the result to a string, and rather than use :data:`c` assumes that a dict is passed in to be used in the templates global namespace. It also returns a Genshi stream instead the rendered string. .. code-block:: python from pylons.templating import pylons_globals def render(template_name, tmpl_vars): # First, get the globals globs = pylons_globals() # Update the passed in vars with the globals tmpl_vars.update(globs) # Grab a template reference template = globs['app_globals'].genshi_loader.load(template_name) # Render the template return template.generate(**tmpl_vars) Using the :func:`~pylons.templating.pylons_globals` function also makes it easy to get to the :term:`app_globals` object which is where the template engine was attached in :file:`config/environment.py`. .. versionchanged:: 0.9.7 Prior to 0.9.7, all templating was handled through a layer called 'Buffet'. This layer frequently made customization of the template engine difficult as any customization required additional plugin modules being installed. Pylons 0.9.7 now deprecates use of the Buffet plug-in layer. .. seealso:: :mod:`pylons.templating` - Pylons templating API ******************** Templating with Mako ******************** Introduction ============ The template library deals with the *view*, presenting the model. It generates (X)HTML code, CSS and Javascript that is sent to the browser. *(In the examples for this section, the project root is ``myapp``.)* Static vs. dynamic ------------------ Templates to generate dynamic web content are stored in `myapp/templates`, static files are stored in `myapp/public`. Both are served from the server root, **if there is a name conflict the static files will be served in preference** Making a template hierarchy =========================== Create a base template ---------------------- In `myapp/templates` create a file named `base.mako` and edit it to appear as follows: .. code-block:: html+mako ${self.head_tags()} ${self.body()} A base template such as the very basic one above can be used for all pages rendered by Mako. This is useful for giving a consistent look to the application. * Expressions wrapped in `${...}` are evaluated by Mako and returned as text * `${` and `}` may span several lines but the closing brace should not be on a line by itself (or Mako throws an error) * Functions that are part of the `self` namespace are defined in the Mako templates Create child templates ---------------------- Create another file in `myapp/templates` called `my_action.mako` and edit it to appear as follows: .. code-block:: html+mako <%inherit file="/base.mako" /> <%def name="head_tags()">

My Controller

Lorem ipsum dolor ...

This file define the functions called by `base.mako`. * The `inherit` tag specifies a parent file to pass program flow to * Mako defines functions with `<%def name="function_name()">...`, the contents of the tag are returned * Anything left after the Mako tags are parsed out is automatically put into the `body()` function A consistent feel to an application can be more readily achieved if all application pages refer back to single file (in this case `base.mako`).. Check that it works ------------------- In the controller action, use the following as a `return()` value, .. code-block:: python return render('/my_action.mako') Now run the action, usually by visiting something like ``http://localhost:5000/my_controller/my_action`` in a browser. Selecting 'View Source' in the browser should reveal the following output: .. code-block:: html

My Controller

Lorem ipsum dolor ...

.. seealso:: The `Mako documentation `_ Reasonably straightforward to follow See the :ref:`i18n` Provides more help on making your application more worldly. ================================================ FILE: pylons/docs/en/windowsnotes.rst ================================================ .. _windows_notes: ============= Windows Notes ============= Python scripts installed as part of the Pylons install process will be put in the ``Scripts`` directory of your Python installation, typically in ``C:\Python24\Scripts``. By default on Windows, this directory is not in your ``PATH``; this can cause the following error message when running a command such as ``paster`` from the command prompt: .. code-block:: text C:\Documents and Settings\James>paster 'paster' is not recognized as an internal or external command, operable program or batch file. To run the scripts installed with Pylons either the full path must be specified: .. code-block:: text C:\Documents and Settings\James>C:\Python24\Scripts\paster Usage: C:\Python24\Scripts\paster-script.py COMMAND usage: paster-script.py [paster_options] COMMAND [command_options] options: --version show program's version number and exit --plugin=PLUGINS Add a plugin to the list of commands (plugins are Egg specs; will also require() the Egg) -h, --help Show this help message ... etc ... or (the preferable solution) the ``Scripts`` directory must be added to the ``PATH`` as described below. For Win2K or WinXP ------------------ #. From the desktop or Start Menu, right click My Computer and click Properties. #. In the System Properties window, click on the Advanced tab. #. In the Advanced section, click the Environment Variables button. #. Finally, in the Environment Variables window, highlight the path variable in the Systems Variable section and click edit. Add or modify the path lines with the paths you wish the computer to access. Each different directory is separated with a semicolon as shown below: .. code-block:: text C:\Program Files;C:\WINDOWS;C:\WINDOWS\System32 #. Add the path to your scripts directory: .. code-block:: text C:\Program Files;C:\WINDOWS;C:\WINDOWS\System32;C:\Python24\Scripts See `Finally`_ below. For Windows 95, 98 and ME ------------------------- Edit ``autoexec.bat``, and add the following line to the end of the file: .. code-block:: bash set PATH=%PATH%;C:\Python24\Scripts See `Finally`_ below. Finally ------- Restarting your computer may be required to enable the change to the ``PATH``. Then commands may be entered from any location: .. code-block:: text C:\Documents and Settings\James>paster Usage: C:\Python24\Scripts\paster-script.py COMMAND usage: paster-script.py [paster_options] COMMAND [command_options] options: --version show program's version number and exit --plugin=PLUGINS Add a plugin to the list of commands (plugins are Egg specs; will also require() the Egg) -h, --help Show this help message ... etc ... All documentation assumes the ``PATH`` is setup correctly as described above. ================================================ FILE: pylons/docs/en/wsgi_support.rst ================================================ .. _wsgi_support: ============ WSGI support ============ The Web Server Gateway Interface `defined in PEP 333 `_ is a standard interface between web servers and Python web applications or frameworks, to promote web application portability across a variety of web servers. Pylons supports the Web Server Gateway Interface (or WSGI for short, pronounced "wizgy") throughout its stack. This is important for developers because it means that as well coming with all the features you would expect of a modern web framework, Pylons is also extremely flexible. With the WSGI it is possible to change any part of the Pylons stack to add new functionality or modify a request or a response without having to take apart the whole framework. Paste and WSGI -------------- Most of Pylons' WSGI capability comes from its close integration with Paste. Paste provides all the tools and middleware necessary to deploy WSGI applications. It can be thought of as a low-level WSGI framework designed for other web frameworks to build upon. Pylons is an example of a framework which makes full use of the possibilities of Paste. If you want to, you can get the WSGI application object from your Pylons configuration file like this: .. code-block:: python from paste.deploy import loadapp wsgi_app = loadapp('config:/path/to/config.ini') You can then serve the file using a WSGI server. Here is an example using the WSGI Reference Implementation included with Python 2.5: .. code-block:: python from paste.deploy import loadapp wsgi_app = loadapp('config:/path/to/config.ini') from wsgiref import simple_server httpd = simple_server.WSGIServer(('',8000), simple_server.WSGIRequestHandler) httpd.set_app(wsgi_app) httpd.serve_forever() The ``paster serve`` command you will be used to using during the development of Pylons projects combines these two steps of creating a WSGI app from the config file and serving the resulting file to give the illusion that it is serving the config file directly. Because the resulting Pylons application is a WSGI application it means you can do the same things with it that you can do with any WSGI application. For example add a middleware chain to it or serve it via FastCGI/SCGI/CGI/mod_python/AJP or standalone. You can also configure extra WSGI middleware, applications and more directly using the configuration file. The various options are described in the `Paste Deploy Documentation `_ so we won't repeat them here. Using a WSGI Application as a Pylons 0.9 Controller --------------------------------------------------- In Pylons 0.9 controllers are derived from ``pylons.controllers.WSGIController`` and are also valid WSGI applications. Unless your controller is derived from the legacy ``pylons.controllers.Controller`` class it is also assumed to be a WSGI application. This means that you don't actually need to use a Pylons controller class in your controller, any WSGI application will work as long as you give it the same name. For example, if you added a ``hello`` controller by executing ``paster controller hello``, you could modify it to look like this: .. code-block:: python def HelloController(environ, start_response): start_response('200 OK', [('Content-Type','text/html')]) return ['Hello World!'] or use ``yield`` statements like this: .. code-block:: python def HelloController(environ, start_response): start_response('200 OK', [('Content-Type','text/html')]) yield 'Hello ' yield 'World!' or use the standard Pylons ``Response`` object which is a valid WSGI response which takes care of calling ``start_response()`` for you: .. code-block:: python def HelloController(environ, start_response): return Response('Hello World!') and you could use the ``render()`` and ``render_response()`` objects exactly like you would in a normal controller action. As well as writing your WSGI application as a function you could write it as a class: .. code-block:: python class HelloController: def __call__(self, environ, start_response): start_response('200 OK', [('Content-Type','text/html')]) return ['Hello World!'] All the standard Pylons middleware defined in ``config/middleware.py`` is still available. Running a WSGI Application From Within a Controller --------------------------------------------------- There may be occasions where you don't want to replace your entire controller with a WSGI application but simply want to run a WSGI application from with a controller action. If your project was called ``test`` and you had a WSGI application called ``wsgi_app`` you could even do this: .. code-block:: python from test.lib.base import * def wsgi_app(environ, start_response): start_response('200 OK',[('Content-type','text/html')]) return ['\n\nHello World!\n\n'] class HelloController(BaseController): def index(self): return wsgi_app(request.environ, self.start_response) Configuring Middleware Within a Pylons Application -------------------------------------------------- A Pylons application middleware stack is directly exposed in the project's ``config/middleware.py`` file. This means that you can add and remove pieces from the stack as you choose. .. Warning:: If you remove any of the default middleware you are likely to find that various parts of Pylons stop working! As an example, if you wanted to add middleware that added a new key to the environ dictionary you might do this: .. code-block:: python # YOUR MIDDLEWARE # Put your own middleware here, so that any problems are caught by the error # handling middleware underneath class KeyAdder: def __init__(self, app, key, value): self.app = app if '.' not in key: raise Exception("WSGI environ keys must contain a '.' character") self.key = key self.value = value def __call__(self, environ, start_response): environ[self.key] = self.value return self.app(environ, start_response) app = KeyAdder(app, 'test.hello', 'Hello World') Then in your controller you could write: .. code-block:: python return Response(request.environ['test.hello']) and you would see your ``Hello World!`` message. Of course, this isn't a particularly useful thing to do. Middleware classes can do one of four things or a combination of them: * Change the environ dictionary * Change the status * Change the HTTP headers * Change the response body of the application With the ability to do these things as a middleware you can create authentication code, error handling middleware and more but the great thing about WSGI is that someone probably already has so you can consult the `wsgi.org middleware list `_ or have a look at the `Paste project `_ and reuse an exisiting piece of middleware. The Cascade ----------- Towards the end of the middleware stack in your project's ``config/middleware.py`` file you will find a special piece of middleware called the cascade: .. code-block:: python app = Cascade([static_app, javascripts_app, app]) Passed a list of applications, ``Cascade`` will try each of them in turn. If one returns a 404 status code then the next application is tried until one of the applications returns a code other than ``404`` in which case its response is returned. If all applications fail, then the last application's failure response is used. The three WSGI applications in the cascade serve files from your project's ``public`` directory first then if nothing matches, the WebHelpers module JavaScripts are searched and finally if no JavaScripts are found your Pylons app is tried. This is why the ``public/index.html`` file is served before your controller is executed and why you can put ``/javascripts/`` into your HTML and the files will be found. You are free to change the order of the cascade or add extra WSGI applications to it before ``app`` so that other locations are checked before your Pylons application is executed. Useful Resources ---------------- Whilst other frameworks have put WSGI adapters at the end of their stacks so that their applications can be served by WSGI servers, we hope you can see how fully Pylons embraces WSGI throughout its design to be the most flexible and extensible of the main Python web frameworks. To find out more about the Web Server Gateway Interface you might find the following resources useful: * `PEP 333 `_ * `The WSGI website at wsgi.org `_ * XML.com articles: Introducing WSGI - Pythons Secret Web Weapon.html `Part 1 `_ `Part 2 `_ ================================================ FILE: pylons/docs/uploader.py ================================================ import cPickle import mimetypes import os import sys import socket from os import path import httplib2 import simplejson socket.setdefaulttimeout(60) HERE_DIR = os.getcwd() BUILD_DIR = path.join(HERE_DIR, '_build') #host = 'http://localhost:25050' host = 'http://pylonshq.com' post_uri = '%s/docs/upload' % host image_uri = '%s/docs/upload_image' % host delete_uri = '%s/docs/delete_revision' % host def scan_dir(parent, directory): files = [] for name in os.listdir(directory): full_name = path.join(directory, name) if name in ['_sources', '_static']: continue if path.isdir(full_name): new_parent = parent + '/' + name files.extend(scan_dir(new_parent, full_name)) else: if name.endswith('.fpickle') or name.endswith('.pickle'): fp = open(full_name, 'r') data = cPickle.load(fp) fp.close() files.append( ('/'.join([parent, name]), data) ) elif name == 'objects.inv': fp = open(full_name, 'r') data = {} data['current_page_name'] = 'objects.inv' data['content'] = fp.read() fp.close() files.append( ('/'.join([parent, name]), data) ) return files def scan_images(directory): files = [] for name in os.listdir(directory): if name[-4:] in ['.png', '.jpg', 'gif']: files.append(name) return files files = scan_dir('', path.join(BUILD_DIR, 'web')) images = scan_images(path.join(BUILD_DIR, 'web', '_images')) http = httplib2.Http(timeout=60) basedata = {} # Find the metadata about versions and such for filename, filedoc in files: if 'globalcontext.pickle' in filename: basedata['version'] = filedoc['version'] basedata['project'] = filedoc['project'] basedata['shorttitle'] = filedoc['shorttitle'] if len(sys.argv) < 2: raise Exception('Failed to specify doc-key') dockey = sys.argv[1] headers = {} headers.setdefault('Accept', 'application/json') headers.setdefault('User-Agent', 'Doc Uploader') headers.setdefault('Content-Type', 'application/json') headers.setdefault('Authkey', dockey) language = os.path.split(HERE_DIR)[-1] # Delete this revision, just in case # del_uri = '%s/%s/%s' % (delete_uri, basedata['project'], basedata['version']) # resp, data = http.request(del_uri, 'GET', headers=headers) for filename, filedoc in files: if not isinstance(filedoc, dict): continue filedoc['filename'] = filename filedoc['language'] = language filedoc.update(basedata) content = simplejson.dumps(filedoc, ensure_ascii=False).encode('utf-8') headers['Content-Length'] = str(len(content)) resp, data = http.request(post_uri, 'POST', body=content, headers=headers) status_code = int(resp.status) if status_code == 200: print "Uploaded %s" % filename else: print "FAILED: %s" % filename for filename in images: headers['Content-Type'] = mimetypes.guess_type(filename) fp = open(path.join(BUILD_DIR, 'web', '_images', filename), 'r') file_content = fp.read() fp.close() headers['Content-Length'] = str(len(file_content)) resp, data = http.request(image_uri + '?version=%s&project=%s&name=%s' % (basedata['version'], basedata['project'], filename), 'POST', body=file_content, headers=headers) status_code = int(resp.status) if status_code == 200: print "Uploaded %s" % filename else: print "FAILED: %s" % filename ================================================ FILE: pylons/error.py ================================================ """Custom EvalException support Provides template engine HTML error formatters for the Template tab of EvalException. """ import sys try: import mako.exceptions except ImportError: mako = None __all__ = ['handle_mako_error'] def handle_mako_error(context, exc): try: exc.is_mako_exception = True except: pass raise exc, None, sys.exc_info()[2] def myghty_html_data(exc_value): """Format a Myghty exception as HTML""" if hasattr(exc_value, 'htmlformat'): return exc_value.htmlformat()[333:-14] if hasattr(exc_value, 'mtrace'): return exc_value.mtrace.htmlformat()[333:-14] template_error_formatters = [myghty_html_data] if mako: def mako_html_data(exc_value): """Format a Mako exception as HTML""" if getattr(exc_value, 'is_mako_exception', False) or \ isinstance(exc_value, (mako.exceptions.CompileException, mako.exceptions.SyntaxException)): return mako.exceptions.html_error_template().render(full=False, css=False) template_error_formatters.insert(0, mako_html_data) ================================================ FILE: pylons/i18n/__init__.py ================================================ """Internationalization and Localization functionality""" from pylons.i18n.translation import * ================================================ FILE: pylons/i18n/translation.py ================================================ """Translation/Localization functions. Provides :mod:`gettext` translation functions via an app's ``pylons.translator`` and get/set_lang for changing the language translated to. """ import os from gettext import NullTranslations, translation import pylons __all__ = ['_', 'add_fallback', 'get_lang', 'gettext', 'gettext_noop', 'lazy_gettext', 'lazy_ngettext', 'lazy_ugettext', 'lazy_ungettext', 'ngettext', 'set_lang', 'ugettext', 'ungettext', 'LanguageError', 'N_'] class LanguageError(Exception): """Exception raised when a problem occurs with changing languages""" pass class LazyString(object): """Has a number of lazily evaluated functions replicating a string. Just override the eval() method to produce the actual value. This method copied from TurboGears. """ def __init__(self, func, *args, **kwargs): self.func = func self.args = args self.kwargs = kwargs def eval(self): return self.func(*self.args, **self.kwargs) def __unicode__(self): return unicode(self.eval()) def __str__(self): return str(self.eval()) def __mod__(self, other): return self.eval() % other def format(self, *args): return self.eval().format(*args) def lazify(func): """Decorator to return a lazy-evaluated version of the original""" def newfunc(*args, **kwargs): return LazyString(func, *args, **kwargs) newfunc.__name__ = 'lazy_%s' % func.__name__ newfunc.__doc__ = 'Lazy-evaluated version of the %s function\n\n%s' % \ (func.__name__, func.__doc__) return newfunc def gettext_noop(value): """Mark a string for translation without translating it. Returns value. Used for global strings, e.g.:: foo = N_('Hello') class Bar: def __init__(self): self.local_foo = _(foo) h.set_lang('fr') assert Bar().local_foo == 'Bonjour' h.set_lang('es') assert Bar().local_foo == 'Hola' assert foo == 'Hello' """ return value N_ = gettext_noop def gettext(value): """Mark a string for translation. Returns the localized string of value. Mark a string to be localized as follows:: gettext('This should be in lots of languages') """ return pylons.translator.gettext(value) lazy_gettext = lazify(gettext) def ugettext(value): """Mark a string for translation. Returns the localized unicode string of value. Mark a string to be localized as follows:: _('This should be in lots of languages') """ return pylons.translator.ugettext(value) _ = ugettext lazy_ugettext = lazify(ugettext) def ngettext(singular, plural, n): """Mark a string for translation. Returns the localized string of the pluralized value. This does a plural-forms lookup of a message id. ``singular`` is used as the message id for purposes of lookup in the catalog, while ``n`` is used to determine which plural form to use. The returned message is a string. Mark a string to be localized as follows:: ngettext('There is %(num)d file here', 'There are %(num)d files here', n) % {'num': n} """ return pylons.translator.ngettext(singular, plural, n) lazy_ngettext = lazify(ngettext) def ungettext(singular, plural, n): """Mark a string for translation. Returns the localized unicode string of the pluralized value. This does a plural-forms lookup of a message id. ``singular`` is used as the message id for purposes of lookup in the catalog, while ``n`` is used to determine which plural form to use. The returned message is a Unicode string. Mark a string to be localized as follows:: ungettext('There is %(num)d file here', 'There are %(num)d files here', n) % {'num': n} """ return pylons.translator.ungettext(singular, plural, n) lazy_ungettext = lazify(ungettext) def _get_translator(lang, **kwargs): """Utility method to get a valid translator object from a language name""" if not lang: return NullTranslations() if 'pylons_config' in kwargs: conf = kwargs.pop('pylons_config') else: conf = pylons.config.current_conf() localedir = os.path.join(conf['pylons.paths']['root'], 'i18n') if not isinstance(lang, list): lang = [lang] try: translator = translation(conf['pylons.package'], localedir, languages=lang, **kwargs) except IOError, ioe: raise LanguageError('IOError: %s' % ioe) translator.pylons_lang = lang return translator def set_lang(lang, set_environ=True, **kwargs): """Set the current language used for translations. ``lang`` should be a string or a list of strings. If a list of strings, the first language is set as the main and the subsequent languages are added as fallbacks. """ translator = _get_translator(lang, **kwargs) if not set_environ: return translator environ = pylons.request.environ environ['pylons.pylons'].translator = translator if 'paste.registry' in environ: environ['paste.registry'].replace(pylons.translator, translator) def get_lang(): """Return the current i18n language used""" return getattr(pylons.translator, 'pylons_lang', None) def add_fallback(lang, **kwargs): """Add a fallback language from which words not matched in other languages will be translated to. This fallback will be associated with the currently selected language -- that is, resetting the language via set_lang() resets the current fallbacks. This function can be called multiple times to add multiple fallbacks. """ return pylons.translator.add_fallback(_get_translator(lang, **kwargs)) ================================================ FILE: pylons/log.py ================================================ """Logging related functionality This logging Handler logs to ``environ['wsgi.errors']`` as designated in :pep:`333`. """ import logging import types import pylons __all__ = ['WSGIErrorsHandler'] class WSGIErrorsHandler(logging.Handler): """A handler class that writes logging records to `environ['wsgi.errors']`. This code is derived from CherryPy's :class:`cherrypy._cplogging.WSGIErrorHandler`. ``cache`` Whether the `wsgi.errors` stream is cached (instead of looked up via `pylons.request.environ` per every logged message). Enabling this option is not recommended (particularly for the use case of logging to `wsgi.errors` outside of a request) as the behavior of a cached `wsgi.errors` stream is not strictly defined. In particular, `mod_wsgi `_'s `wsgi.errors` will raise an exception when used outside of a request. """ def __init__(self, cache=False, *args, **kwargs): logging.Handler.__init__(self, *args, **kwargs) self.cache = cache self.cached_stream = None def get_wsgierrors(self): """Return the wsgi.errors stream Raises a TypeError when outside of a web request (pylons.request is not setup) """ if not self.cache: return pylons.request.environ.get('wsgi.errors') elif not self.cached_stream: self.cached_stream = pylons.request.environ.get('wsgi.errors') return self.cached_stream return self.cached_stream def flush(self): """Flushes the stream""" try: stream = self.get_wsgierrors() except TypeError: pass else: if stream: stream.flush() def emit(self, record): """Emit a record""" try: stream = self.get_wsgierrors() except TypeError: pass else: if not stream: return try: msg = self.format(record) fs = "%s\n" if not hasattr(types, "UnicodeType"): # if no unicode support stream.write(fs % msg) else: try: stream.write(fs % msg) except UnicodeError: stream.write(fs % msg.encode("UTF-8")) self.flush() except (KeyboardInterrupt, SystemExit): raise except: self.handleError(record) ================================================ FILE: pylons/media/javascripts/traceback.js ================================================ $(document).ready(function() { var getProxyJSON = function(host, path, data, callback) { data['host'] = host; data['path'] = path; var uri = TRACEBACK.uri+'/relay'; $.getJSON(uri, data, callback); }; // Manipulate DOM to add the iframe, and move things around var newholder = $(document.createElement("div")).addClass('widget_layout'); $('div.feature-highlight').wrapAll(newholder[0])[0].appendChild( $('button:last').remove()[0]); $('div.widget_layout')[0].appendChild(document.getElementById('service_widget')); // Hide the tabs we shouldn't see $('.searchtab').hide(); // Bind the click functions for each tab $('a.overview').click(function () { $('#supportnav li').removeClass('active'); $(this).parent().addClass('active'); $('.overviewtab').show(); $('.posttracebacktab, .searchtab').hide(); return false; }); $('a.search').click(function () { $('#supportnav li').removeClass('active'); $(this).parent().addClass('active'); $('.searchtab').show(); $('.posttracebacktab, .overviewtab').hide(); return false; }); // Set the search field to the exception as the default search $('.searchtab input[@type="text"]')[0].value = $('code.main-exception').text(); // Bind the search button to hit the mail lists $('.searchtab input[@type="submit"]').click(function () { // Populate the lists array with the strings for the lists to include // in the search var lists = []; var options = $('.searchtab input[@type="checkbox"]').serializeArray(); $.each(options, function(i, val) { lists.push('list:' + val.value); }); // Setup the search data var data = {mode:'json', page:1}; data.q = lists.join(' ') + " " + $('.searchtab input[@type="text"]')[0].value; var sr = $('.searchresults'); sr.html('Loading...'); var searchResults = getProxyJSON('markmail.org', '/results.xqy', data, function(data, textStatus) { stuff = data; var numresults = $(document.createElement('p')); numresults.addClass('results'); numresults.html(data.search.start + ' to ' + data.search.end + ' of ' + data.search.estimation); var searchlink = document.createElement('a'); searchlink.href = 'http://markmail.org' + data.search.permalink; searchlink.target = '_blank'; $(searchlink).html('View all results'); numresults.prepend(searchlink); sr.html('').append(numresults); // If there's no search results, stop here if (!data.search.results) { return false; } // Iterate through the search results adding them dynamically // to the element $.each(data.search.results.result, function(i, val) { var result = $(document.createElement('div')).addClass('result'); var link = document.createElement('a'); link.href = 'http://markmail.org' + val.url; link.target = '_blank'; $(link).html(val.subject); result.append(link); var blurb = $(document.createElement('div')).addClass('blurb'); blurb.html(val.blurb); result.append(blurb); var meta = $(document.createElement('div')).addClass('meta'); meta.html(val.date + ' - ' + val.from + ' - ' + val.list); result.append(meta); sr.append(result); }); // Scroll the window down to the top of the service widget with some space window.scrollTo(0, $('#service_widget').offset().top-20); }); return false; }); }); ================================================ FILE: pylons/media/style/black.css ================================================ body { font-family: arial, helvetica, sans-serif; background-color: #ffc900; background-image: url(../img/bg.png); background-repeat: repeat-x; width:100%; height:100%; margin:0; max-height: 100%; padding:0; border:none; line-height:1.4; } #container { color:white; background-color:#111; position: absolute; left: 50%; width: 500px; margin-left: -300px; padding:50px; height:100%; } #footer { margin: 120px 0 0 0; padding: 3px; text-align:center; font-size:small; background-color:#222; letter-spacing: 1px; } h1 { text-align:center; font-size:xx-large; font-weight:normal; margin: 0 0 20px 0; border:none; padding:0; letter-spacing: 5px; } h2 { font-size:xx-large; font-weight:normal; margin: 0 0 20px 0; border:none; padding:0; } hr { margin-bottom:30px; border: 1px solid #222; background-color: #222; padding: 2px; } #logo { background-image: url(../img/pylons-logo.gif); background-repeat: no-repeat; height: 0; overflow: hidden; padding-top: 99px; width: 239px; } #left { float:left; width:250px; margin:0 50px 0 0; border:none; padding:0 0 0 10px; } #right { margin:0 0 0 330px; border:none; padding:0; } ul { list-style:none; margin:0; border:none; padding:0; } a:visited { color:white; text-decoration:none; } a:link { color:white; text-decoration:none; } ================================================ FILE: pylons/media/style/itraceback.css ================================================ /* @override http://localhost:5000/_debug/media/pylons/style/itraceback.css */ div.credits { padding: 5px 0 0 0; color: #777; font-size: 11px; } #footer { border-top: 1px solid #666666; padding: 0; margin: 20px 0 0 0; } #pylons_logo { float: right; margin: 5px 0 0 0; } div.widget_layout { margin: 30px 0 0 0; } div.feature-highlight { float: right; width: 400px; } div.feature-highlight button { margin: 15px 0 0 20px; } div#service_widget { width: 500px; padding: 0; margin: 0; margin-right: 10px; line-height: 1.4; } div#service_widget h4, div.overviewtab h3 { margin: 0; padding: 0; font-weight: bold; font-size: 100%; } div#service_widget h2 { color: #000; border-bottom: 1px solid #0a0; font-variant: small-caps; font-size: 20px; line-height: 1.3; font-weight: bold; } div.overviewtab, div.posttracebacktab, div.searchtab { padding: 13px 10px 10px 10px; background-color: #f8f8f8; border-bottom: 1px solid #ccc; border-right: 1px solid #ccc; border-left: 1px solid #ccc; } div.clearfix { clear: left; margin: 0; padding: 0; height: 0; } .hide { display: none; } input.query { width: 350px; } p.query { margin: 0 0 0 20px; padding: 0; } /* Nav Bar */ #nv li { display:block; float:left; list-style-type:none; margin:0; padding:0; } #nv a { display:block; float:left; border: 0px; } /*** Tabs ***/ #supportnav { float: left; width: 500px; border-bottom: 1px solid #CCC; margin: 0px; padding: 0px; } #supportnav li { float: left; padding-left: 10px; margin-right: 3px; background: url("../img/tab-traceback.png") no-repeat left top; } #supportnav li.first-child { margin: 0; } #supportnav li a { display: block; padding: 3px 10px 3px 0; border: none; outline: none; background: url("../img/tab-traceback.png") no-repeat right top; color: #666; } #supportnav li a:hover { color: #333; } #supportnav li.active { position: relative; margin-bottom: -1px; background-position: 0 -40px; } #supportnav li.active a { display: block; padding: 3px 10px 4px 0; background: url("../img/tab-traceback.png") no-repeat right -40px; font-weight: bold; color: #333; } #supportnav li.active a { background-position: right -40px; } * html #supportnav { margin-bottom: 0; } div.searchresults div.result { margin-bottom: 5px; padding: 2px 0px 2px 4px; cursor: pointer; background-color: rgb(247, 247, 247); border: 1px solid rgb(235, 235, 235); } div.searchresults div.result div.meta { font-size: 0.8em; color: rgb(1, 116, 22); } p.results a { float: right; } div.searchresults div.blurb { font-size: 0.8em; } div.searchresults div.result a { text-decoration: underline; font-size: 0.9em; cursor: pointer; } ================================================ FILE: pylons/media/style/orange.css ================================================ body{ margin: 0px; padding: 0px;/* 4% 0 0;*/ background: #494943 url(../img/bg.jpg) top repeat-x; font-family: Verdana, Arial, sans-serif; font-size: 75%; line-height: 160%; color: #333; /*min-width:770px; max-width: 1100px;*/ } /* Layout Styles */ /*.top-bar{background: #fff url(../img/bar.png) right top no-repeat;} */ div.main-content-bg{ } div.main-content-bar{ } div#nav-bar{ border-top: 1px solid #fff; float: left; width: 100%; margin-top: 16px; background: #fff url(../img/bar-bg.png) right top repeat-x; } div#main-content{ float: left; width: 100%; background: #fff; } div#side-content{ /*background: #FFf url(../img/bar.png) top right no-repeat;*/ padding-top: 42px; float: left; margin-top: 0px; width: 30%; clear: right; height: 550px; } div.three, div.two, div.one{ width: 746px; padding: 0 12px 0px 12px; clear: both; } div.three div.a, div.two div.a{ float: left; clear: left; width: 204px; /*background-color: #ddd;*/ } div.a div.padding, div.b div.padding, div.c div.padding, div.one div.padding{ padding: 0px 12px 20px 12px; } div.three div.b{ float: left; width: 271px; /*background-color: #ccc;*/ } div.three div.c{ float: left; clear: right; width: 271px; /*background-color: #bbb;*/ } div.two div.b{ float: left; clear: right; width: 542px; /* background-color: #333;*/ } /* Logo */ h1#logo{ margin: 20px 5% 0 5%; padding: 0; float: left; width: 155px; height: 52px; /*padding: 25px 5% 0 5%; margin: 0 0 0 -2px; float: left;*/ } /* Nav Bar */ #nav-global { margin:0; white-space:nowrap; float: right; /*position: absolute; margin:0; left:380px; top: 0px;*/ padding-right: 5%; clear: right; padding-bottom: 20px; } #nav-global li { display:block; float:left; list-style-type:none; margin:0; padding:0; } #nav-global a { display:block; float:left; /*font-family:"Trebuchet MS"; font-size: 110%; */ padding:42px 18px 7px 18px; background:#000000; color: #ccc; border: 0px; text-decoration: underline; } #nav-global a:hover { color:white; background: url(../img/main-nav-bg-on.png) bottom repeat-x; border: 0px; } #homepage #nav-homepage a, #overview #nav-overview a, #download #nav-download a, #documentation #nav-documentation a, #weblog #nav-weblog a, #community #nav-community a, #code #nav-code a { color:white; background:#000 url(../img/main-nav-bg.png) bottom repeat-x; } ul#navlist { /*background: url(../img/bar.png) left top no-repeat;*/ margin: 0; padding:0; padding-left: 5%; padding-top: 8px; color: #eee; line-height: 34px; } ul#navlist li { display: inline; } ul#navlist li a { /*font-family: Tahoma, sans-serif; font-size: 11px;font-weight: bold;*/ border: 0px; border-left: 1px solid #49A200; /*padding: 0em 1em;*/ padding: 0 18px 0 18px; color: #fff; margin-right: 9px; text-decoration: none; float: left; background: url('../img/tab-brown.png') no-repeat top right; } ul#navlist li a#highlight , ul#navlist li a#highlight:hover { background: url('../img/tab-yellow-highlight.png') no-repeat top right; color: #fff; padding-right: 33px; } ul#navlist li a:hover { background: url('../img/tab-yellow.png') no-repeat top right; color: #fff; } ul#navlist li.active a.active, ul#main-nav li.active a.active:hover { background: url('../img/tab-white.png') no-repeat top left; color: #333; } /* Font Styles */ a, a:link, a:visited, a:active { color: #0040BB; text-decoration: none; border-bottom: 1px dotted #ccc; } a:hover{ color: #000; border-bottom: 1px dotted #000; } a.no-underline, a.no-underline:link, a.no-underline:visited, a.no-underline:active { border: 0px; } img.no-border{ border: 0px; } /* Paragraph Styles */ .last{ padding-bottom: 0px; margin-bottom: 0px; } .first{ margin-top: 0px; } blockquote { padding-left: 10px; padding-right: 10px; margin-left: 5px; margin-right: 0; border-left: #ddd; border-width: 0 0 0 1px; border-style: none none none solid; } p.first, form{ padding: 0px; margin: 0px; } .medium{ font-family: Verdana, Geneva, Arial, sans-serif; font-size: 11px; line-height: 19px; } .large{ font-size: 110%; } .small{ font-size: 90%; line-height: 150%; font-family: Verdana, sans-serif; color: #000; } p.indent{ padding-left: 15px; padding-bottom: 15px; } tt, code{ font-size: 130%; color: #800000; } pre{ /* border-top: 1px dotted #000; border-left: 1px dotted #000; border-right: 1px solid #999; border-bottom: 1px solid #999; background: #eee;*/ font-size: 120%; padding: 3px 5px 3px 10px; margin-right: 10px; } .go{ /*background: url(../img/go.png) no-repeat right bottom;*/ padding-right: 20px; } .help, a.help{ cursor: help; font-weight: normal; } .highlight{ background: #ffffcc url(../img/highlight.png) repeat; } /* Horizontal Rule */ div.hr { clear: both; line-height: 0px; font-size: 0px; margin: 0px; padding: 0px; /*height: 4px; url(../img/horizontal-rule.png) no-repeat;background: #000; margin-bottom: 20px; width: 770px;*/ } hr { height: 1em; visibility: hidden; margin: 0px; padding: 0px; } /* Form Styles */ .form-style input[type=submit]{ background: #87CD15; color: #fff; border-top: 1px solid #999; border-left: 1px solid #999; border-bottom: 1px solid #333; border-right: 1px solid #333; } .form-style input[type=text], textarea, input[type=file]{ font-family: Tahoma, sans-serif; font-size: 11px; color: #444; background: #EAFEC2; border: solid 1px #aaa; padding: 3px; } .form-style select{ font-family: Tahoma, sans-serif; font-size: 11px; color: #444; background: #EAFEC2; border: solid 1px #aaa; } /* Lists */ ul.large{ padding: 0 0 0 15px; margin: 0 0 15px 0px; } ul.large li a { font-weight: normal; } /* Header Styles */ h1.first{ margin-top: 0px; border-bottom: 1px solid #666; background: #fFF; padding: 3px; } h1, h2, h3 { font-size: 170%; line-height: normal; font-weight: normal; font-family: Arial, sans-serif; color: #000000; text-decoration: none; /*margin: 0 0 12px 0; padding: 0;*/ text-align: left; } h2{ font-size: 140%; } h3 { font-size: 120%; } h4{ font-size: 100%; } h4.asterix{ /*background: url(../img/box-small.png) no-repeat left center;*/ padding: 0px 0px 0px 15px; margin: 0px; /*font-size: 110%; */ color: #000000; } /* Boxes */ div.faq-box{ /*border-style: solid; border-color: #C6F27A; border-width: 1px;*/ /*background:url('../img/hatch-green.png');*/ padding: 10px; /*font-family: Verdana, Geneva, Arial, sans-serif; font-size: 11px; line-height: 17px;*/ } div.highlight-box{ border-style: solid; border-color: #cccccc; border-width: 1px; background:url('../img/hatch-yellow.png'); padding: 10px; color: #555; /*font-family: Verdana, Geneva, Arial, sans-serif; font-size: 11px; line-height: 17px;*/ } .box-top{ width: 518px; /*background: url(../img/box-top.png) no-repeat left top;*/ padding-top: 7px; margin-bottom: 15px; } .box-middle{ /*background: url(../img/box-middle.png) repeat-y left top;*/ } .box-bottom{ /*background: url(../img/box-bottom.png) no-repeat left bottom;*/ padding: 0 10px 11px 12px; } /* Utility Styles */ .content-padding{ padding: 20px 6.5% 40px 6.5%; } .sidebar-padding{ padding: 15px 15px 40px 25px; } .invisible{ visibility: hidden; font-size: 1px; padding: 0px; line-height: 0px; margin: 0px; } a.no-underline{ border: 0px; } img.no-border{ border: 0px; } br.clear{ clear: both; width: 100%; } /* Quotes */ .quote-top p{ padding: 0px; margin: 0px; } .quote-top{ font-family: tahoma; /*background: url(../img/quote-top.png) no-repeat left top;*/ padding-left: 25px; padding-top: 4px; } .quote-bottom{ /*background: url(../img/quote-bottom.png) no-repeat right bottom;*/ padding-right: 17px; padding-bottom: 7px; } .quote-author{ /*background: url(../img/quote-author.png) no-repeat left top;*/ padding-left: 20px; padding-bottom: 20px; } /* Footer Styles */ #footer{padding: 5px 5% 40px 5%;} #footer p{ color: #333; } #footer a{ font-weight: normal; } #footer a:visited{ color: #666; text-decoration: none; border-bottom: 1px dotted; } #footer a{ color: #888; font-weight: normal; } #footer a:hover{ color: #000; text-decoration: none; border-bottom: 1px dotted; } ================================================ FILE: pylons/middleware.py ================================================ """Pylons' WSGI middlewares""" import logging import os.path from paste.deploy.converters import asbool from paste.urlparser import StaticURLParser from weberror.evalexception import EvalException from weberror.errormiddleware import ErrorMiddleware from webhelpers.html import literal import pylons from pylons.controllers.util import Request, Response from pylons.error import template_error_formatters from pylons.util import call_wsgi_application __all__ = ['ErrorHandler', 'error_document_template', 'footer_html', 'head_html', 'media_path'] log = logging.getLogger(__name__) media_path = os.path.join(os.path.dirname(__file__), 'media') head_html = """\ """ footer_html = """\

Online Assistance

 
Looking for help?

Here are a few tips for troubleshooting if the above traceback isn't helping out.

  1. Search the mail list
  2. Post a message to the mail list

The following mail lists will be searched:
Pylons
Python
Mako
SQLAlchemy

for:

Pylons version %s
""" report_libs = ['pylons', 'genshi', 'sqlalchemy'] def DebugHandler(app, global_conf, **kwargs): footer = footer_html % (kwargs.get('traceback_host', 'pylonshq.com'), pylons.__version__) py_media = dict(pylons=media_path) app = EvalException(app, global_conf, templating_formatters=template_error_formatters, media_paths=py_media, head_html=head_html, footer_html=footer, libraries=report_libs) return app def ErrorHandler(app, global_conf, **errorware): """ErrorHandler Toggle If debug is enabled, this function will return the app wrapped in the WebError ``EvalException`` middleware which displays interactive debugging sessions when a traceback occurs. Otherwise, the app will be wrapped in the WebError ``ErrorMiddleware``, and the ``errorware`` dict will be passed into it. The ``ErrorMiddleware`` handles sending an email to the address listed in the .ini file, under ``email_to``. """ if asbool(global_conf.get('debug')): footer = footer_html % (pylons.config.get('traceback_host', 'pylonshq.com'), pylons.__version__) py_media = dict(pylons=media_path) app = EvalException(app, global_conf, templating_formatters=template_error_formatters, media_paths=py_media, head_html=head_html, footer_html=footer, libraries=report_libs) else: app = ErrorMiddleware(app, global_conf, **errorware) return app class StatusCodeRedirect(object): """Internally redirects a request based on status code StatusCodeRedirect watches the response of the app it wraps. If the response is an error code in the errors sequence passed the request will be re-run with the path URL set to the path passed in. This operation is non-recursive and the output of the second request will be used no matter what it is. Should an application wish to bypass the error response (ie, to purposely return a 401), set ``environ['pylons.status_code_redirect'] = True`` in the application. """ def __init__(self, app, errors=(400, 401, 403, 404), path='/error/document'): """Initialize the ErrorRedirect ``errors`` A sequence (list, tuple) of error code integers that should be caught. ``path`` The path to set for the next request down to the application. """ self.app = app self.error_path = path # Transform errors to str for comparison self.errors = tuple([str(x) for x in errors]) def __call__(self, environ, start_response): status, headers, app_iter, exc_info = call_wsgi_application( self.app, environ, catch_exc_info=True) if status[:3] in self.errors and \ 'pylons.status_code_redirect' not in environ and self.error_path: # Create a response object environ['pylons.original_response'] = Response( status=status, headerlist=headers, app_iter=app_iter) environ['pylons.original_request'] = Request(environ) # Create a new environ to avoid touching the original request data new_environ = environ.copy() new_environ['PATH_INFO'] = self.error_path newstatus, headers, app_iter, exc_info = call_wsgi_application( self.app, new_environ, catch_exc_info=True) start_response(status, headers, exc_info) return app_iter error_document_template = literal("""\ Server Error %(code)s
%(message)s
""") def debugger_filter_factory(global_conf, **kwargs): def filter(app): return DebugHandler(app, global_conf, **kwargs) return filter def debugger_filter_app_factory(app, global_conf, **kwargs): return DebugHandler(app, global_conf, **kwargs) ================================================ FILE: pylons/templates/__init__.py ================================================ ================================================ FILE: pylons/templates/controller.py_tmpl ================================================ import logging from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect {{importstatement}} log = logging.getLogger(__name__) class {{name}}Controller(BaseController): def index(self): # Return a rendered template #return render('/{{tmpl_name}}.mako') # or, return a string return 'Hello World' ================================================ FILE: pylons/templates/default_project/+package+/__init__.py_tmpl ================================================ ================================================ FILE: pylons/templates/default_project/+package+/config/__init__.py_tmpl ================================================ ================================================ FILE: pylons/templates/default_project/+package+/config/deployment.ini_tmpl_tmpl ================================================ # # {{project}} - Pylons configuration # # The %(here)s variable will be replaced with the parent directory of this file # [DEFAULT] debug = true email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 0.0.0.0 port = 5000 [app:main] use = egg:{{project}} full_stack = true static_files = true cache_dir = %(here)s/data beaker.session.key = {{package}} beaker.session.secret = ${app_instance_secret} app_instance_uuid = ${app_instance_uuid} # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings # here: #beaker.cache.data_dir = %(here)s/data/cache #beaker.session.data_dir = %(here)s/data/sessions {{if sqlalchemy}} # SQLAlchemy database URL sqlalchemy.url = sqlite:///production.db {{endif}} # WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* # Debug mode will enable the interactive debugging tool, allowing ANYONE to # execute malicious code after an exception is raised. set debug = false # Logging configuration [loggers] keys = root [handlers] keys = console [formatters] keys = generic [logger_root] level = INFO handlers = console [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s ================================================ FILE: pylons/templates/default_project/+package+/config/environment.py_tmpl ================================================ """Pylons environment configuration""" import os {{if template_engine == 'mako'}} from mako.lookup import TemplateLookup {{elif template_engine == 'genshi'}} from genshi.template import TemplateLoader {{elif template_engine == 'jinja2'}} from jinja2 import Environment, FileSystemLoader {{endif}} from pylons.configuration import PylonsConfig {{if template_engine == 'mako'}} from pylons.error import handle_mako_error {{endif}} {{if sqlalchemy}} from sqlalchemy import engine_from_config {{endif}} import {{package}}.lib.app_globals as app_globals import {{package}}.lib.helpers from {{package}}.config.routing import make_map {{if sqlalchemy}} from {{package}}.model import init_model {{endif}} def load_environment(global_conf, app_conf): """Configure the Pylons environment via the ``pylons.config`` object """ config = PylonsConfig() # Pylons paths root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) paths = dict(root=root, controllers=os.path.join(root, 'controllers'), static_files=os.path.join(root, 'public'), templates=[os.path.join(root, 'templates')]) # Initialize config with the basic options config.init_app(global_conf, app_conf, package='{{package}}', paths=paths) config['routes.map'] = make_map(config) config['pylons.app_globals'] = app_globals.Globals(config) config['pylons.h'] = {{package}}.lib.helpers # Setup cache object as early as possible import pylons pylons.cache._push_object(config['pylons.app_globals'].cache) {{if template_engine == 'mako'}} # Create the Mako TemplateLookup, with the default auto-escaping config['pylons.app_globals'].mako_lookup = TemplateLookup( directories=paths['templates'], error_handler=handle_mako_error, module_directory=os.path.join(app_conf['cache_dir'], 'templates'), input_encoding='utf-8', default_filters=['escape'], imports=['from markupsafe import escape']) {{elif template_engine == 'genshi'}} # Create the Genshi TemplateLoader config['pylons.app_globals'].genshi_loader = TemplateLoader( paths['templates'], auto_reload=True) {{elif template_engine == 'jinja2'}} # Create the Jinja2 Environment jinja2_env = Environment(loader=FileSystemLoader(paths['templates'])) config['pylons.app_globals'].jinja2_env = jinja2_env {{endif}}{{if sqlalchemy}} # Setup the SQLAlchemy database engine engine = engine_from_config(config, 'sqlalchemy.') init_model(engine) {{endif}} # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) return config ================================================ FILE: pylons/templates/default_project/+package+/config/middleware.py_tmpl ================================================ """Pylons middleware initialization""" from beaker.middleware import SessionMiddleware from paste.cascade import Cascade from paste.registry import RegistryManager from paste.urlparser import StaticURLParser from paste.deploy.converters import asbool from pylons.middleware import ErrorHandler, StatusCodeRedirect from pylons.wsgiapp import PylonsApp from routes.middleware import RoutesMiddleware from {{package}}.config.environment import load_environment def make_app(global_conf, full_stack=True, static_files=True, **app_conf): """Create a Pylons WSGI application and return it ``global_conf`` The inherited configuration for this application. Normally from the [DEFAULT] section of the Paste ini file. ``full_stack`` Whether this application provides a full WSGI stack (by default, meaning it handles its own exceptions and errors). Disable full_stack when this application is "managed" by another WSGI middleware. ``static_files`` Whether this application serves its own static files; disable when another web server is responsible for serving them. ``app_conf`` The application's local configuration. Normally specified in the [app:] section of the Paste ini file (where defaults to main). """ # Configure the Pylons environment config = load_environment(global_conf, app_conf) # The Pylons WSGI app app = PylonsApp(config=config) # Routing/Session Middleware app = RoutesMiddleware(app, config['routes.map'], singleton=False) app = SessionMiddleware(app, config) # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) # Establish the Registry for this application app = RegistryManager(app) if asbool(static_files): # Serve static files static_app = StaticURLParser(config['pylons.paths']['static_files']) app = Cascade([static_app, app]) app.config = config return app ================================================ FILE: pylons/templates/default_project/+package+/config/routing.py_tmpl ================================================ """Routes configuration The more specific and detailed routes should be defined first so they may take precedent over the more generic routes. For more information refer to the routes manual at http://routes.groovie.org/docs/ """ from routes import Mapper def make_map(config): """Create, configure and return the routes Mapper""" map = Mapper(directory=config['pylons.paths']['controllers'], always_scan=config['debug']) map.minimization = False map.explicit = False # The ErrorController route (handles 404/500 error pages); it should # likely stay at the top, ensuring it can always be resolved map.connect('/error/{action}', controller='error') map.connect('/error/{action}/{id}', controller='error') # CUSTOM ROUTES HERE map.connect('/{controller}/{action}') map.connect('/{controller}/{action}/{id}') return map ================================================ FILE: pylons/templates/default_project/+package+/controllers/__init__.py_tmpl ================================================ ================================================ FILE: pylons/templates/default_project/+package+/controllers/error.py_tmpl ================================================ import cgi from paste.urlparser import PkgResourcesParser from pylons.middleware import error_document_template from webhelpers.html.builder import literal from {{package}}.lib.base import BaseController class ErrorController(BaseController): """Generates error documents as and when they are required. The ErrorDocuments middleware forwards to ErrorController when error related status codes are returned from the application. This behaviour can be altered by changing the parameters to the ErrorDocuments middleware in your config/middleware.py file. """ def document(self): """Render the error document""" request = self._py_object.request resp = request.environ.get('pylons.original_response') content = literal(resp.body) or cgi.escape(request.GET.get('message', '')) page = error_document_template % \ dict(prefix=request.environ.get('SCRIPT_NAME', ''), code=cgi.escape(request.GET.get('code', str(resp.status_int))), message=content) return page def img(self, id): """Serve Pylons' stock images""" return self._serve_file('/'.join(['media/img', id])) def style(self, id): """Serve Pylons' stock stylesheets""" return self._serve_file('/'.join(['media/style', id])) def _serve_file(self, path): """Call Paste's FileApp (a WSGI application) to serve the file at the specified path """ request = self._py_object.request request.environ['PATH_INFO'] = '/%s' % path return PkgResourcesParser('pylons', 'pylons')(request.environ, self.start_response) ================================================ FILE: pylons/templates/default_project/+package+/lib/__init__.py_tmpl ================================================ ================================================ FILE: pylons/templates/default_project/+package+/lib/app_globals.py_tmpl ================================================ """The application's Globals object""" from beaker.cache import CacheManager from beaker.util import parse_cache_config_options class Globals(object): """Globals acts as a container for objects available throughout the life of the application """ def __init__(self, config): """One instance of Globals is created during application initialization and is available during requests via the 'app_globals' variable """ self.cache = CacheManager(**parse_cache_config_options(config)) ================================================ FILE: pylons/templates/default_project/+package+/lib/base.py_tmpl ================================================ """The base Controller API Provides the BaseController class for subclassing. """ from pylons.controllers import WSGIController {{if template_engine in ('genshi', 'jinja2', 'mako')}} from pylons.templating import render_{{template_engine}} as render {{endif}} {{if sqlalchemy}} from {{package}}.model.meta import Session {{endif}} class BaseController(WSGIController): def __call__(self, environ, start_response): """Invoke the Controller""" # WSGIController.__call__ dispatches to the Controller method # the request is routed to. This routing information is # available in environ['pylons.routes_dict'] {{if sqlalchemy}} try: return WSGIController.__call__(self, environ, start_response) finally: Session.remove(){{else}} return WSGIController.__call__(self, environ, start_response){{endif}} ================================================ FILE: pylons/templates/default_project/+package+/lib/helpers.py_tmpl ================================================ """Helper functions Consists of functions to typically be used within templates, but also available to Controllers. This module is available to templates as 'h'. """ # Import helpers as desired, or define your own, ie: #from webhelpers.html.tags import checkbox, password ================================================ FILE: pylons/templates/default_project/+package+/model/__init__.py_tmpl ================================================ {{if sqlalchemy}} """The application's model objects""" from {{package}}.model.meta import Session, Base def init_model(engine): """Call me before using any of the tables or classes in the model""" Session.configure(bind=engine) {{endif}} ================================================ FILE: pylons/templates/default_project/+package+/model/meta.py_tmpl ================================================ {{if sqlalchemy}} """SQLAlchemy Metadata and Session object""" from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import scoped_session, sessionmaker __all__ = ['Base', 'Session'] # SQLAlchemy session manager. Updated by model.init_model() Session = scoped_session(sessionmaker()) # The declarative Base Base = declarative_base() {{else}} {{skip_template()}} {{endif}} ================================================ FILE: pylons/templates/default_project/+package+/public/index.html_tmpl ================================================ Welcome to Pylons!

Welcome to Logo displaying the word Pylons


Let's begin!

If you haven't used Pylons before, start with the beginners' tutorial.

================================================ FILE: pylons/templates/default_project/+package+/templates/.distutils_placeholder ================================================ ================================================ FILE: pylons/templates/default_project/+package+/templates/__init__.py_tmpl ================================================ {{if template_engine not in ['genshi', 'kid']}} {{skip_template()}} {{endif}} ================================================ FILE: pylons/templates/default_project/+package+/tests/__init__.py_tmpl ================================================ """Pylons application test package This package assumes the Pylons environment is already loaded, such as when this script is imported from the `nosetests --with-pylons=test.ini` command. This module initializes the application via ``websetup`` (`paster setup-app`) and provides the base testing objects. """ from unittest import TestCase import os import sys import pylons from pylons.i18n.translation import _get_translator from paste.deploy import loadapp from pylons import url from paste.script.appinstall import SetupCommand from routes.util import URLGenerator from webtest import TestApp from {{package}}.config.environment import load_environment __all__ = ['environ', 'url', 'TestController'] environ = {} here_dir = os.path.dirname(os.path.abspath(__file__)) conf_dir = os.path.dirname(os.path.dirname(here_dir)) sys.path.insert(0, conf_dir) class TestController(TestCase): def __init__(self, *args, **kwargs): wsgiapp = loadapp('config:test.ini', relative_to=conf_dir) config = wsgiapp.config pylons.app_globals._push_object(config['pylons.app_globals']) pylons.config._push_object(config) # Initialize a translator for tests that utilize i18n translator = _get_translator(pylons.config.get('lang')) pylons.translator._push_object(translator) url._push_object(URLGenerator(config['routes.map'], environ)) self.app = TestApp(wsgiapp) TestCase.__init__(self, *args, **kwargs) ================================================ FILE: pylons/templates/default_project/+package+/tests/functional/__init__.py_tmpl ================================================ ================================================ FILE: pylons/templates/default_project/+package+/tests/test_models.py_tmpl ================================================ ================================================ FILE: pylons/templates/default_project/+package+/websetup.py_tmpl ================================================ """Setup the {{project}} application""" import logging from {{package}}.config.environment import load_environment {{if sqlalchemy}} from {{package}}.model.meta import Session, Base {{endif}} log = logging.getLogger(__name__) def setup_app(command, conf, vars): """Place any commands to setup {{package}} here""" # Don't reload the app if it was loaded under the testing environment load_environment(conf.global_conf, conf.local_conf) {{if sqlalchemy}} # Create the tables if they don't already exist Base.metadata.create_all(bind=Session.bind) {{endif}} ================================================ FILE: pylons/templates/default_project/MANIFEST.in_tmpl ================================================ include {{package}}/config/deployment.ini_tmpl recursive-include {{package}}/public * recursive-include {{package}}/templates * ================================================ FILE: pylons/templates/default_project/README.txt_tmpl ================================================ This file is for you to describe the {{project}} application. Typically you would include information such as the information below: Installation and Setup ====================== Install ``{{project}}`` using easy_install:: easy_install {{project}} Make a config file as follows:: paster make-config {{project}} config.ini Tweak the config file as appropriate and then setup the application:: paster setup-app config.ini Then you are ready to go. ================================================ FILE: pylons/templates/default_project/development.ini_tmpl ================================================ # # {{project}} - Pylons development environment configuration # # The %(here)s variable will be replaced with the parent directory of this file # [DEFAULT] debug = true # Uncomment and replace with the address which should receive any error reports #email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 127.0.0.1 port = 5000 [app:main] use = egg:{{project}} full_stack = true static_files = true cache_dir = %(here)s/data beaker.session.key = {{package}} beaker.session.secret = somesecret # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings # here: #beaker.cache.data_dir = %(here)s/data/cache #beaker.session.data_dir = %(here)s/data/sessions {{if sqlalchemy}} # SQLAlchemy database URL sqlalchemy.url = sqlite:///%(here)s/development.db {{endif}} # WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* # Debug mode will enable the interactive debugging tool, allowing ANYONE to # execute malicious code after an exception is raised. #set debug = false # Logging configuration [loggers] keys = root, routes, {{package_logger}}{{if sqlalchemy}}, sqlalchemy{{endif}} [handlers] keys = console [formatters] keys = generic [logger_root] level = INFO handlers = console [logger_routes] level = INFO handlers = qualname = routes.middleware # "level = DEBUG" logs the route matched and routing variables. [logger_{{package_logger}}] level = DEBUG handlers = qualname = {{package}} {{if sqlalchemy}} [logger_sqlalchemy] level = INFO handlers = qualname = sqlalchemy.engine # "level = INFO" logs SQL queries. # "level = DEBUG" logs SQL queries and results. # "level = WARN" logs neither. (Recommended for production systems.) {{endif}} [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s datefmt = %H:%M:%S ================================================ FILE: pylons/templates/default_project/ez_setup.py ================================================ #!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c9" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import sys, os try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) ================================================ FILE: pylons/templates/default_project/setup.cfg_tmpl ================================================ [egg_info] tag_build = dev tag_svn_revision = true [easy_install] find_links = http://www.pylonshq.com/download/ # Babel configuration [compile_catalog] domain = {{package}} directory = {{package}}/i18n statistics = true [extract_messages] add_comments = TRANSLATORS: output_file = {{package}}/i18n/{{package}}.pot width = 80 [init_catalog] domain = {{package}} input_file = {{package}}/i18n/{{package}}.pot output_dir = {{package}}/i18n [update_catalog] domain = {{package}} input_file = {{package}}/i18n/{{package}}.pot output_dir = {{package}}/i18n previous = true ================================================ FILE: pylons/templates/default_project/setup.py_tmpl ================================================ try: from setuptools import setup, find_packages except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages setup( name='{{project}}', version='{{version}}', description={{repr(description)}}, author={{repr(author)}}, author_email={{repr(author_email)}}, url={{repr(url)}}, install_requires=[ "Pylons>=1.0.1rc1", {{if sqlalchemy}} "SQLAlchemy>=0.5", {{endif}} {{if template_engine == 'genshi'}} "Genshi>=0.4", {{elif template_engine == 'jinja2'}} "Jinja2", {{endif}} ], setup_requires=["PasteScript>=1.6.3"], packages=find_packages(exclude=['ez_setup']), include_package_data=True, test_suite='nose.collector', package_data={'{{package}}': ['i18n/*/LC_MESSAGES/*.mo']}, #message_extractors={'{{package}}': [ # ('**.py', 'python', None), # {{babel_templates_extractor}}('public/**', 'ignore', None)]}, zip_safe={{zip_safe}}, paster_plugins={{egg_plugins}}, entry_points=""" [paste.app_factory] main = {{package}}.config.middleware:make_app [paste.app_install] main = pylons.util:PylonsInstaller """, ) ================================================ FILE: pylons/templates/default_project/test.ini_tmpl ================================================ # # {{project}} - Pylons testing environment configuration # # The %(here)s variable will be replaced with the parent directory of this file # [DEFAULT] debug = true # Uncomment and replace with the address which should receive any error reports #email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 127.0.0.1 port = 5000 [app:main] use = config:development.ini # Add additional test specific configuration options as necessary. ================================================ FILE: pylons/templates/minimal_project/+package+/__init__.py_tmpl ================================================ ================================================ FILE: pylons/templates/minimal_project/+package+/config/deployment.ini_tmpl_tmpl ================================================ # # {{project}} - Pylons configuration # # The %(here)s variable will be replaced with the parent directory of this file # [DEFAULT] debug = true email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 0.0.0.0 port = 5000 [app:main] use = egg:{{project}} full_stack = true static_files = true cache_dir = %(here)s/data beaker.session.key = {{package}} beaker.session.secret = ${app_instance_secret} app_instance_uuid = ${app_instance_uuid} # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings # here: #beaker.cache.data_dir = %(here)s/data/cache #beaker.session.data_dir = %(here)s/data/sessions # WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* # Debug mode will enable the interactive debugging tool, allowing ANYONE to # execute malicious code after an exception is raised. set debug = false # Logging configuration [loggers] keys = root [handlers] keys = console [formatters] keys = generic [logger_root] level = INFO handlers = console [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(asctime)s %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s ================================================ FILE: pylons/templates/minimal_project/+package+/controllers/__init__.py_tmpl ================================================ """The base Controller API Provides the BaseController class for subclassing. """ from pylons.controllers import WSGIController {{if template_engine in ('genshi', 'jinja2', 'mako')}} from pylons.templating import render_{{template_engine}} as render {{endif}} class BaseController(WSGIController): def __call__(self, environ, start_response): """Invoke the Controller""" # WSGIController.__call__ dispatches to the Controller method # the request is routed to. This routing information is # available in environ['pylons.routes_dict'] return WSGIController.__call__(self, environ, start_response) ================================================ FILE: pylons/templates/minimal_project/+package+/helpers.py_tmpl ================================================ """Helper functions Consists of functions to typically be used within templates, but also available to Controllers. This module is available to templates as 'h'. """ # Import helpers as desired, or define your own, ie: #from webhelpers.html.tags import checkbox, password ================================================ FILE: pylons/templates/minimal_project/+package+/public/index.html_tmpl ================================================ Welcome to Pylons!

Welcome to Logo displaying the word Pylons


Let's begin!

If you haven't used Pylons before, start with the beginners' tutorial.

================================================ FILE: pylons/templates/minimal_project/+package+/routing.py_tmpl ================================================ """Routes configuration The more specific and detailed routes should be defined first so they may take precedent over the more generic routes. For more information refer to the routes manual at http://routes.groovie.org/docs/ """ from routes import Mapper def make_map(config): """Create, configure and return the routes Mapper""" map = Mapper(directory=config['pylons.paths']['controllers'], always_scan=config['debug']) map.minimization = False map.explicit = False # The ErrorController route (handles 404/500 error pages); it should # likely stay at the top, ensuring it can always be resolved map.connect('/error/{action}', controller='error') map.connect('/error/{action}/{id}', controller='error') # CUSTOM ROUTES HERE map.connect('/{controller}/{action}') map.connect('/{controller}/{action}/{id}') return map ================================================ FILE: pylons/templates/minimal_project/+package+/templates/.distutils_placeholder ================================================ ================================================ FILE: pylons/templates/minimal_project/+package+/templates/__init__.py_tmpl ================================================ {{if template_engine not in ['genshi', 'kid']}} {{skip_template()}} {{endif}} ================================================ FILE: pylons/templates/minimal_project/+package+/tests/__init__.py_tmpl ================================================ """Pylons application test package This package assumes the Pylons environment is already loaded, such as when this script is imported from the `nosetests --with-pylons=test.ini` command. This module initializes the application via ``websetup`` (`paster setup-app`) and provides the base testing objects. """ from unittest import TestCase from paste.deploy import loadapp from paste.script.appinstall import SetupCommand from pylons import url from routes.util import URLGenerator from webtest import TestApp import pylons.test __all__ = ['environ', 'url', 'TestController'] # Invoke websetup with the current config file SetupCommand('setup-app').run([pylons.test.pylonsapp.config['__file__']]) environ = {} class TestController(TestCase): def __init__(self, *args, **kwargs): wsgiapp = pylons.test.pylonsapp config = wsgiapp.config self.app = TestApp(wsgiapp) url._push_object(URLGenerator(config['routes.map'], environ)) TestCase.__init__(self, *args, **kwargs) ================================================ FILE: pylons/templates/minimal_project/+package+/wsgiapp.py_tmpl ================================================ """The {{project}} WSGI application""" import os from beaker.cache import CacheManager from beaker.middleware import SessionMiddleware from beaker.util import parse_cache_config_options {{if template_engine == 'mako'}} from mako.lookup import TemplateLookup {{elif template_engine == 'genshi'}} from genshi.template import TemplateLoader {{elif template_engine == 'jinja2'}} from jinja2 import ChoiceLoader, Environment, FileSystemLoader {{endif}} from paste.cascade import Cascade from paste.registry import RegistryManager from paste.urlparser import StaticURLParser from paste.deploy.converters import asbool from pylons.configuration import PylonsConfig {{if template_engine == 'mako'}} from pylons.error import handle_mako_error {{endif}} from pylons.middleware import ErrorHandler, StatusCodeRedirect from pylons.wsgiapp import PylonsApp from routes.middleware import RoutesMiddleware import {{package}}.helpers from {{package}}.routing import make_map def load_environment(global_conf, app_conf): """Configure the Pylons environment via the ``pylons.config`` object """ # Pylons paths config = PylonsConfig() root = os.path.dirname(os.path.abspath(__file__)) paths = dict(root=root, controllers=os.path.join(root, 'controllers'), static_files=os.path.join(root, 'public'), templates=[os.path.join(root, 'templates')]) # Initialize config with the basic options config.init_app(global_conf, app_conf, package='{{package}}', paths=paths) config['routes.map'] = make_map(config) config['pylons.app_globals'] = Globals(config) config['pylons.h'] = {{package}}.helpers {{if template_engine == 'mako'}} # Create the Mako TemplateLookup, with the default auto-escaping config['pylons.app_globals'].mako_lookup = TemplateLookup( directories=paths['templates'], error_handler=handle_mako_error, module_directory=os.path.join(app_conf['cache_dir'], 'templates'), input_encoding='utf-8', default_filters=['escape'], imports=['from markupsafe import escape']) {{elif template_engine == 'genshi'}} # Create the Genshi TemplateLoader config['pylons.app_globals'].genshi_loader = TemplateLoader( paths['templates'], auto_reload=True) {{elif template_engine == 'jinja2'}} # Create the Jinja2 Environment config['pylons.app_globals'].jinja2_env = Environment(loader=ChoiceLoader( [FileSystemLoader(path) for path in paths['templates']])) # Jinja2's unable to request c's attributes without strict_c config['pylons.strict_c'] = True {{endif}} # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) return config def make_app(global_conf, full_stack=True, static_files=True, **app_conf): """Create a Pylons WSGI application and return it ``global_conf`` The inherited configuration for this application. Normally from the [DEFAULT] section of the Paste ini file. ``full_stack`` Whether or not this application provides a full WSGI stack (by default, meaning it handles its own exceptions and errors). Disable full_stack when this application is "managed" by another WSGI middleware. ``static_files`` Whether this application serves its own static files; disable when another web server is responsible for serving them. ``app_conf`` The application's local configuration. Normally specified in the [app:] section of the Paste ini file (where defaults to main). """ # Configure the Pylons environment config = load_environment(global_conf, app_conf) # The Pylons WSGI app app = PylonsApp(config=config) # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map'], singleton=False) app = SessionMiddleware(app, config) # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [401, 403, 404, 500]) # Establish the Registry for this application app = RegistryManager(app) if asbool(static_files): # Serve static files static_app = StaticURLParser(config['pylons.paths']['static_files']) app = Cascade([static_app, app]) app.config = config return app class Globals(object): """Globals acts as a container for objects available throughout the life of the application """ def __init__(self, config): """One instance of Globals is created during application initialization and is available during requests via the 'app_globals' variable """ self.cache = CacheManager(**parse_cache_config_options(config)) ================================================ FILE: pylons/templates/minimal_project/MANIFEST.in_tmpl ================================================ include {{package}}/config/deployment.ini_tmpl recursive-include {{package}}/public * recursive-include {{package}}/templates * ================================================ FILE: pylons/templates/minimal_project/README.txt_tmpl ================================================ This file is for you to describe the {{project}} application. Typically you would include information such as the information below: Installation and Setup ====================== Install ``{{project}}`` using easy_install:: easy_install {{project}} Make a config file as follows:: paster make-config {{project}} config.ini Tweak the config file as appropriate and then setup the application:: paster setup-app config.ini Then you are ready to go. ================================================ FILE: pylons/templates/minimal_project/development.ini_tmpl ================================================ # # {{project}} - Pylons development environment configuration # # The %(here)s variable will be replaced with the parent directory of this file # [DEFAULT] debug = true # Uncomment and replace with the address which should receive any error reports #email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 127.0.0.1 port = 5000 [app:main] use = egg:{{project}} full_stack = true static_files = true cache_dir = %(here)s/data beaker.session.key = {{package}} beaker.session.secret = somesecret # If you'd like to fine-tune the individual locations of the cache data dirs # for the Cache data, or the Session saves, un-comment the desired settings # here: #beaker.cache.data_dir = %(here)s/data/cache #beaker.session.data_dir = %(here)s/data/sessions # WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* # Debug mode will enable the interactive debugging tool, allowing ANYONE to # execute malicious code after an exception is raised. #set debug = false # Logging configuration [loggers] keys = root, routes, {{package_logger}} [handlers] keys = console [formatters] keys = generic [logger_root] level = INFO handlers = console [logger_routes] level = INFO handlers = qualname = routes.middleware # "level = DEBUG" logs the route matched and routing variables. [logger_{{package_logger}}] level = DEBUG handlers = qualname = {{package}} {{if sqlalchemy}} [logger_sqlalchemy] level = INFO handlers = qualname = sqlalchemy.engine # "level = INFO" logs SQL queries. # "level = DEBUG" logs SQL queries and results. # "level = WARN" logs neither. (Recommended for production systems.) {{endif}} [handler_console] class = StreamHandler args = (sys.stderr,) level = NOTSET formatter = generic [formatter_generic] format = %(asctime)s,%(msecs)03d %(levelname)-5.5s [%(name)s] [%(threadName)s] %(message)s datefmt = %H:%M:%S ================================================ FILE: pylons/templates/minimal_project/ez_setup.py ================================================ #!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c9" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', } import sys, os try: from hashlib import md5 except ImportError: from md5 import md5 def _validate_md5(egg_name, data): if egg_name in md5_data: digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules def do_download(): egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg try: import pkg_resources except ImportError: return do_download() try: pkg_resources.require("setuptools>="+version); return except pkg_resources.VersionConflict, e: if was_imported: print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first, using 'easy_install -U setuptools'." "\n\n(Currently using %r)" ) % (version, e.args[0]) sys.exit(2) else: del pkg_resources, sys.modules['pkg_resources'] # reload ok return do_download() except pkg_resources.DistributionNotFound: return do_download() def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) ================================================ FILE: pylons/templates/minimal_project/setup.cfg_tmpl ================================================ [egg_info] tag_build = dev tag_svn_revision = true [easy_install] find_links = http://www.pylonshq.com/download/ [nosetests] with-pylons = test.ini ================================================ FILE: pylons/templates/minimal_project/setup.py_tmpl ================================================ try: from setuptools import setup, find_packages except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages setup( name='{{project}}', version='{{version}}', description={{repr(description)}}, author={{repr(author)}}, author_email={{repr(author_email)}}, url={{repr(url)}}, install_requires=[ "Pylons>=1.0.1rc1", {{if sqlalchemy}} "SQLAlchemy>=0.5", {{endif}} {{if template_engine == 'genshi'}} "Genshi>=0.4", {{elif template_engine == 'jinja2'}} "Jinja2", {{endif}} ], setup_requires=["PasteScript>=1.6.3"], packages=find_packages(exclude=['ez_setup']), include_package_data=True, test_suite='nose.collector', package_data={'{{package}}': ['i18n/*/LC_MESSAGES/*.mo']}, #message_extractors={'{{package}}': [ # ('**.py', 'python', None), # {{babel_templates_extractor}}('public/**', 'ignore', None)]}, zip_safe={{zip_safe}}, paster_plugins={{egg_plugins}}, entry_points=""" [paste.app_factory] main = {{package}}.wsgiapp:make_app [paste.app_install] main = pylons.util:PylonsInstaller """, ) ================================================ FILE: pylons/templates/minimal_project/test.ini_tmpl ================================================ # # {{project}} - Pylons testing environment configuration # # The %(here)s variable will be replaced with the parent directory of this file # [DEFAULT] debug = true # Uncomment and replace with the address which should receive any error reports #email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@localhost [server:main] use = egg:Paste#http host = 127.0.0.1 port = 5000 [app:main] use = config:development.ini # Add additional test specific configuration options as necessary. ================================================ FILE: pylons/templates/restcontroller.py_tmpl ================================================ import logging from pylons import request, response, session, tmpl_context as c, url from pylons.controllers.util import abort, redirect {{importstatement}} log = logging.getLogger(__name__) class {{classname}}Controller(BaseController): """REST Controller styled on the Atom Publishing Protocol""" # To properly map this controller, ensure your config/routing.py # file has a resource setup: # {{resource_command}} def index(self, format='html'): """GET /{{path}}{{pluralname}}: All items in the collection""" # url('{{nameprefix}}{{pluralname}}') def create(self): """POST /{{path}}{{pluralname}}: Create a new item""" # url('{{nameprefix}}{{pluralname}}') def new(self, format='html'): """GET /{{path}}{{pluralname}}/new: Form to create a new item""" # url('{{nameprefix}}new_{{singularname}}') def update(self, id): """PUT /{{path}}{{pluralname}}/id: Update an existing item""" # Forms posted to this method should contain a hidden field: # # Or using helpers: # h.form(url('{{nameprefix}}{{singularname}}', id=ID), # method='put') # url('{{nameprefix}}{{singularname}}', id=ID) def delete(self, id): """DELETE /{{path}}{{pluralname}}/id: Delete an existing item""" # Forms posted to this method should contain a hidden field: # # Or using helpers: # h.form(url('{{nameprefix}}{{singularname}}', id=ID), # method='delete') # url('{{nameprefix}}{{singularname}}', id=ID) def show(self, id, format='html'): """GET /{{path}}{{pluralname}}/id: Show a specific item""" # url('{{nameprefix}}{{singularname}}', id=ID) def edit(self, id, format='html'): """GET /{{path}}{{pluralname}}/id/edit: Form to edit an existing item""" # url('{{nameprefix}}edit_{{singularname}}', id=ID) ================================================ FILE: pylons/templates/test_controller.py_tmpl ================================================ from {{base_package}}.tests import * class Test{{name}}Controller(TestController): def test_index(self): response = self.app.get(url(controller='{{fname}}', action='index')) # Test response... ================================================ FILE: pylons/templates/test_restcontroller.py_tmpl ================================================ from {{base_package}}.tests import * class Test{{name}}Controller(TestController): def test_index(self): response = self.app.get(url('{{nameprefix}}{{pluralname}}')) # Test response... def test_index_as_xml(self): response = self.app.get(url('formatted_{{nameprefix}}{{pluralname}}', format='xml')) def test_create(self): response = self.app.post(url('{{nameprefix}}{{pluralname}}')) def test_new(self): response = self.app.get(url('{{nameprefix}}new_{{singularname}}')) def test_new_as_xml(self): response = self.app.get(url('formatted_{{nameprefix}}new_{{singularname}}', format='xml')) def test_update(self): response = self.app.put(url('{{nameprefix}}{{singularname}}', id=1)) def test_update_browser_fakeout(self): response = self.app.post(url('{{nameprefix}}{{singularname}}', id=1), params=dict(_method='put')) def test_delete(self): response = self.app.delete(url('{{nameprefix}}{{singularname}}', id=1)) def test_delete_browser_fakeout(self): response = self.app.post(url('{{nameprefix}}{{singularname}}', id=1), params=dict(_method='delete')) def test_show(self): response = self.app.get(url('{{nameprefix}}{{singularname}}', id=1)) def test_show_as_xml(self): response = self.app.get(url('formatted_{{nameprefix}}{{singularname}}', id=1, format='xml')) def test_edit(self): response = self.app.get(url('{{nameprefix}}edit_{{singularname}}', id=1)) def test_edit_as_xml(self): response = self.app.get(url('formatted_{{nameprefix}}edit_{{singularname}}', id=1, format='xml')) ================================================ FILE: pylons/templating.py ================================================ """Render functions and helpers Render functions and helpers ============================ :mod:`pylons.templating` includes several basic render functions, :func:`render_mako`, :func:`render_genshi` and :func:`render_jinja2` that render templates from the file-system with the assumption that variables intended for the will be attached to :data:`tmpl_context` (hereafter referred to by its short name of :data:`c` which it is commonly imported as). The default render functions work with the template language loader object that is setup on the :data:`app_globals` object in the project's :file:`config/environment.py`. Usage ----- Generally, one of the render functions will be imported in the controller. Variables intended for the template are attached to the :data:`c` object. The render functions return unicode (they actually return :class:`~webhelpers.html.literal` objects, a subclass of unicode). .. admonition :: Tip :data:`tmpl_context` (template context) is abbreviated to :data:`c` instead of its full name since it will likely be used extensively and it's much faster to use :data:`c`. Of course, for users that can't tolerate one-letter variables, feel free to not import :data:`tmpl_context` as :data:`c` since both names are available in templates as well. Example of rendering a template with some variables:: from pylons import tmpl_context as c from pylons.templating import render_mako as render from sampleproject.lib.base import BaseController class SampleController(BaseController): def index(self): c.first_name = "Joe" c.last_name = "Smith" return render('/some/template.mako') And the accompanying Mako template: .. code-block:: mako Hello ${c.first name}, I see your lastname is ${c.last_name}! Your controller will have additional default imports for commonly used functions. Template Globals ---------------- Templates rendered in Pylons should include the default Pylons globals as the :func:`render_mako`, :func:`render_genshi` and :func:`render_jinja2` functions. The full list of Pylons globals that are included in the template's namespace are: - :term:`c` -- Template context object - :term:`tmpl_context` -- Template context object - :data:`config` -- Pylons :class:`~pylons.configuration.PylonsConfig` object (acts as a dict) - :term:`app_globals` -- Project application globals object - :term:`h` -- Project helpers module reference - :data:`request` -- Pylons :class:`~pylons.controllers.util.Request` object for this request - :data:`response` -- Pylons :class:`~pylons.controllers.util.Response` object for this request - :class:`session` -- Pylons session object (unless Sessions are removed) - :class:`url ` -- Routes url generator object - :class:`translator` -- Gettext translator object configured for current locale - :func:`ungettext` -- Unicode capable version of gettext's ngettext function (handles plural translations) - :func:`_` -- Unicode capable gettext translate function - :func:`N_` -- gettext no-op function to mark a string for translation, but doesn't actually translate Configuring the template language --------------------------------- The template engine is created in the projects ``config/environment.py`` and attached to the ``app_globals`` (g) instance. Configuration options can be directly passed into the template engine, and are used by the render functions. .. warning:: Don't change the variable name on :data:`app_globals` that the template loader is attached to if you want to use the render_* functions that :mod:`pylons.templating` comes with. The render_* functions look for the template loader to render the template. """ import logging from webhelpers.html import literal import pylons __all__ = ['render_genshi', 'render_jinja2', 'render_mako'] PYLONS_VARS = ['c', 'app_globals', 'config', 'h', 'render', 'request', 'session', 'translator', 'ungettext', '_', 'N_'] log = logging.getLogger(__name__) def pylons_globals(): """Create and return a dictionary of global Pylons variables Render functions should call this to retrieve a list of global Pylons variables that should be included in the global template namespace if possible. Pylons variables that are returned in the dictionary: ``c``, ``h``, ``_``, ``N_``, config, request, response, translator, ungettext, ``url`` If SessionMiddleware is being used, ``session`` will also be available in the template namespace. """ conf = pylons.config._current_obj() c = pylons.tmpl_context._current_obj() app_globals = conf.get('pylons.app_globals') pylons_vars = dict( c=c, tmpl_context=c, config=conf, app_globals=app_globals, h=conf.get('pylons.h'), request=pylons.request._current_obj(), response=pylons.response._current_obj(), url=pylons.url._current_obj(), translator=pylons.translator._current_obj(), ungettext=pylons.i18n.ungettext, _=pylons.i18n._, N_=pylons.i18n.N_ ) # If the session was overriden to be None, don't populate the session # var econf = pylons.config['pylons.environ_config'] if 'beaker.session' in pylons.request.environ or \ ('session' in econf and econf['session'] in pylons.request.environ): pylons_vars['session'] = pylons.session._current_obj() log.debug("Created render namespace with pylons vars: %s", pylons_vars) return pylons_vars def cached_template(template_name, render_func, ns_options=(), cache_key=None, cache_type=None, cache_expire=None, **kwargs): """Cache and render a template Cache a template to the namespace ``template_name``, along with a specific key if provided. Basic Options ``template_name`` Name of the template, which is used as the template namespace. ``render_func`` Function used to generate the template should it no longer be valid or doesn't exist in the cache. ``ns_options`` Tuple of strings, that should correspond to keys likely to be in the ``kwargs`` that should be used to construct the namespace used for the cache. For example, if the template language supports the 'fragment' option, the namespace should include it so that the cached copy for a template is not the same as the fragment version of it. Caching options (uses Beaker caching middleware) ``cache_key`` Key to cache this copy of the template under. ``cache_type`` Valid options are ``dbm``, ``file``, ``memory``, ``database``, or ``memcached``. ``cache_expire`` Time in seconds to cache this template with this ``cache_key`` for. Or use 'never' to designate that the cache should never expire. The minimum key required to trigger caching is ``cache_expire='never'`` which will cache the template forever seconds with no key. """ # If one of them is not None then the user did set something if cache_key is not None or cache_expire is not None or cache_type \ is not None: if not cache_type: cache_type = 'dbm' if not cache_key: cache_key = 'default' if cache_expire == 'never': cache_expire = None namespace = template_name for name in ns_options: namespace += str(kwargs.get(name)) cache = pylons.cache.get_cache(namespace, type=cache_type) content = cache.get_value(cache_key, createfunc=render_func, expiretime=cache_expire) return content else: return render_func() def render_mako(template_name, extra_vars=None, cache_key=None, cache_type=None, cache_expire=None): """Render a template with Mako Accepts the cache options ``cache_key``, ``cache_type``, and ``cache_expire``. """ # Create a render callable for the cache function def render_template(): # Pull in extra vars if needed globs = extra_vars or {} # Second, get the globals globs.update(pylons_globals()) # Grab a template reference template = globs['app_globals'].mako_lookup.get_template(template_name) return literal(template.render_unicode(**globs)) return cached_template(template_name, render_template, cache_key=cache_key, cache_type=cache_type, cache_expire=cache_expire) def render_mako_def(template_name, def_name, cache_key=None, cache_type=None, cache_expire=None, **kwargs): """Render a def block within a Mako template Takes the template name, and the name of the def within it to call. If the def takes arguments, they should be passed in as keyword arguments. Example:: # To call the def 'header' within the 'layout.mako' template # with a title argument render_mako_def('layout.mako', 'header', title='Testing') Also accepts the cache options ``cache_key``, ``cache_type``, and ``cache_expire``. """ # Create a render callable for the cache function def render_template(): # Pull in extra vars if needed globs = kwargs or {} # Second, get the globals globs.update(pylons_globals()) # Grab a template reference template = globs['app_globals'].mako_lookup.get_template( template_name).get_def(def_name) return literal(template.render_unicode(**globs)) return cached_template(template_name, render_template, cache_key=cache_key, cache_type=cache_type, cache_expire=cache_expire) def render_genshi(template_name, extra_vars=None, cache_key=None, cache_type=None, cache_expire=None, method='xhtml'): """Render a template with Genshi Accepts the cache options ``cache_key``, ``cache_type``, and ``cache_expire`` in addition to method which are passed to Genshi's render function. """ # Create a render callable for the cache function def render_template(): # Pull in extra vars if needed globs = extra_vars or {} # Second, get the globals globs.update(pylons_globals()) # Grab a template reference template = globs['app_globals'].genshi_loader.load(template_name) return literal(template.generate(**globs).render(method=method, encoding=None)) return cached_template(template_name, render_template, cache_key=cache_key, cache_type=cache_type, cache_expire=cache_expire, ns_options=('method'), method=method) def render_jinja2(template_name, extra_vars=None, cache_key=None, cache_type=None, cache_expire=None): """Render a template with Jinja2 Accepts the cache options ``cache_key``, ``cache_type``, and ``cache_expire``. """ # Create a render callable for the cache function def render_template(): # Pull in extra vars if needed globs = extra_vars or {} # Second, get the globals globs.update(pylons_globals()) # Grab a template reference template = \ globs['app_globals'].jinja2_env.get_template(template_name) return literal(template.render(**globs)) return cached_template(template_name, render_template, cache_key=cache_key, cache_type=cache_type, cache_expire=cache_expire) ================================================ FILE: pylons/test.py ================================================ """Test related functionality Adds a Pylons plugin to `nose `_ that loads the Pylons app *before* scanning for doc tests. This can be configured in the projects :file:`setup.cfg` under a ``[nosetests]`` block: .. code-block:: ini [nosetests] with-pylons=development.ini Alternate ini files may be specified if the app should be loaded using a different configuration. """ import os import sys import nose.plugins import pkg_resources from paste.deploy import loadapp import pylons from pylons.i18n.translation import _get_translator pylonsapp = None class PylonsPlugin(nose.plugins.Plugin): """Nose plugin extension For use with nose to allow a project to be configured before nose proceeds to scan the project for doc tests and unit tests. This prevents modules from being loaded without a configured Pylons environment. """ enabled = False enableOpt = 'pylons_config' name = 'pylons' def options(self, parser, env=os.environ): """Add command-line options for this plugin""" env_opt = 'NOSE_WITH_%s' % self.name.upper() env_opt.replace('-', '_') parser.add_option("--with-%s" % self.name, dest=self.enableOpt, type="string", default="", help="Setup Pylons environment with the config file" " specified by ATTR [NOSE_ATTR]") def configure(self, options, conf): """Configure the plugin""" self.config_file = None self.conf = conf if hasattr(options, self.enableOpt): self.enabled = bool(getattr(options, self.enableOpt)) self.config_file = getattr(options, self.enableOpt) def begin(self): """Called before any tests are collected or run Loads the application, and in turn its configuration. """ global pylonsapp path = os.getcwd() sys.path.insert(0, path) pkg_resources.working_set.add_entry(path) self.app = pylonsapp = loadapp('config:' + self.config_file, relative_to=path) # Setup the config and app_globals, only works if we can get # to the config object conf = getattr(pylonsapp, 'config') if conf: pylons.config._push_object(conf) if 'pylons.app_globals' in conf: pylons.app_globals._push_object(conf['pylons.app_globals']) # Initialize a translator for tests that utilize i18n translator = _get_translator(pylons.config.get('lang')) pylons.translator._push_object(translator) ================================================ FILE: pylons/testutil.py ================================================ """Utility classes for creating workable pylons controllers for unit testing. These classes are used solely by Pylons for unit testing controller functionality. """ import gettext import pylons from pylons.configuration import request_defaults, response_defaults from pylons.controllers.util import Request, Response from pylons.util import ContextObj, PylonsContext class ControllerWrap(object): def __init__(self, controller): self.controller = controller def __call__(self, environ, start_response): app = self.controller() app.start_response = None return app(environ, start_response) class SetupCacheGlobal(object): def __init__(self, app, environ, setup_g=True, setup_cache=False, setup_session=False): if setup_g: g = type('G object', (object,), {}) g.message = 'Hello' g.counter = 0 g.pylons_config = type('App conf', (object,), {}) g.pylons_config.app_conf = dict(cache_enabled='True') self.g = g self.app = app self.environ = environ self.setup_cache = setup_cache self.setup_session = setup_session self.setup_g = setup_g def __call__(self, environ, start_response): registry = environ['paste.registry'] py_obj = PylonsContext() environ_config = environ.setdefault('pylons.environ_config', {}) if self.setup_cache: py_obj.cache = environ['beaker.cache'] registry.register(pylons.cache, environ['beaker.cache']) environ_config['cache'] = 'beaker.cache' if self.setup_session: py_obj.session = environ['beaker.session'] registry.register(pylons.session, environ['beaker.session']) environ_config['session'] = 'beaker.session' if self.setup_g: py_obj.app_globals = self.g registry.register(pylons.app_globals, self.g) translator = gettext.NullTranslations() py_obj.translator = translator registry.register(pylons.translator, translator) # Update the environ req = Request(environ, charset=request_defaults['charset'], unicode_errors=request_defaults['errors'], decode_param_names=request_defaults['decode_param_names'] ) req.language = request_defaults['language'] response = Response( content_type=response_defaults['content_type'], charset=response_defaults['charset']) response.headers.update(response_defaults['headers']) environ.update(self.environ) py_obj.config = pylons.config._current_obj() py_obj.request = req py_obj.response = response py_obj.tmpl_context = ContextObj() environ['pylons.pylons'] = py_obj registry.register(pylons.request, req) registry.register(pylons.response, response) if 'routes.url' in environ: registry.register(pylons.url, environ['routes.url']) return self.app(environ, start_response) ================================================ FILE: pylons/url.py ================================================ from repoze.bfg.encode import urlencode from repoze.bfg.threadlocal import get_current_registry from repoze.bfg.url import _join_elements from pylons.interfaces import IRoutesMapper def route_url(route_name, request, *elements, **kw): try: reg = request.registry except AttributeError: reg = get_current_registry() # b/c mapper = reg.getUtility(IRoutesMapper) route = mapper.routes.get(route_name) if route and 'custom_url_generator' in route.__dict__: route_name, request, elements, kw = route.custom_url_generator( route_name, request, *elements, **kw) anchor = '' qs = '' app_url = None if '_query' in kw: qs = '?' + urlencode(kw.pop('_query'), doseq=True) if '_anchor' in kw: anchor = kw.pop('_anchor') if isinstance(anchor, unicode): anchor = anchor.encode('utf-8') anchor = '#' + anchor if '_app_url' in kw: app_url = kw.pop('_app_url') path = mapper.generate(route_name, kw) # raises KeyError if generate fails if elements: suffix = _join_elements(elements) if not path.endswith('/'): suffix = '/' + suffix else: suffix = '' if app_url is None: # we only defer lookup of application_url until here because # it's somewhat expensive; we won't need to do it if we've # been passed _app_url app_url = request.application_url return app_url + path + suffix + qs + anchor ================================================ FILE: pylons/util.py ================================================ """Paste Template and Pylons utility functions PylonsTemplate is a Paste Template sub-class that configures the source directory and default plug-ins for a new Pylons project. The minimal template a more minimal template with less additional directories and layout. """ import logging import sys import pkg_resources from paste.deploy.converters import asbool from paste.script.appinstall import Installer from paste.script.templates import Template, var from tempita import paste_script_template_renderer import pylons import pylons.configuration import pylons.i18n __all__ = ['AttribSafeContextObj', 'ContextObj', 'PylonsContext', 'class_name_from_module_name', 'call_wsgi_application'] log = logging.getLogger(__name__) def call_wsgi_application(application, environ, catch_exc_info=False): """ Call the given WSGI application, returning ``(status_string, headerlist, app_iter)`` Be sure to call ``app_iter.close()`` if it's there. If catch_exc_info is true, then returns ``(status_string, headerlist, app_iter, exc_info)``, where the fourth item may be None, but won't be if there was an exception. If you don't do this and there was an exception, the exception will be raised directly. """ captured = [] output = [] def start_response(status, headers, exc_info=None): if exc_info is not None and not catch_exc_info: raise exc_info[0], exc_info[1], exc_info[2] captured[:] = [status, headers, exc_info] return output.append app_iter = application(environ, start_response) if not captured or output: try: output.extend(app_iter) finally: if hasattr(app_iter, 'close'): app_iter.close() app_iter = output if catch_exc_info: return (captured[0], captured[1], app_iter, captured[2]) else: return (captured[0], captured[1], app_iter) def class_name_from_module_name(module_name): """Takes a module name and returns the name of the class it defines. If the module name contains dashes, they are replaced with underscores. Example:: >>> class_name_from_module_name('with-dashes') 'WithDashes' >>> class_name_from_module_name('with_underscores') 'WithUnderscores' >>> class_name_from_module_name('oneword') 'Oneword' """ words = module_name.replace('-', '_').split('_') return ''.join(w.title() for w in words) class PylonsContext(object): """Pylons context object All the Pylons Stacked Object Proxies are also stored here, for use in generators and async based operation where the globals can't be used. This object is attached in :class:`~pylons.controllers.core.WSGIController` instances as :attr:`~WSGIController._py_object`. For example:: class MyController(WSGIController): def index(self): pyobj = self._py_object return "Environ is %s" % pyobj.request.environ """ pass class ContextObj(object): """The :term:`tmpl_context` object, with strict attribute access (raises an Exception when the attribute does not exist)""" def __repr__(self): attrs = sorted((name, value) for name, value in self.__dict__.iteritems() if not name.startswith('_')) parts = [] for name, value in attrs: value_repr = repr(value) if len(value_repr) > 70: value_repr = value_repr[:60] + '...' + value_repr[-5:] parts.append(' %s=%s' % (name, value_repr)) return '<%s.%s at %s%s>' % ( self.__class__.__module__, self.__class__.__name__, hex(id(self)), ','.join(parts)) class AttribSafeContextObj(ContextObj): """The :term:`tmpl_context` object, with lax attribute access ( returns '' when the attribute does not exist)""" def __getattr__(self, name): try: return object.__getattribute__(self, name) except AttributeError: log.debug("No attribute called %s found on c object, returning " "empty string", name) return '' class PylonsTemplate(Template): _template_dir = ('pylons', 'templates/default_project') template_renderer = staticmethod(paste_script_template_renderer) summary = 'Pylons application template' egg_plugins = ['PasteScript', 'Pylons'] vars = [ var('template_engine', 'mako/genshi/jinja2/etc: Template language', default='mako'), var('sqlalchemy', 'True/False: Include SQLAlchemy configuration', default=False), ] ensure_names = ['description', 'author', 'author_email', 'url'] def pre(self, command, output_dir, vars): """Called before template is applied.""" package_logger = vars['package'] if package_logger == 'root': # Rename the app logger in the rare case a project is named 'root' package_logger = 'app' vars['package_logger'] = package_logger vars['template_engine'] = 'mako' template_engine = 'mako' if template_engine == 'mako': # Support a Babel extractor default for Mako vars['babel_templates_extractor'] = \ ("('templates/**.mako', 'mako', {'input_encoding': 'utf-8'})" ",\n%s#%s" % (' ' * 4, ' ' * 8)) else: vars['babel_templates_extractor'] = '' # Ensure these exist in the namespace for name in self.ensure_names: vars.setdefault(name, '') vars['version'] = vars.get('version', '0.1') vars['zip_safe'] = asbool(vars.get('zip_safe', 'false')) vars['sqlalchemy'] = asbool(vars.get('sqlalchemy', 'false')) class MinimalPylonsTemplate(PylonsTemplate): _template_dir = ('pylons', 'templates/minimal_project') summary = 'Pylons minimal application template' vars = [ var('template_engine', 'mako/genshi/jinja2/etc: Template language', default='mako'), ] class LegacyPylonsTemplate(PylonsTemplate): _template_dir = ('pylons', 'templates/legacy_project') summary = 'Pylons legacy application template' vars = [ var('template_engine', 'mako/genshi/jinja2/etc: Template language', default='mako'), ] class NewPylonsTemplate(PylonsTemplate): _template_dir = ('pylons', 'templates/new_project') summary = 'Pylons "newstyle" application template' vars = [] class NewMinimalPylonsTemplate(PylonsTemplate): _template_dir = ('pylons', 'templates/newminimal_project') summary = 'Pylons "newstyle" minimal application template' vars = [] class NewSQLAlchemyTemplate(PylonsTemplate): _template_dir = ('pylons', 'templates/newsqla_project') summary = 'Pylons "newstyle" SQLAlchemy template' vars = [] class PylonsInstaller(Installer): use_cheetah = False config_file = 'config/deployment.ini_tmpl' def config_content(self, command, vars): """ Called by ``self.write_config``, this returns the text content for the config file, given the provided variables. """ modules = [line.strip() for line in self.dist.get_metadata_lines('top_level.txt') if line.strip() and not line.strip().startswith('#')] if not modules: print >> sys.stderr, 'No modules are listed in top_level.txt' print >> sys.stderr, \ 'Try running python setup.py egg_info to regenerate that file' for module in modules: if pkg_resources.resource_exists(module, self.config_file): return self.template_renderer( pkg_resources.resource_string(module, self.config_file), vars, filename=self.config_file) # Legacy support for the old location in egg-info return super(PylonsInstaller, self).config_content(command, vars) def resolve_dotted(name): return pkg_resources.EntryPoint.parse('x=%s' % name).load(False) ================================================ FILE: pylons/wsgiapp.py ================================================ """WSGI App Creator This module is responsible for creating the basic Pylons WSGI application (PylonsApp). It's generally assumed that it will be called by Paste, though any WSGI server could create and call the WSGI app as well. """ import logging import sys import paste.registry import pkg_resources from webob.exc import HTTPNotFound import pylons import pylons.templating from pylons.controllers.util import Request, Response from pylons.i18n.translation import _get_translator from pylons.util import (AttribSafeContextObj, ContextObj, PylonsContext, class_name_from_module_name) __all__ = ['PylonsApp'] log = logging.getLogger(__name__) class PylonsApp(object): """Pylons WSGI Application This basic WSGI app is provided should a web developer want to get access to the most basic Pylons web application environment available. By itself, this Pylons web application does little more than dispatch to a controller and setup the context object, the request object, and the globals object. Additional functionality like sessions, and caching can be setup by altering the ``environ['pylons.environ_config']`` setting to indicate what key the ``session`` and ``cache`` functionality should come from. Resolving the URL and dispatching can be customized by sub-classing or "monkey-patching" this class. Subclassing is the preferred approach. """ def __init__(self, config=None, **kwargs): """Initialize a base Pylons WSGI application The base Pylons WSGI application requires several keywords, the package name, and the globals object. If no helpers object is provided then h will be None. """ self.config = config = config or pylons.config._current_obj() package_name = config['pylons.package'] self.helpers = config['pylons.h'] self.globals = config.get('pylons.app_globals') self.environ_config = config['pylons.environ_config'] self.package_name = package_name self.request_options = config['pylons.request_options'] self.response_options = config['pylons.response_options'] self.controller_classes = {} self.log_debug = False self.config.setdefault('lang', None) # Cache some options for use during requests self._session_key = self.environ_config.get('session', 'beaker.session') self._cache_key = self.environ_config.get('cache', 'beaker.cache') def __call__(self, environ, start_response): """Setup and handle a web request PylonsApp splits its functionality into several methods to make it easier to subclass and customize core functionality. The methods are called in the following order: 1. :meth:`~PylonsApp.setup_app_env` 2. :meth:`~PylonsApp.load_test_env` (Only if operating in testing mode) 3. :meth:`~PylonsApp.resolve` 4. :meth:`~PylonsApp.dispatch` The response from :meth:`~PylonsApp.dispatch` is expected to be an iterable (valid :pep:`333` WSGI response), which is then sent back as the response. """ # Cache the logging level for the request log_debug = self.log_debug = logging.DEBUG >= log.getEffectiveLevel() environ['pylons.log_debug'] = log_debug self.setup_app_env(environ, start_response) if 'paste.testing_variables' in environ: self.load_test_env(environ) if environ['PATH_INFO'] == '/_test_vars': paste.registry.restorer.save_registry_state(environ) start_response('200 OK', [('Content-type', 'text/plain')]) return ['%s' % paste.registry.restorer.get_request_id(environ)] controller = self.resolve(environ, start_response) response = self.dispatch(controller, environ, start_response) response_obj = callable(response) if 'paste.testing_variables' in environ and response_obj: environ['paste.testing_variables']['response'] = response try: if response_obj: return response(environ, start_response) elif response is not None: return response raise Exception("No content returned by controller (Did you " "remember to 'return' it?) in: %r" % controller.__name__) finally: # Help Python collect ram a bit faster by removing the reference # cycle that the pylons object causes if 'pylons.pylons' in environ: del environ['pylons.pylons'] def register_globals(self, environ): """Registers globals in the environment, called from :meth:`~PylonsApp.setup_app_env` Override this to control how the Pylons API is setup. Note that a custom render function will need to be used if the ``pylons.app_globals`` global is not available. """ pylons_obj = environ['pylons.pylons'] registry = environ['paste.registry'] registry.register(pylons.response, pylons_obj.response) registry.register(pylons.request, pylons_obj.request) registry.register(pylons.app_globals, self.globals) registry.register(pylons.config, self.config) registry.register(pylons.tmpl_context, pylons_obj.tmpl_context) registry.register(pylons.translator, pylons_obj.translator) if 'session' in pylons_obj.__dict__: registry.register(pylons.session, pylons_obj.session) if 'cache' in pylons_obj.__dict__: registry.register(pylons.cache, pylons_obj.cache) elif 'cache' in pylons_obj.app_globals.__dict__: registry.register(pylons.cache, pylons_obj.app_globals.cache) if 'routes.url' in environ: registry.register(pylons.url, environ['routes.url']) def setup_app_env(self, environ, start_response): """Setup and register all the Pylons objects with the registry After creating all the global objects for use in the request, :meth:`~PylonsApp.register_globals` is called to register them in the environment. """ if self.log_debug: log.debug("Setting up Pylons stacked object globals") # Setup the basic pylons global objects req_options = self.request_options req = Request(environ, charset=req_options['charset'], unicode_errors=req_options['errors'], decode_param_names=req_options['decode_param_names']) req.language = req_options['language'] req.config = self.config req.link, req.route_dict = environ['wsgiorg.routing_args'] response = Response( content_type=self.response_options['content_type'], charset=self.response_options['charset']) response.headers.update(self.response_options['headers']) # Store a copy of the request/response in environ for faster access pylons_obj = PylonsContext() pylons_obj.config = self.config pylons_obj.request = req pylons_obj.response = response pylons_obj.app_globals = self.globals pylons_obj.h = self.helpers if 'routes.url' in environ: pylons_obj.url = environ['routes.url'] environ['pylons.pylons'] = pylons_obj environ['pylons.environ_config'] = self.environ_config # Setup the translator object lang = self.config['lang'] pylons_obj.translator = _get_translator(lang, pylons_config=self.config) if self.config['pylons.strict_tmpl_context']: tmpl_context = ContextObj() else: tmpl_context = AttribSafeContextObj() pylons_obj.tmpl_context = req.tmpl_context = tmpl_context if self._session_key in environ: pylons_obj.session = req.session = environ[self._session_key] if self._cache_key in environ: pylons_obj.cache = environ[self._cache_key] # Load the globals with the registry if around if 'paste.registry' in environ: self.register_globals(environ) def resolve(self, environ, start_response): """Uses dispatching information found in ``environ['wsgiorg.routing_args']`` to retrieve a controller name and return the controller instance from the appropriate controller module. Override this to change how the controller name is found and returned. """ match = environ['wsgiorg.routing_args'][1] environ['pylons.routes_dict'] = match controller = match.get('controller', match.get('responder')) if not controller: return if self.log_debug: log.debug("Resolved URL to controller: %r", controller) return self.find_controller(controller) def find_controller(self, controller): """Locates a controller by attempting to import it then grab the SomeController instance from the imported module. Controller name is assumed to be a module in the controllers directory unless it contains a '.' or ':' which is then assumed to be a dotted path to the module and name of the controller object. Override this to change how the controller object is found once the URL has been resolved. """ # If this isn't a basestring, its an object, assume that its the # proper instance to begin with if not isinstance(controller, basestring): return controller # Check to see if we've cached the class instance for this name if controller in self.controller_classes: return self.controller_classes[controller] # Check to see if its a dotted name if '.' in controller or ':' in controller: mycontroller = pkg_resources.EntryPoint.parse( 'x=%s' % controller).load(False) self.controller_classes[controller] = mycontroller return mycontroller # Pull the controllers class name, import controller full_module_name = self.package_name + '.controllers.' \ + controller.replace('/', '.') # Hide the traceback here if the import fails (bad syntax and such) __traceback_hide__ = 'before_and_this' __import__(full_module_name) if hasattr(sys.modules[full_module_name], '__controller__'): mycontroller = getattr(sys.modules[full_module_name], sys.modules[full_module_name].__controller__) else: module_name = controller.split('/')[-1] class_name = class_name_from_module_name(module_name) + 'Controller' if self.log_debug: log.debug("Found controller, module: '%s', class: '%s'", full_module_name, class_name) mycontroller = getattr(sys.modules[full_module_name], class_name) self.controller_classes[controller] = mycontroller return mycontroller def dispatch(self, controller, environ, start_response): """Dispatches to a controller, will instantiate the controller if necessary. Override this to change how the controller dispatch is handled. """ log_debug = self.log_debug if not controller: if log_debug: log.debug("No controller found, returning 404 HTTP Not Found") return HTTPNotFound()(environ, start_response) # Is it a responder? if 'responder' in environ['pylons.routes_dict']: return controller(environ['pylons.pylons'].request) # Is it a class? Then its a WSGIController if hasattr(controller, '__bases__'): if log_debug: log.debug("Controller appears to be a class, instantiating") controller = controller() controller._pylons_log_debug = log_debug # Add a reference to the controller app located environ['pylons.controller'] = controller # Controller is assumed to handle a WSGI call if log_debug: log.debug("Calling controller class with WSGI interface") return controller(environ, start_response) def load_test_env(self, environ): """Sets up our Paste testing environment""" if self.log_debug: log.debug("Setting up paste testing environment variables") testenv = environ['paste.testing_variables'] pylons_obj = environ['pylons.pylons'] testenv['req'] = pylons_obj.request testenv['response'] = pylons_obj.response testenv['tmpl_context'] = pylons_obj.tmpl_context testenv['app_globals'] = testenv['g'] = pylons_obj.app_globals testenv['h'] = self.config['pylons.h'] testenv['config'] = self.config if hasattr(pylons_obj, 'session'): testenv['session'] = pylons_obj.session if hasattr(pylons_obj, 'cache'): testenv['cache'] = pylons_obj.cache elif hasattr(pylons_obj.app_globals, 'cache'): testenv['cache'] = pylons_obj.app_globals.cache ================================================ FILE: rtd.txt ================================================ repoze.sphinx.autointerface babel ================================================ FILE: scripts/gen-go-pylons.py ================================================ #!/usr/bin/env python """Generate go-pylons.py""" import sys import textwrap import virtualenv filename = 'go-pylons.py' after_install = """\ import os, subprocess def after_install(options, home_dir): etc = join(home_dir, 'etc') ## TODO: this should all come from distutils ## like distutils.sysconfig.get_python_inc() if sys.platform == 'win32': lib_dir = join(home_dir, 'Lib') bin_dir = join(home_dir, 'Scripts') elif is_jython: lib_dir = join(home_dir, 'Lib') bin_dir = join(home_dir, 'bin') else: lib_dir = join(home_dir, 'lib', py_version) bin_dir = join(home_dir, 'bin') if not os.path.exists(etc): os.makedirs(etc) subprocess.call([join(bin_dir, 'easy_install'), '-f', 'http://pylonshq.com/download/%s', 'Pylons==%s']) """ def generate(filename, version): path = version if '==' in version: path = version[:version.find('==')] output = virtualenv.create_bootstrap_script( textwrap.dedent(after_install % (path, version))) fp = open(filename, 'w') fp.write(output) fp.close() def main(): if len(sys.argv) != 2: print >> sys.stderr, 'usage: %s version' % sys.argv[0] sys.exit(1) generate(filename, sys.argv[1]) if __name__ == '__main__': main() ================================================ FILE: scripts/go-pylons.py ================================================ #!/usr/bin/env python ## WARNING: This file is generated #!/usr/bin/env python """Create a "virtual" Python installation """ __version__ = "12.0.5" virtualenv_version = __version__ # legacy import base64 import sys import os import codecs import optparse import re import shutil import logging import tempfile import zlib import errno import glob import distutils.sysconfig from distutils.util import strtobool import struct import subprocess import tarfile if sys.version_info < (2, 6): print('ERROR: %s' % sys.exc_info()[1]) print('ERROR: this script requires Python 2.6 or greater.') sys.exit(101) try: basestring except NameError: basestring = str try: import ConfigParser except ImportError: import configparser as ConfigParser join = os.path.join py_version = 'python%s.%s' % (sys.version_info[0], sys.version_info[1]) is_jython = sys.platform.startswith('java') is_pypy = hasattr(sys, 'pypy_version_info') is_win = (sys.platform == 'win32') is_cygwin = (sys.platform == 'cygwin') is_darwin = (sys.platform == 'darwin') abiflags = getattr(sys, 'abiflags', '') user_dir = os.path.expanduser('~') if is_win: default_storage_dir = os.path.join(user_dir, 'virtualenv') else: default_storage_dir = os.path.join(user_dir, '.virtualenv') default_config_file = os.path.join(default_storage_dir, 'virtualenv.ini') if is_pypy: expected_exe = 'pypy' elif is_jython: expected_exe = 'jython' else: expected_exe = 'python' # Return a mapping of version -> Python executable # Only provided for Windows, where the information in the registry is used if not is_win: def get_installed_pythons(): return {} else: try: import winreg except ImportError: import _winreg as winreg def get_installed_pythons(): try: python_core = winreg.CreateKey(winreg.HKEY_LOCAL_MACHINE, "Software\\Python\\PythonCore") except WindowsError: # No registered Python installations return {} i = 0 versions = [] while True: try: versions.append(winreg.EnumKey(python_core, i)) i = i + 1 except WindowsError: break exes = dict() for ver in versions: try: path = winreg.QueryValue(python_core, "%s\\InstallPath" % ver) except WindowsError: continue exes[ver] = join(path, "python.exe") winreg.CloseKey(python_core) # Add the major versions # Sort the keys, then repeatedly update the major version entry # Last executable (i.e., highest version) wins with this approach for ver in sorted(exes): exes[ver[0]] = exes[ver] return exes REQUIRED_MODULES = ['os', 'posix', 'posixpath', 'nt', 'ntpath', 'genericpath', 'fnmatch', 'locale', 'encodings', 'codecs', 'stat', 'UserDict', 'readline', 'copy_reg', 'types', 're', 'sre', 'sre_parse', 'sre_constants', 'sre_compile', 'zlib'] REQUIRED_FILES = ['lib-dynload', 'config'] majver, minver = sys.version_info[:2] if majver == 2: if minver >= 6: REQUIRED_MODULES.extend(['warnings', 'linecache', '_abcoll', 'abc']) if minver >= 7: REQUIRED_MODULES.extend(['_weakrefset']) elif majver == 3: # Some extra modules are needed for Python 3, but different ones # for different versions. REQUIRED_MODULES.extend(['_abcoll', 'warnings', 'linecache', 'abc', 'io', '_weakrefset', 'copyreg', 'tempfile', 'random', '__future__', 'collections', 'keyword', 'tarfile', 'shutil', 'struct', 'copy', 'tokenize', 'token', 'functools', 'heapq', 'bisect', 'weakref', 'reprlib']) if minver >= 2: REQUIRED_FILES[-1] = 'config-%s' % majver if minver >= 3: import sysconfig platdir = sysconfig.get_config_var('PLATDIR') REQUIRED_FILES.append(platdir) # The whole list of 3.3 modules is reproduced below - the current # uncommented ones are required for 3.3 as of now, but more may be # added as 3.3 development continues. REQUIRED_MODULES.extend([ #"aifc", #"antigravity", #"argparse", #"ast", #"asynchat", #"asyncore", "base64", #"bdb", #"binhex", #"bisect", #"calendar", #"cgi", #"cgitb", #"chunk", #"cmd", #"codeop", #"code", #"colorsys", #"_compat_pickle", #"compileall", #"concurrent", #"configparser", #"contextlib", #"cProfile", #"crypt", #"csv", #"ctypes", #"curses", #"datetime", #"dbm", #"decimal", #"difflib", #"dis", #"doctest", #"dummy_threading", "_dummy_thread", #"email", #"filecmp", #"fileinput", #"formatter", #"fractions", #"ftplib", #"functools", #"getopt", #"getpass", #"gettext", #"glob", #"gzip", "hashlib", #"heapq", "hmac", #"html", #"http", #"idlelib", #"imaplib", #"imghdr", "imp", "importlib", #"inspect", #"json", #"lib2to3", #"logging", #"macpath", #"macurl2path", #"mailbox", #"mailcap", #"_markupbase", #"mimetypes", #"modulefinder", #"multiprocessing", #"netrc", #"nntplib", #"nturl2path", #"numbers", #"opcode", #"optparse", #"os2emxpath", #"pdb", #"pickle", #"pickletools", #"pipes", #"pkgutil", #"platform", #"plat-linux2", #"plistlib", #"poplib", #"pprint", #"profile", #"pstats", #"pty", #"pyclbr", #"py_compile", #"pydoc_data", #"pydoc", #"_pyio", #"queue", #"quopri", #"reprlib", "rlcompleter", #"runpy", #"sched", #"shelve", #"shlex", #"smtpd", #"smtplib", #"sndhdr", #"socket", #"socketserver", #"sqlite3", #"ssl", #"stringprep", #"string", #"_strptime", #"subprocess", #"sunau", #"symbol", #"symtable", #"sysconfig", #"tabnanny", #"telnetlib", #"test", #"textwrap", #"this", #"_threading_local", #"threading", #"timeit", #"tkinter", #"tokenize", #"token", #"traceback", #"trace", #"tty", #"turtledemo", #"turtle", #"unittest", #"urllib", #"uuid", #"uu", #"wave", #"weakref", #"webbrowser", #"wsgiref", #"xdrlib", #"xml", #"xmlrpc", #"zipfile", ]) if minver >= 4: REQUIRED_MODULES.extend([ 'operator', '_collections_abc', '_bootlocale', ]) if is_pypy: # these are needed to correctly display the exceptions that may happen # during the bootstrap REQUIRED_MODULES.extend(['traceback', 'linecache']) class Logger(object): """ Logging object for use in command-line script. Allows ranges of levels, to avoid some redundancy of displayed information. """ DEBUG = logging.DEBUG INFO = logging.INFO NOTIFY = (logging.INFO+logging.WARN)/2 WARN = WARNING = logging.WARN ERROR = logging.ERROR FATAL = logging.FATAL LEVELS = [DEBUG, INFO, NOTIFY, WARN, ERROR, FATAL] def __init__(self, consumers): self.consumers = consumers self.indent = 0 self.in_progress = None self.in_progress_hanging = False def debug(self, msg, *args, **kw): self.log(self.DEBUG, msg, *args, **kw) def info(self, msg, *args, **kw): self.log(self.INFO, msg, *args, **kw) def notify(self, msg, *args, **kw): self.log(self.NOTIFY, msg, *args, **kw) def warn(self, msg, *args, **kw): self.log(self.WARN, msg, *args, **kw) def error(self, msg, *args, **kw): self.log(self.ERROR, msg, *args, **kw) def fatal(self, msg, *args, **kw): self.log(self.FATAL, msg, *args, **kw) def log(self, level, msg, *args, **kw): if args: if kw: raise TypeError( "You may give positional or keyword arguments, not both") args = args or kw rendered = None for consumer_level, consumer in self.consumers: if self.level_matches(level, consumer_level): if (self.in_progress_hanging and consumer in (sys.stdout, sys.stderr)): self.in_progress_hanging = False sys.stdout.write('\n') sys.stdout.flush() if rendered is None: if args: rendered = msg % args else: rendered = msg rendered = ' '*self.indent + rendered if hasattr(consumer, 'write'): consumer.write(rendered+'\n') else: consumer(rendered) def start_progress(self, msg): assert not self.in_progress, ( "Tried to start_progress(%r) while in_progress %r" % (msg, self.in_progress)) if self.level_matches(self.NOTIFY, self._stdout_level()): sys.stdout.write(msg) sys.stdout.flush() self.in_progress_hanging = True else: self.in_progress_hanging = False self.in_progress = msg def end_progress(self, msg='done.'): assert self.in_progress, ( "Tried to end_progress without start_progress") if self.stdout_level_matches(self.NOTIFY): if not self.in_progress_hanging: # Some message has been printed out since start_progress sys.stdout.write('...' + self.in_progress + msg + '\n') sys.stdout.flush() else: sys.stdout.write(msg + '\n') sys.stdout.flush() self.in_progress = None self.in_progress_hanging = False def show_progress(self): """If we are in a progress scope, and no log messages have been shown, write out another '.'""" if self.in_progress_hanging: sys.stdout.write('.') sys.stdout.flush() def stdout_level_matches(self, level): """Returns true if a message at this level will go to stdout""" return self.level_matches(level, self._stdout_level()) def _stdout_level(self): """Returns the level that stdout runs at""" for level, consumer in self.consumers: if consumer is sys.stdout: return level return self.FATAL def level_matches(self, level, consumer_level): """ >>> l = Logger([]) >>> l.level_matches(3, 4) False >>> l.level_matches(3, 2) True >>> l.level_matches(slice(None, 3), 3) False >>> l.level_matches(slice(None, 3), 2) True >>> l.level_matches(slice(1, 3), 1) True >>> l.level_matches(slice(2, 3), 1) False """ if isinstance(level, slice): start, stop = level.start, level.stop if start is not None and start > consumer_level: return False if stop is not None and stop <= consumer_level: return False return True else: return level >= consumer_level #@classmethod def level_for_integer(cls, level): levels = cls.LEVELS if level < 0: return levels[0] if level >= len(levels): return levels[-1] return levels[level] level_for_integer = classmethod(level_for_integer) # create a silent logger just to prevent this from being undefined # will be overridden with requested verbosity main() is called. logger = Logger([(Logger.LEVELS[-1], sys.stdout)]) def mkdir(path): if not os.path.exists(path): logger.info('Creating %s', path) os.makedirs(path) else: logger.info('Directory %s already exists', path) def copyfileordir(src, dest, symlink=True): if os.path.isdir(src): shutil.copytree(src, dest, symlink) else: shutil.copy2(src, dest) def copyfile(src, dest, symlink=True): if not os.path.exists(src): # Some bad symlink in the src logger.warn('Cannot find file %s (bad symlink)', src) return if os.path.exists(dest): logger.debug('File %s already exists', dest) return if not os.path.exists(os.path.dirname(dest)): logger.info('Creating parent directories for %s', os.path.dirname(dest)) os.makedirs(os.path.dirname(dest)) if not os.path.islink(src): srcpath = os.path.abspath(src) else: srcpath = os.readlink(src) if symlink and hasattr(os, 'symlink') and not is_win: logger.info('Symlinking %s', dest) try: os.symlink(srcpath, dest) except (OSError, NotImplementedError): logger.info('Symlinking failed, copying to %s', dest) copyfileordir(src, dest, symlink) else: logger.info('Copying to %s', dest) copyfileordir(src, dest, symlink) def writefile(dest, content, overwrite=True): if not os.path.exists(dest): logger.info('Writing %s', dest) f = open(dest, 'wb') f.write(content.encode('utf-8')) f.close() return else: f = open(dest, 'rb') c = f.read() f.close() if c != content.encode("utf-8"): if not overwrite: logger.notify('File %s exists with different content; not overwriting', dest) return logger.notify('Overwriting %s with new content', dest) f = open(dest, 'wb') f.write(content.encode('utf-8')) f.close() else: logger.info('Content %s already in place', dest) def rmtree(dir): if os.path.exists(dir): logger.notify('Deleting tree %s', dir) shutil.rmtree(dir) else: logger.info('Do not need to delete %s; already gone', dir) def make_exe(fn): if hasattr(os, 'chmod'): oldmode = os.stat(fn).st_mode & 0xFFF # 0o7777 newmode = (oldmode | 0x16D) & 0xFFF # 0o555, 0o7777 os.chmod(fn, newmode) logger.info('Changed mode of %s to %s', fn, oct(newmode)) def _find_file(filename, dirs): for dir in reversed(dirs): files = glob.glob(os.path.join(dir, filename)) if files and os.path.isfile(files[0]): return True, files[0] return False, filename def file_search_dirs(): here = os.path.dirname(os.path.abspath(__file__)) dirs = ['.', here, join(here, 'virtualenv_support')] if os.path.splitext(os.path.dirname(__file__))[0] != 'virtualenv': # Probably some boot script; just in case virtualenv is installed... try: import virtualenv except ImportError: pass else: dirs.append(os.path.join(os.path.dirname(virtualenv.__file__), 'virtualenv_support')) return [d for d in dirs if os.path.isdir(d)] class UpdatingDefaultsHelpFormatter(optparse.IndentedHelpFormatter): """ Custom help formatter for use in ConfigOptionParser that updates the defaults before expanding them, allowing them to show up correctly in the help listing """ def expand_default(self, option): if self.parser is not None: self.parser.update_defaults(self.parser.defaults) return optparse.IndentedHelpFormatter.expand_default(self, option) class ConfigOptionParser(optparse.OptionParser): """ Custom option parser which updates its defaults by checking the configuration files and environmental variables """ def __init__(self, *args, **kwargs): self.config = ConfigParser.RawConfigParser() self.files = self.get_config_files() self.config.read(self.files) optparse.OptionParser.__init__(self, *args, **kwargs) def get_config_files(self): config_file = os.environ.get('VIRTUALENV_CONFIG_FILE', False) if config_file and os.path.exists(config_file): return [config_file] return [default_config_file] def update_defaults(self, defaults): """ Updates the given defaults with values from the config files and the environ. Does a little special handling for certain types of options (lists). """ # Then go and look for the other sources of configuration: config = {} # 1. config files config.update(dict(self.get_config_section('virtualenv'))) # 2. environmental variables config.update(dict(self.get_environ_vars())) # Then set the options with those values for key, val in config.items(): key = key.replace('_', '-') if not key.startswith('--'): key = '--%s' % key # only prefer long opts option = self.get_option(key) if option is not None: # ignore empty values if not val: continue # handle multiline configs if option.action == 'append': val = val.split() else: option.nargs = 1 if option.action == 'store_false': val = not strtobool(val) elif option.action in ('store_true', 'count'): val = strtobool(val) try: val = option.convert_value(key, val) except optparse.OptionValueError: e = sys.exc_info()[1] print("An error occurred during configuration: %s" % e) sys.exit(3) defaults[option.dest] = val return defaults def get_config_section(self, name): """ Get a section of a configuration """ if self.config.has_section(name): return self.config.items(name) return [] def get_environ_vars(self, prefix='VIRTUALENV_'): """ Returns a generator with all environmental vars with prefix VIRTUALENV """ for key, val in os.environ.items(): if key.startswith(prefix): yield (key.replace(prefix, '').lower(), val) def get_default_values(self): """ Overridding to make updating the defaults after instantiation of the option parser possible, update_defaults() does the dirty work. """ if not self.process_default_values: # Old, pre-Optik 1.5 behaviour. return optparse.Values(self.defaults) defaults = self.update_defaults(self.defaults.copy()) # ours for option in self._get_all_options(): default = defaults.get(option.dest) if isinstance(default, basestring): opt_str = option.get_opt_string() defaults[option.dest] = option.check_value(opt_str, default) return optparse.Values(defaults) def main(): parser = ConfigOptionParser( version=virtualenv_version, usage="%prog [OPTIONS] DEST_DIR", formatter=UpdatingDefaultsHelpFormatter()) parser.add_option( '-v', '--verbose', action='count', dest='verbose', default=0, help="Increase verbosity.") parser.add_option( '-q', '--quiet', action='count', dest='quiet', default=0, help='Decrease verbosity.') parser.add_option( '-p', '--python', dest='python', metavar='PYTHON_EXE', help='The Python interpreter to use, e.g., --python=python2.5 will use the python2.5 ' 'interpreter to create the new environment. The default is the interpreter that ' 'virtualenv was installed with (%s)' % sys.executable) parser.add_option( '--clear', dest='clear', action='store_true', help="Clear out the non-root install and start from scratch.") parser.set_defaults(system_site_packages=False) parser.add_option( '--no-site-packages', dest='system_site_packages', action='store_false', help="DEPRECATED. Retained only for backward compatibility. " "Not having access to global site-packages is now the default behavior.") parser.add_option( '--system-site-packages', dest='system_site_packages', action='store_true', help="Give the virtual environment access to the global site-packages.") parser.add_option( '--always-copy', dest='symlink', action='store_false', default=True, help="Always copy files rather than symlinking.") parser.add_option( '--unzip-setuptools', dest='unzip_setuptools', action='store_true', help="Unzip Setuptools when installing it.") parser.add_option( '--relocatable', dest='relocatable', action='store_true', help='Make an EXISTING virtualenv environment relocatable. ' 'This fixes up scripts and makes all .pth files relative.') parser.add_option( '--no-setuptools', dest='no_setuptools', action='store_true', help='Do not install setuptools (or pip) in the new virtualenv.') parser.add_option( '--no-pip', dest='no_pip', action='store_true', help='Do not install pip in the new virtualenv.') default_search_dirs = file_search_dirs() parser.add_option( '--extra-search-dir', dest="search_dirs", action="append", metavar='DIR', default=default_search_dirs, help="Directory to look for setuptools/pip distributions in. " "This option can be used multiple times.") parser.add_option( '--never-download', dest="never_download", action="store_true", default=True, help="DEPRECATED. Retained only for backward compatibility. This option has no effect. " "Virtualenv never downloads pip or setuptools.") parser.add_option( '--prompt', dest='prompt', help='Provides an alternative prompt prefix for this environment.') parser.add_option( '--setuptools', dest='setuptools', action='store_true', help="DEPRECATED. Retained only for backward compatibility. This option has no effect.") parser.add_option( '--distribute', dest='distribute', action='store_true', help="DEPRECATED. Retained only for backward compatibility. This option has no effect.") if 'extend_parser' in globals(): extend_parser(parser) options, args = parser.parse_args() global logger if 'adjust_options' in globals(): adjust_options(options, args) verbosity = options.verbose - options.quiet logger = Logger([(Logger.level_for_integer(2 - verbosity), sys.stdout)]) if options.python and not os.environ.get('VIRTUALENV_INTERPRETER_RUNNING'): env = os.environ.copy() interpreter = resolve_interpreter(options.python) if interpreter == sys.executable: logger.warn('Already using interpreter %s' % interpreter) else: logger.notify('Running virtualenv with interpreter %s' % interpreter) env['VIRTUALENV_INTERPRETER_RUNNING'] = 'true' file = __file__ if file.endswith('.pyc'): file = file[:-1] popen = subprocess.Popen([interpreter, file] + sys.argv[1:], env=env) raise SystemExit(popen.wait()) if not args: print('You must provide a DEST_DIR') parser.print_help() sys.exit(2) if len(args) > 1: print('There must be only one argument: DEST_DIR (you gave %s)' % ( ' '.join(args))) parser.print_help() sys.exit(2) home_dir = args[0] if os.environ.get('WORKING_ENV'): logger.fatal('ERROR: you cannot run virtualenv while in a workingenv') logger.fatal('Please deactivate your workingenv, then re-run this script') sys.exit(3) if 'PYTHONHOME' in os.environ: logger.warn('PYTHONHOME is set. You *must* activate the virtualenv before using it') del os.environ['PYTHONHOME'] if options.relocatable: make_environment_relocatable(home_dir) return if not options.never_download: logger.warn('The --never-download option is for backward compatibility only.') logger.warn('Setting it to false is no longer supported, and will be ignored.') create_environment(home_dir, site_packages=options.system_site_packages, clear=options.clear, unzip_setuptools=options.unzip_setuptools, prompt=options.prompt, search_dirs=options.search_dirs, never_download=True, no_setuptools=options.no_setuptools, no_pip=options.no_pip, symlink=options.symlink) if 'after_install' in globals(): after_install(options, home_dir) def call_subprocess(cmd, show_stdout=True, filter_stdout=None, cwd=None, raise_on_returncode=True, extra_env=None, remove_from_env=None): cmd_parts = [] for part in cmd: if len(part) > 45: part = part[:20]+"..."+part[-20:] if ' ' in part or '\n' in part or '"' in part or "'" in part: part = '"%s"' % part.replace('"', '\\"') if hasattr(part, 'decode'): try: part = part.decode(sys.getdefaultencoding()) except UnicodeDecodeError: part = part.decode(sys.getfilesystemencoding()) cmd_parts.append(part) cmd_desc = ' '.join(cmd_parts) if show_stdout: stdout = None else: stdout = subprocess.PIPE logger.debug("Running command %s" % cmd_desc) if extra_env or remove_from_env: env = os.environ.copy() if extra_env: env.update(extra_env) if remove_from_env: for varname in remove_from_env: env.pop(varname, None) else: env = None try: proc = subprocess.Popen( cmd, stderr=subprocess.STDOUT, stdin=None, stdout=stdout, cwd=cwd, env=env) except Exception: e = sys.exc_info()[1] logger.fatal( "Error %s while executing command %s" % (e, cmd_desc)) raise all_output = [] if stdout is not None: stdout = proc.stdout encoding = sys.getdefaultencoding() fs_encoding = sys.getfilesystemencoding() while 1: line = stdout.readline() try: line = line.decode(encoding) except UnicodeDecodeError: line = line.decode(fs_encoding) if not line: break line = line.rstrip() all_output.append(line) if filter_stdout: level = filter_stdout(line) if isinstance(level, tuple): level, line = level logger.log(level, line) if not logger.stdout_level_matches(level): logger.show_progress() else: logger.info(line) else: proc.communicate() proc.wait() if proc.returncode: if raise_on_returncode: if all_output: logger.notify('Complete output from command %s:' % cmd_desc) logger.notify('\n'.join(all_output) + '\n----------------------------------------') raise OSError( "Command %s failed with error code %s" % (cmd_desc, proc.returncode)) else: logger.warn( "Command %s had error code %s" % (cmd_desc, proc.returncode)) def filter_install_output(line): if line.strip().startswith('running'): return Logger.INFO return Logger.DEBUG def find_wheels(projects, search_dirs): """Find wheels from which we can import PROJECTS. Scan through SEARCH_DIRS for a wheel for each PROJECT in turn. Return a list of the first wheel found for each PROJECT """ wheels = [] # Look through SEARCH_DIRS for the first suitable wheel. Don't bother # about version checking here, as this is simply to get something we can # then use to install the correct version. for project in projects: for dirname in search_dirs: # This relies on only having "universal" wheels available. # The pattern could be tightened to require -py2.py3-none-any.whl. files = glob.glob(os.path.join(dirname, project + '-*.whl')) if files: wheels.append(os.path.abspath(files[0])) break else: # We're out of luck, so quit with a suitable error logger.fatal('Cannot find a wheel for %s' % (project,)) return wheels def install_wheel(project_names, py_executable, search_dirs=None): if search_dirs is None: search_dirs = file_search_dirs() wheels = find_wheels(['setuptools', 'pip'], search_dirs) pythonpath = os.pathsep.join(wheels) findlinks = ' '.join(search_dirs) cmd = [ py_executable, '-c', 'import sys, pip; sys.exit(pip.main(["install", "--ignore-installed"] + sys.argv[1:]))', ] + project_names logger.start_progress('Installing %s...' % (', '.join(project_names))) logger.indent += 2 try: call_subprocess(cmd, show_stdout=False, extra_env = { 'PYTHONPATH': pythonpath, 'PIP_FIND_LINKS': findlinks, 'PIP_USE_WHEEL': '1', 'PIP_PRE': '1', 'PIP_NO_INDEX': '1' } ) finally: logger.indent -= 2 logger.end_progress() def create_environment(home_dir, site_packages=False, clear=False, unzip_setuptools=False, prompt=None, search_dirs=None, never_download=False, no_setuptools=False, no_pip=False, symlink=True): """ Creates a new environment in ``home_dir``. If ``site_packages`` is true, then the global ``site-packages/`` directory will be on the path. If ``clear`` is true (default False) then the environment will first be cleared. """ home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) py_executable = os.path.abspath(install_python( home_dir, lib_dir, inc_dir, bin_dir, site_packages=site_packages, clear=clear, symlink=symlink)) install_distutils(home_dir) if not no_setuptools: to_install = ['setuptools'] if not no_pip: to_install.append('pip') install_wheel(to_install, py_executable, search_dirs) install_activate(home_dir, bin_dir, prompt) def is_executable_file(fpath): return os.path.isfile(fpath) and os.access(fpath, os.X_OK) def path_locations(home_dir): """Return the path locations for the environment (where libraries are, where scripts go, etc)""" # XXX: We'd use distutils.sysconfig.get_python_inc/lib but its # prefix arg is broken: http://bugs.python.org/issue3386 if is_win: # Windows has lots of problems with executables with spaces in # the name; this function will remove them (using the ~1 # format): mkdir(home_dir) if ' ' in home_dir: import ctypes GetShortPathName = ctypes.windll.kernel32.GetShortPathNameW size = max(len(home_dir)+1, 256) buf = ctypes.create_unicode_buffer(size) try: u = unicode except NameError: u = str ret = GetShortPathName(u(home_dir), buf, size) if not ret: print('Error: the path "%s" has a space in it' % home_dir) print('We could not determine the short pathname for it.') print('Exiting.') sys.exit(3) home_dir = str(buf.value) lib_dir = join(home_dir, 'Lib') inc_dir = join(home_dir, 'Include') bin_dir = join(home_dir, 'Scripts') if is_jython: lib_dir = join(home_dir, 'Lib') inc_dir = join(home_dir, 'Include') bin_dir = join(home_dir, 'bin') elif is_pypy: lib_dir = home_dir inc_dir = join(home_dir, 'include') bin_dir = join(home_dir, 'bin') elif not is_win: lib_dir = join(home_dir, 'lib', py_version) multiarch_exec = '/usr/bin/multiarch-platform' if is_executable_file(multiarch_exec): # In Mageia (2) and Mandriva distros the include dir must be like: # virtualenv/include/multiarch-x86_64-linux/python2.7 # instead of being virtualenv/include/python2.7 p = subprocess.Popen(multiarch_exec, stdout=subprocess.PIPE, stderr=subprocess.PIPE) stdout, stderr = p.communicate() # stdout.strip is needed to remove newline character inc_dir = join(home_dir, 'include', stdout.strip(), py_version + abiflags) else: inc_dir = join(home_dir, 'include', py_version + abiflags) bin_dir = join(home_dir, 'bin') return home_dir, lib_dir, inc_dir, bin_dir def change_prefix(filename, dst_prefix): prefixes = [sys.prefix] if is_darwin: prefixes.extend(( os.path.join("/Library/Python", sys.version[:3], "site-packages"), os.path.join(sys.prefix, "Extras", "lib", "python"), os.path.join("~", "Library", "Python", sys.version[:3], "site-packages"), # Python 2.6 no-frameworks os.path.join("~", ".local", "lib","python", sys.version[:3], "site-packages"), # System Python 2.7 on OSX Mountain Lion os.path.join("~", "Library", "Python", sys.version[:3], "lib", "python", "site-packages"))) if hasattr(sys, 'real_prefix'): prefixes.append(sys.real_prefix) if hasattr(sys, 'base_prefix'): prefixes.append(sys.base_prefix) prefixes = list(map(os.path.expanduser, prefixes)) prefixes = list(map(os.path.abspath, prefixes)) # Check longer prefixes first so we don't split in the middle of a filename prefixes = sorted(prefixes, key=len, reverse=True) filename = os.path.abspath(filename) for src_prefix in prefixes: if filename.startswith(src_prefix): _, relpath = filename.split(src_prefix, 1) if src_prefix != os.sep: # sys.prefix == "/" assert relpath[0] == os.sep relpath = relpath[1:] return join(dst_prefix, relpath) assert False, "Filename %s does not start with any of these prefixes: %s" % \ (filename, prefixes) def copy_required_modules(dst_prefix, symlink): import imp # If we are running under -p, we need to remove the current # directory from sys.path temporarily here, so that we # definitely get the modules from the site directory of # the interpreter we are running under, not the one # virtualenv.py is installed under (which might lead to py2/py3 # incompatibility issues) _prev_sys_path = sys.path if os.environ.get('VIRTUALENV_INTERPRETER_RUNNING'): sys.path = sys.path[1:] try: for modname in REQUIRED_MODULES: if modname in sys.builtin_module_names: logger.info("Ignoring built-in bootstrap module: %s" % modname) continue try: f, filename, _ = imp.find_module(modname) except ImportError: logger.info("Cannot import bootstrap module: %s" % modname) else: if f is not None: f.close() # special-case custom readline.so on OS X, but not for pypy: if modname == 'readline' and sys.platform == 'darwin' and not ( is_pypy or filename.endswith(join('lib-dynload', 'readline.so'))): dst_filename = join(dst_prefix, 'lib', 'python%s' % sys.version[:3], 'readline.so') elif modname == 'readline' and sys.platform == 'win32': # special-case for Windows, where readline is not a # standard module, though it may have been installed in # site-packages by a third-party package pass else: dst_filename = change_prefix(filename, dst_prefix) copyfile(filename, dst_filename, symlink) if filename.endswith('.pyc'): pyfile = filename[:-1] if os.path.exists(pyfile): copyfile(pyfile, dst_filename[:-1], symlink) finally: sys.path = _prev_sys_path def subst_path(prefix_path, prefix, home_dir): prefix_path = os.path.normpath(prefix_path) prefix = os.path.normpath(prefix) home_dir = os.path.normpath(home_dir) if not prefix_path.startswith(prefix): logger.warn('Path not in prefix %r %r', prefix_path, prefix) return return prefix_path.replace(prefix, home_dir, 1) def install_python(home_dir, lib_dir, inc_dir, bin_dir, site_packages, clear, symlink=True): """Install just the base environment, no distutils patches etc""" if sys.executable.startswith(bin_dir): print('Please use the *system* python to run this script') return if clear: rmtree(lib_dir) ## FIXME: why not delete it? ## Maybe it should delete everything with #!/path/to/venv/python in it logger.notify('Not deleting %s', bin_dir) if hasattr(sys, 'real_prefix'): logger.notify('Using real prefix %r' % sys.real_prefix) prefix = sys.real_prefix elif hasattr(sys, 'base_prefix'): logger.notify('Using base prefix %r' % sys.base_prefix) prefix = sys.base_prefix else: prefix = sys.prefix mkdir(lib_dir) fix_lib64(lib_dir, symlink) stdlib_dirs = [os.path.dirname(os.__file__)] if is_win: stdlib_dirs.append(join(os.path.dirname(stdlib_dirs[0]), 'DLLs')) elif is_darwin: stdlib_dirs.append(join(stdlib_dirs[0], 'site-packages')) if hasattr(os, 'symlink'): logger.info('Symlinking Python bootstrap modules') else: logger.info('Copying Python bootstrap modules') logger.indent += 2 try: # copy required files... for stdlib_dir in stdlib_dirs: if not os.path.isdir(stdlib_dir): continue for fn in os.listdir(stdlib_dir): bn = os.path.splitext(fn)[0] if fn != 'site-packages' and bn in REQUIRED_FILES: copyfile(join(stdlib_dir, fn), join(lib_dir, fn), symlink) # ...and modules copy_required_modules(home_dir, symlink) finally: logger.indent -= 2 mkdir(join(lib_dir, 'site-packages')) import site site_filename = site.__file__ if site_filename.endswith('.pyc'): site_filename = site_filename[:-1] elif site_filename.endswith('$py.class'): site_filename = site_filename.replace('$py.class', '.py') site_filename_dst = change_prefix(site_filename, home_dir) site_dir = os.path.dirname(site_filename_dst) writefile(site_filename_dst, SITE_PY) writefile(join(site_dir, 'orig-prefix.txt'), prefix) site_packages_filename = join(site_dir, 'no-global-site-packages.txt') if not site_packages: writefile(site_packages_filename, '') if is_pypy or is_win: stdinc_dir = join(prefix, 'include') else: stdinc_dir = join(prefix, 'include', py_version + abiflags) if os.path.exists(stdinc_dir): copyfile(stdinc_dir, inc_dir, symlink) else: logger.debug('No include dir %s' % stdinc_dir) platinc_dir = distutils.sysconfig.get_python_inc(plat_specific=1) if platinc_dir != stdinc_dir: platinc_dest = distutils.sysconfig.get_python_inc( plat_specific=1, prefix=home_dir) if platinc_dir == platinc_dest: # Do platinc_dest manually due to a CPython bug; # not http://bugs.python.org/issue3386 but a close cousin platinc_dest = subst_path(platinc_dir, prefix, home_dir) if platinc_dest: # PyPy's stdinc_dir and prefix are relative to the original binary # (traversing virtualenvs), whereas the platinc_dir is relative to # the inner virtualenv and ignores the prefix argument. # This seems more evolved than designed. copyfile(platinc_dir, platinc_dest, symlink) # pypy never uses exec_prefix, just ignore it if sys.exec_prefix != prefix and not is_pypy: if is_win: exec_dir = join(sys.exec_prefix, 'lib') elif is_jython: exec_dir = join(sys.exec_prefix, 'Lib') else: exec_dir = join(sys.exec_prefix, 'lib', py_version) for fn in os.listdir(exec_dir): copyfile(join(exec_dir, fn), join(lib_dir, fn), symlink) if is_jython: # Jython has either jython-dev.jar and javalib/ dir, or just # jython.jar for name in 'jython-dev.jar', 'javalib', 'jython.jar': src = join(prefix, name) if os.path.exists(src): copyfile(src, join(home_dir, name), symlink) # XXX: registry should always exist after Jython 2.5rc1 src = join(prefix, 'registry') if os.path.exists(src): copyfile(src, join(home_dir, 'registry'), symlink=False) copyfile(join(prefix, 'cachedir'), join(home_dir, 'cachedir'), symlink=False) mkdir(bin_dir) py_executable = join(bin_dir, os.path.basename(sys.executable)) if 'Python.framework' in prefix: # OS X framework builds cause validation to break # https://github.com/pypa/virtualenv/issues/322 if os.environ.get('__PYVENV_LAUNCHER__'): del os.environ["__PYVENV_LAUNCHER__"] if re.search(r'/Python(?:-32|-64)*$', py_executable): # The name of the python executable is not quite what # we want, rename it. py_executable = os.path.join( os.path.dirname(py_executable), 'python') logger.notify('New %s executable in %s', expected_exe, py_executable) pcbuild_dir = os.path.dirname(sys.executable) pyd_pth = os.path.join(lib_dir, 'site-packages', 'virtualenv_builddir_pyd.pth') if is_win and os.path.exists(os.path.join(pcbuild_dir, 'build.bat')): logger.notify('Detected python running from build directory %s', pcbuild_dir) logger.notify('Writing .pth file linking to build directory for *.pyd files') writefile(pyd_pth, pcbuild_dir) else: pcbuild_dir = None if os.path.exists(pyd_pth): logger.info('Deleting %s (not Windows env or not build directory python)' % pyd_pth) os.unlink(pyd_pth) if sys.executable != py_executable: ## FIXME: could I just hard link? executable = sys.executable shutil.copyfile(executable, py_executable) make_exe(py_executable) if is_win or is_cygwin: pythonw = os.path.join(os.path.dirname(sys.executable), 'pythonw.exe') if os.path.exists(pythonw): logger.info('Also created pythonw.exe') shutil.copyfile(pythonw, os.path.join(os.path.dirname(py_executable), 'pythonw.exe')) python_d = os.path.join(os.path.dirname(sys.executable), 'python_d.exe') python_d_dest = os.path.join(os.path.dirname(py_executable), 'python_d.exe') if os.path.exists(python_d): logger.info('Also created python_d.exe') shutil.copyfile(python_d, python_d_dest) elif os.path.exists(python_d_dest): logger.info('Removed python_d.exe as it is no longer at the source') os.unlink(python_d_dest) # we need to copy the DLL to enforce that windows will load the correct one. # may not exist if we are cygwin. py_executable_dll = 'python%s%s.dll' % ( sys.version_info[0], sys.version_info[1]) py_executable_dll_d = 'python%s%s_d.dll' % ( sys.version_info[0], sys.version_info[1]) pythondll = os.path.join(os.path.dirname(sys.executable), py_executable_dll) pythondll_d = os.path.join(os.path.dirname(sys.executable), py_executable_dll_d) pythondll_d_dest = os.path.join(os.path.dirname(py_executable), py_executable_dll_d) if os.path.exists(pythondll): logger.info('Also created %s' % py_executable_dll) shutil.copyfile(pythondll, os.path.join(os.path.dirname(py_executable), py_executable_dll)) if os.path.exists(pythondll_d): logger.info('Also created %s' % py_executable_dll_d) shutil.copyfile(pythondll_d, pythondll_d_dest) elif os.path.exists(pythondll_d_dest): logger.info('Removed %s as the source does not exist' % pythondll_d_dest) os.unlink(pythondll_d_dest) if is_pypy: # make a symlink python --> pypy-c python_executable = os.path.join(os.path.dirname(py_executable), 'python') if sys.platform in ('win32', 'cygwin'): python_executable += '.exe' logger.info('Also created executable %s' % python_executable) copyfile(py_executable, python_executable, symlink) if is_win: for name in ['libexpat.dll', 'libpypy.dll', 'libpypy-c.dll', 'libeay32.dll', 'ssleay32.dll', 'sqlite3.dll', 'tcl85.dll', 'tk85.dll']: src = join(prefix, name) if os.path.exists(src): copyfile(src, join(bin_dir, name), symlink) for d in sys.path: if d.endswith('lib_pypy'): break else: logger.fatal('Could not find lib_pypy in sys.path') raise SystemExit(3) logger.info('Copying lib_pypy') copyfile(d, os.path.join(home_dir, 'lib_pypy'), symlink) if os.path.splitext(os.path.basename(py_executable))[0] != expected_exe: secondary_exe = os.path.join(os.path.dirname(py_executable), expected_exe) py_executable_ext = os.path.splitext(py_executable)[1] if py_executable_ext.lower() == '.exe': # python2.4 gives an extension of '.4' :P secondary_exe += py_executable_ext if os.path.exists(secondary_exe): logger.warn('Not overwriting existing %s script %s (you must use %s)' % (expected_exe, secondary_exe, py_executable)) else: logger.notify('Also creating executable in %s' % secondary_exe) shutil.copyfile(sys.executable, secondary_exe) make_exe(secondary_exe) if '.framework' in prefix: if 'Python.framework' in prefix: logger.debug('MacOSX Python framework detected') # Make sure we use the embedded interpreter inside # the framework, even if sys.executable points to # the stub executable in ${sys.prefix}/bin # See http://groups.google.com/group/python-virtualenv/ # browse_thread/thread/17cab2f85da75951 original_python = os.path.join( prefix, 'Resources/Python.app/Contents/MacOS/Python') if 'EPD' in prefix: logger.debug('EPD framework detected') original_python = os.path.join(prefix, 'bin/python') shutil.copy(original_python, py_executable) # Copy the framework's dylib into the virtual # environment virtual_lib = os.path.join(home_dir, '.Python') if os.path.exists(virtual_lib): os.unlink(virtual_lib) copyfile( os.path.join(prefix, 'Python'), virtual_lib, symlink) # And then change the install_name of the copied python executable try: mach_o_change(py_executable, os.path.join(prefix, 'Python'), '@executable_path/../.Python') except: e = sys.exc_info()[1] logger.warn("Could not call mach_o_change: %s. " "Trying to call install_name_tool instead." % e) try: call_subprocess( ["install_name_tool", "-change", os.path.join(prefix, 'Python'), '@executable_path/../.Python', py_executable]) except: logger.fatal("Could not call install_name_tool -- you must " "have Apple's development tools installed") raise if not is_win: # Ensure that 'python', 'pythonX' and 'pythonX.Y' all exist py_exe_version_major = 'python%s' % sys.version_info[0] py_exe_version_major_minor = 'python%s.%s' % ( sys.version_info[0], sys.version_info[1]) py_exe_no_version = 'python' required_symlinks = [ py_exe_no_version, py_exe_version_major, py_exe_version_major_minor ] py_executable_base = os.path.basename(py_executable) if py_executable_base in required_symlinks: # Don't try to symlink to yourself. required_symlinks.remove(py_executable_base) for pth in required_symlinks: full_pth = join(bin_dir, pth) if os.path.exists(full_pth): os.unlink(full_pth) if symlink: os.symlink(py_executable_base, full_pth) else: copyfile(py_executable, full_pth, symlink) if is_win and ' ' in py_executable: # There's a bug with subprocess on Windows when using a first # argument that has a space in it. Instead we have to quote # the value: py_executable = '"%s"' % py_executable # NOTE: keep this check as one line, cmd.exe doesn't cope with line breaks cmd = [py_executable, '-c', 'import sys;out=sys.stdout;' 'getattr(out, "buffer", out).write(sys.prefix.encode("utf-8"))'] logger.info('Testing executable with %s %s "%s"' % tuple(cmd)) try: proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) proc_stdout, proc_stderr = proc.communicate() except OSError: e = sys.exc_info()[1] if e.errno == errno.EACCES: logger.fatal('ERROR: The executable %s could not be run: %s' % (py_executable, e)) sys.exit(100) else: raise e proc_stdout = proc_stdout.strip().decode("utf-8") proc_stdout = os.path.normcase(os.path.abspath(proc_stdout)) norm_home_dir = os.path.normcase(os.path.abspath(home_dir)) if hasattr(norm_home_dir, 'decode'): norm_home_dir = norm_home_dir.decode(sys.getfilesystemencoding()) if proc_stdout != norm_home_dir: logger.fatal( 'ERROR: The executable %s is not functioning' % py_executable) logger.fatal( 'ERROR: It thinks sys.prefix is %r (should be %r)' % (proc_stdout, norm_home_dir)) logger.fatal( 'ERROR: virtualenv is not compatible with this system or executable') if is_win: logger.fatal( 'Note: some Windows users have reported this error when they ' 'installed Python for "Only this user" or have multiple ' 'versions of Python installed. Copying the appropriate ' 'PythonXX.dll to the virtualenv Scripts/ directory may fix ' 'this problem.') sys.exit(100) else: logger.info('Got sys.prefix result: %r' % proc_stdout) pydistutils = os.path.expanduser('~/.pydistutils.cfg') if os.path.exists(pydistutils): logger.notify('Please make sure you remove any previous custom paths from ' 'your %s file.' % pydistutils) ## FIXME: really this should be calculated earlier fix_local_scheme(home_dir, symlink) if site_packages: if os.path.exists(site_packages_filename): logger.info('Deleting %s' % site_packages_filename) os.unlink(site_packages_filename) return py_executable def install_activate(home_dir, bin_dir, prompt=None): home_dir = os.path.abspath(home_dir) if is_win or is_jython and os._name == 'nt': files = { 'activate.bat': ACTIVATE_BAT, 'deactivate.bat': DEACTIVATE_BAT, 'activate.ps1': ACTIVATE_PS, } # MSYS needs paths of the form /c/path/to/file drive, tail = os.path.splitdrive(home_dir.replace(os.sep, '/')) home_dir_msys = (drive and "/%s%s" or "%s%s") % (drive[:1], tail) # Run-time conditional enables (basic) Cygwin compatibility home_dir_sh = ("""$(if [ "$OSTYPE" "==" "cygwin" ]; then cygpath -u '%s'; else echo '%s'; fi;)""" % (home_dir, home_dir_msys)) files['activate'] = ACTIVATE_SH.replace('__VIRTUAL_ENV__', home_dir_sh) else: files = {'activate': ACTIVATE_SH} # suppling activate.fish in addition to, not instead of, the # bash script support. files['activate.fish'] = ACTIVATE_FISH # same for csh/tcsh support... files['activate.csh'] = ACTIVATE_CSH files['activate_this.py'] = ACTIVATE_THIS if hasattr(home_dir, 'decode'): home_dir = home_dir.decode(sys.getfilesystemencoding()) vname = os.path.basename(home_dir) for name, content in files.items(): content = content.replace('__VIRTUAL_PROMPT__', prompt or '') content = content.replace('__VIRTUAL_WINPROMPT__', prompt or '(%s)' % vname) content = content.replace('__VIRTUAL_ENV__', home_dir) content = content.replace('__VIRTUAL_NAME__', vname) content = content.replace('__BIN_NAME__', os.path.basename(bin_dir)) writefile(os.path.join(bin_dir, name), content) def install_distutils(home_dir): distutils_path = change_prefix(distutils.__path__[0], home_dir) mkdir(distutils_path) ## FIXME: maybe this prefix setting should only be put in place if ## there's a local distutils.cfg with a prefix setting? home_dir = os.path.abspath(home_dir) ## FIXME: this is breaking things, removing for now: #distutils_cfg = DISTUTILS_CFG + "\n[install]\nprefix=%s\n" % home_dir writefile(os.path.join(distutils_path, '__init__.py'), DISTUTILS_INIT) writefile(os.path.join(distutils_path, 'distutils.cfg'), DISTUTILS_CFG, overwrite=False) def fix_local_scheme(home_dir, symlink=True): """ Platforms that use the "posix_local" install scheme (like Ubuntu with Python 2.7) need to be given an additional "local" location, sigh. """ try: import sysconfig except ImportError: pass else: if sysconfig._get_default_scheme() == 'posix_local': local_path = os.path.join(home_dir, 'local') if not os.path.exists(local_path): os.mkdir(local_path) for subdir_name in os.listdir(home_dir): if subdir_name == 'local': continue copyfile(os.path.abspath(os.path.join(home_dir, subdir_name)), \ os.path.join(local_path, subdir_name), symlink) def fix_lib64(lib_dir, symlink=True): """ Some platforms (particularly Gentoo on x64) put things in lib64/pythonX.Y instead of lib/pythonX.Y. If this is such a platform we'll just create a symlink so lib64 points to lib """ if [p for p in distutils.sysconfig.get_config_vars().values() if isinstance(p, basestring) and 'lib64' in p]: # PyPy's library path scheme is not affected by this. # Return early or we will die on the following assert. if is_pypy: logger.debug('PyPy detected, skipping lib64 symlinking') return logger.debug('This system uses lib64; symlinking lib64 to lib') assert os.path.basename(lib_dir) == 'python%s' % sys.version[:3], ( "Unexpected python lib dir: %r" % lib_dir) lib_parent = os.path.dirname(lib_dir) top_level = os.path.dirname(lib_parent) lib_dir = os.path.join(top_level, 'lib') lib64_link = os.path.join(top_level, 'lib64') assert os.path.basename(lib_parent) == 'lib', ( "Unexpected parent dir: %r" % lib_parent) if os.path.lexists(lib64_link): return if symlink: os.symlink('lib', lib64_link) else: copyfile('lib', lib64_link) def resolve_interpreter(exe): """ If the executable given isn't an absolute path, search $PATH for the interpreter """ # If the "executable" is a version number, get the installed executable for # that version python_versions = get_installed_pythons() if exe in python_versions: exe = python_versions[exe] if os.path.abspath(exe) != exe: paths = os.environ.get('PATH', '').split(os.pathsep) for path in paths: if os.path.exists(os.path.join(path, exe)): exe = os.path.join(path, exe) break if not os.path.exists(exe): logger.fatal('The executable %s (from --python=%s) does not exist' % (exe, exe)) raise SystemExit(3) if not is_executable(exe): logger.fatal('The executable %s (from --python=%s) is not executable' % (exe, exe)) raise SystemExit(3) return exe def is_executable(exe): """Checks a file is executable""" return os.access(exe, os.X_OK) ############################################################ ## Relocating the environment: def make_environment_relocatable(home_dir): """ Makes the already-existing environment use relative paths, and takes out the #!-based environment selection in scripts. """ home_dir, lib_dir, inc_dir, bin_dir = path_locations(home_dir) activate_this = os.path.join(bin_dir, 'activate_this.py') if not os.path.exists(activate_this): logger.fatal( 'The environment doesn\'t have a file %s -- please re-run virtualenv ' 'on this environment to update it' % activate_this) fixup_scripts(home_dir, bin_dir) fixup_pth_and_egg_link(home_dir) ## FIXME: need to fix up distutils.cfg OK_ABS_SCRIPTS = ['python', 'python%s' % sys.version[:3], 'activate', 'activate.bat', 'activate_this.py', 'activate.fish', 'activate.csh'] def fixup_scripts(home_dir, bin_dir): if is_win: new_shebang_args = ( '%s /c' % os.path.normcase(os.environ.get('COMSPEC', 'cmd.exe')), '', '.exe') else: new_shebang_args = ('/usr/bin/env', sys.version[:3], '') # This is what we expect at the top of scripts: shebang = '#!%s' % os.path.normcase(os.path.join( os.path.abspath(bin_dir), 'python%s' % new_shebang_args[2])) # This is what we'll put: new_shebang = '#!%s python%s%s' % new_shebang_args for filename in os.listdir(bin_dir): filename = os.path.join(bin_dir, filename) if not os.path.isfile(filename): # ignore subdirs, e.g. .svn ones. continue f = open(filename, 'rb') try: try: lines = f.read().decode('utf-8').splitlines() except UnicodeDecodeError: # This is probably a binary program instead # of a script, so just ignore it. continue finally: f.close() if not lines: logger.warn('Script %s is an empty file' % filename) continue old_shebang = lines[0].strip() old_shebang = old_shebang[0:2] + os.path.normcase(old_shebang[2:]) if not old_shebang.startswith(shebang): if os.path.basename(filename) in OK_ABS_SCRIPTS: logger.debug('Cannot make script %s relative' % filename) elif lines[0].strip() == new_shebang: logger.info('Script %s has already been made relative' % filename) else: logger.warn('Script %s cannot be made relative (it\'s not a normal script that starts with %s)' % (filename, shebang)) continue logger.notify('Making script %s relative' % filename) script = relative_script([new_shebang] + lines[1:]) f = open(filename, 'wb') f.write('\n'.join(script).encode('utf-8')) f.close() def relative_script(lines): "Return a script that'll work in a relocatable environment." activate = "import os; activate_this=os.path.join(os.path.dirname(os.path.realpath(__file__)), 'activate_this.py'); exec(compile(open(activate_this).read(), activate_this, 'exec'), dict(__file__=activate_this)); del os, activate_this" # Find the last future statement in the script. If we insert the activation # line before a future statement, Python will raise a SyntaxError. activate_at = None for idx, line in reversed(list(enumerate(lines))): if line.split()[:3] == ['from', '__future__', 'import']: activate_at = idx + 1 break if activate_at is None: # Activate after the shebang. activate_at = 1 return lines[:activate_at] + ['', activate, ''] + lines[activate_at:] def fixup_pth_and_egg_link(home_dir, sys_path=None): """Makes .pth and .egg-link files use relative paths""" home_dir = os.path.normcase(os.path.abspath(home_dir)) if sys_path is None: sys_path = sys.path for path in sys_path: if not path: path = '.' if not os.path.isdir(path): continue path = os.path.normcase(os.path.abspath(path)) if not path.startswith(home_dir): logger.debug('Skipping system (non-environment) directory %s' % path) continue for filename in os.listdir(path): filename = os.path.join(path, filename) if filename.endswith('.pth'): if not os.access(filename, os.W_OK): logger.warn('Cannot write .pth file %s, skipping' % filename) else: fixup_pth_file(filename) if filename.endswith('.egg-link'): if not os.access(filename, os.W_OK): logger.warn('Cannot write .egg-link file %s, skipping' % filename) else: fixup_egg_link(filename) def fixup_pth_file(filename): lines = [] prev_lines = [] f = open(filename) prev_lines = f.readlines() f.close() for line in prev_lines: line = line.strip() if (not line or line.startswith('#') or line.startswith('import ') or os.path.abspath(line) != line): lines.append(line) else: new_value = make_relative_path(filename, line) if line != new_value: logger.debug('Rewriting path %s as %s (in %s)' % (line, new_value, filename)) lines.append(new_value) if lines == prev_lines: logger.info('No changes to .pth file %s' % filename) return logger.notify('Making paths in .pth file %s relative' % filename) f = open(filename, 'w') f.write('\n'.join(lines) + '\n') f.close() def fixup_egg_link(filename): f = open(filename) link = f.readline().strip() f.close() if os.path.abspath(link) != link: logger.debug('Link in %s already relative' % filename) return new_link = make_relative_path(filename, link) logger.notify('Rewriting link %s in %s as %s' % (link, filename, new_link)) f = open(filename, 'w') f.write(new_link) f.close() def make_relative_path(source, dest, dest_is_directory=True): """ Make a filename relative, where the filename is dest, and it is being referred to from the filename source. >>> make_relative_path('/usr/share/something/a-file.pth', ... '/usr/share/another-place/src/Directory') '../another-place/src/Directory' >>> make_relative_path('/usr/share/something/a-file.pth', ... '/home/user/src/Directory') '../../../home/user/src/Directory' >>> make_relative_path('/usr/share/a-file.pth', '/usr/share/') './' """ source = os.path.dirname(source) if not dest_is_directory: dest_filename = os.path.basename(dest) dest = os.path.dirname(dest) dest = os.path.normpath(os.path.abspath(dest)) source = os.path.normpath(os.path.abspath(source)) dest_parts = dest.strip(os.path.sep).split(os.path.sep) source_parts = source.strip(os.path.sep).split(os.path.sep) while dest_parts and source_parts and dest_parts[0] == source_parts[0]: dest_parts.pop(0) source_parts.pop(0) full_parts = ['..']*len(source_parts) + dest_parts if not dest_is_directory: full_parts.append(dest_filename) if not full_parts: # Special case for the current directory (otherwise it'd be '') return './' return os.path.sep.join(full_parts) ############################################################ ## Bootstrap script creation: def create_bootstrap_script(extra_text, python_version=''): """ Creates a bootstrap script, which is like this script but with extend_parser, adjust_options, and after_install hooks. This returns a string that (written to disk of course) can be used as a bootstrap script with your own customizations. The script will be the standard virtualenv.py script, with your extra text added (your extra text should be Python code). If you include these functions, they will be called: ``extend_parser(optparse_parser)``: You can add or remove options from the parser here. ``adjust_options(options, args)``: You can change options here, or change the args (if you accept different kinds of arguments, be sure you modify ``args`` so it is only ``[DEST_DIR]``). ``after_install(options, home_dir)``: After everything is installed, this function is called. This is probably the function you are most likely to use. An example would be:: def after_install(options, home_dir): subprocess.call([join(home_dir, 'bin', 'easy_install'), 'MyPackage']) subprocess.call([join(home_dir, 'bin', 'my-package-script'), 'setup', home_dir]) This example immediately installs a package, and runs a setup script from that package. If you provide something like ``python_version='2.5'`` then the script will start with ``#!/usr/bin/env python2.5`` instead of ``#!/usr/bin/env python``. You can use this when the script must be run with a particular Python version. """ filename = __file__ if filename.endswith('.pyc'): filename = filename[:-1] f = codecs.open(filename, 'r', encoding='utf-8') content = f.read() f.close() py_exe = 'python%s' % python_version content = (('#!/usr/bin/env %s\n' % py_exe) + '## WARNING: This file is generated\n' + content) return content.replace('##EXT' 'END##', extra_text) import os, subprocess def after_install(options, home_dir): etc = join(home_dir, 'etc') ## TODO: this should all come from distutils ## like distutils.sysconfig.get_python_inc() if sys.platform == 'win32': lib_dir = join(home_dir, 'Lib') bin_dir = join(home_dir, 'Scripts') elif is_jython: lib_dir = join(home_dir, 'Lib') bin_dir = join(home_dir, 'bin') else: lib_dir = join(home_dir, 'lib', py_version) bin_dir = join(home_dir, 'bin') if not os.path.exists(etc): os.makedirs(etc) subprocess.call([join(bin_dir, 'easy_install'), '-f', 'http://pylonshq.com/download/1.0.2', 'Pylons==1.0.2']) def convert(s): b = base64.b64decode(s.encode('ascii')) return zlib.decompress(b).decode('utf-8') ##file site.py SITE_PY = convert(""" eJzFPf1z2zaWv/OvwMqToZTKdOJ0e3tO3RsncVrfuYm3yc7m1vXoKAmyWFMkS5C2tTd3f/u9DwAE +CHb2+6cphNLJPDw8PC+8PAeOhqNTopCZkuxyZd1KoWScblYiyKu1kqs8lJU66Rc7hdxWW3h6eIm vpZKVLlQWxVhqygInv/GT/BcfF4nyqAA3+K6yjdxlSziNN2KZFPkZSWXYlmXSXYtkiypkjhN/g4t 8iwSz387BsFZJmDmaSJLcStLBXCVyFfiYlut80yM6wLn/DL6Y/xqMhVqUSZFBQ1KjTNQZB1XQSbl EtCElrUCUiaV3FeFXCSrZGEb3uV1uhRFGi+k+K//4qlR0zAMVL6Rd2tZSpEBMgBTAqwC8YCvSSkW +VJGQryRixgH4OcNsQKGNsU1U0jGLBdpnl3DnDK5kErF5VaM53VFgAhlscwBpwQwqJI0De7y8kZN YElpPe7gkYiZPfzJMHvAPHH8LucAjh+z4C9Zcj9l2MA9CK5aM9uUcpXcixjBwk95Lxcz/WycrMQy Wa2ABlk1wSYBI6BEmswPClqOb/UKfXdAWFmujGEMiShzY35JPaLgrBJxqoBt6wJppAjzd3KexBlQ I7uF4QAikDToG2eZqMqOQ7MTOQAocR0rkJKNEuNNnGTArD/GC0L7r0m2zO/UhCgAq6XEL7Wq3PmP ewgArR0CTANcLLOadZYmNzLdTgCBz4B9KVWdVigQy6SUiyovE6kIAKC2FfIekJ6KuJSahMyZRm6n RH+iSZLhwqKAocDjSyTJKrmuS5IwsUqAc4Er3n/8Sbw7fXN28kHzmAHGMnu9AZwBCi20gxMMIA5q VR6kOQh0FJzjHxEvlyhk1zg+4NU0OHhwpYMxzL2I2n2cBQey68XVw8AcK1AmNFZA/f4bukzVGujz Pw+sdxCcDFGFJs7f7tY5yGQWb6RYx8xfyBnBtxrOd1FRrV8DNyiEUwGpFC4OIpggPCCJS7NxnklR AIulSSYnAVBoTm39VQRW+JBn+7TWLU4ACGWQwUvn2YRGzCRMtAvrNeoL03hLM9NNArvOm7wkxQH8 ny1IF6VxdkM4KmIo/jaX10mWIULIC0G4F9LA6iYBTlxG4pxakV4wjUTI2otbokjUwEvIdMCT8j7e FKmcsviibt2tRmgwWQmz1ilzHLSsSL3SqjVT7eW9w+hLi+sIzWpdSgBezz2hW+X5VMxBZxM2Rbxh 8arucuKcoEeeqBPyBLWEvvgdKHqiVL2R9iXyCmgWYqhgladpfgckOwoCIfawkTHKPnPCW3gH/wJc /DeV1WIdBM5IFrAGhcgPgUIgYBJkprlaI+Fxm2bltpJJMtYUebmUJQ31OGIfMOKPbIxzDT7klTZq PF1c5XyTVKiS5tpkJmzxsrBi/fia5w3TAMutiGamaUOnDU4vLdbxXBqXZC5XKAl6kV7bZYcxg54x yRZXYsNWBt4BWWTCFqRfsaDSWVWSnACAwcIXZ0lRp9RIIYOJGAbaFAR/E6NJz7WzBOzNZjlAhcTm ewH2B3D7O4jR3ToB+iwAAmgY1FKwfPOkKtFBaPRR4Bt905/HB049W2nbxEOu4iTVVj7OgjN6eFqW JL4LWWCvqSaGghlmFbp21xnQEcV8NBoFgXGHtsp8zVVQldsjYAVhxpnN5nWChm82Q1Ovf6iARxHO wF43287CAw1hOn0AKjldVmW+wdd2bp9AmcBY2CPYExekZSQ7yB4nvkbyuSq9ME3RdjvsLFAPBRc/ nb4/+3L6SRyLy0alTdv67ArGPM1iYGuyCMBUrWEbXQYtUfElqPvEezDvxBRgz6g3ia+Mqxp4F1D/ XNb0Gqax8F4Gpx9O3pyfzv7y6fSn2aezz6eAINgZGezRlNE81uAwqgiEA7hyqSJtX4NOD3rw5uST fRDMEjX75mtgN3gyvpYVMHE5hhlPRbiJ7xUwaDilphPEsdMALHg4mYjvxOHz568OCVqxLbYADMyu 0xQfzrRFnyXZKg8n1PgXdumPWUlp/+3y6OsrcXwswl/i2zgMwIdqmjJL/Eji9HlbSOhawZ9xriZB sJQrEL0biQI6fk5+8YQ7wJJAy1zb6V/yJDPvmSvdIUh/jKkH4DCbLdJYKWw8m4VABOrQ84EOETvX KHVj6Fhs3a4TjQp+SgkLm2GXKf7Tg2I8p36IBqPodjGNQFw3i1hJbkXTh36zGeqs2WysBwRhJokB h4vVUChME9RZZQJ+LXEe6rC5ylP8ifBRC5AA4tYKtSQukt46RbdxWks1diYFRByPW2RERZso4kdw UcZgiZulm0za1DQ8A82AfGkOWrRsUQ4/e+DvgLoymzjc6PHei2mGmP477zQIB3A5Q1T3SrWgsHYU F6cX4tWLw310Z2DPubTU8ZqjhU6yWtqHK1gtIw+MMPcy8uLSZYV6Fp8e7Ya5iezKdFlhpZe4lJv8 Vi4BW2RgZ5XFT/QGduYwj0UMqwh6nfwBVqHGb4xxH8qzB2lB3wGotyEoZv3N0u9xMEBmChQRb6yJ 1HrXz6awKPPbBJ2N+Va/BFsJyhItpnFsAmfhPCZDkwgaArzgDCl1J0NQh2XNDivhjSDRXiwbxRoR uHPU1Ff09SbL77IZ74SPUemOJ5Z1UbA082KDZgn2xHuwQoBkDhu7hmgMBVx+gbK1D8jD9GG6QFna WwAgMPSKtmsOLLPVoynyrhGHRRiT14KEt5ToL9yaIWirZYjhQKK3kX1gtARCgslZBWdVg2YylDXT DAZ2SOJz3XnEW1AfQIuKEZjNsYbGjQz9Lo9AOYtzVyk5/dAif/nyhdlGrSm+gojNcdLoQqzIWEbF FgxrAjrBeGQcrSE2uAPnFsDUSrOm2P8k8oK9MVjPCy3b4AfA7q6qiqODg7u7u0hHF/Ly+kCtDv74 p2+++dML1onLJfEPTMeRFh1qiw7oHXq00bfGAn1nVq7Fj0nmcyPBGkvyysgVRfy+r5NlLo72J1Z/ Ihc3Zhr/Na4MKJCZGZSpDLQdNRg9U/vPoldqJJ6RdbZtxxP2S7RJtVbMt7rQo8rBEwC/ZZHXaKob TlDiK7BusENfynl9HdrBPRtpfsBUUU7Hlgf2X14hBj5nGL4ypniGWoLYAi2+Q/qfmG1i8o60hkDy oonq7J63/VrMEHf5eHm3vqYjNGaGiULuQInwmzxaAG3jruTgR7u2aPcc19Z8PENgLH1gmFc7lmMU HMIF12LqSp3D1ejxgjTdsWoGBeOqRlDQ4CTOmdoaHNnIEEGid2M2+7ywugXQqRU5NPEBswrQwh2n Y+3arOB4QsgDx+IlPZHgIh913r3gpa3TlAI6LR71qMKAvYVGO50DX44NgKkYlX8ZcUuzTfnYWhRe gx5gOceAkMFWHWbCN64PONob9bBTx+oP9WYa94HARRpzLOpR0AnlYx6hVCBNxdjvOcTilrjdwXZa HGIqs0wk0mpAuNrKo1eodhqmVZKh7nUWKVqkOXjFVisSIzXvfWeB9kH4uM+YaQnUZGjI4TQ6Jm/P E8BQt8Pw2XWNgQY3DoMYbRJF1g3JtIZ/wK2g+AYFo4CWBM2CeayU+RP7HWTOzld/GWAPS2hkCLfp kBvSsRgajnm/J5CMOhoDUpABCbvCSK4jq4MUOMxZIE+44bUclG6CESmQM8eCkJoB3Omlt8HBJxGe gJCEIuT7SslCfCVGsHxtUX2c7v5dudQEIcZOA3IVdPTi2I1sOFGN41aUw2doP75BZyVFDhw8B5fH DfS7bG6Y1gZdwFn3FbdFCjQyxWFGExfVK0MYN5j8h2OnRUMsM4hhKG8g70jHjDQJ7HJr0LDgBoy3 5u2x9GM3YoF9x2GuDuXmHvZ/YZmoRa5Cipm0YxfuR3NFlzYW2/NkPoI/3gKMJlceJJnq+AVGWf6B QUIPetgH3ZsshkWWcXmXZCEpME2/Y39pOnhYUnpG7uATbacOYKIY8Tx4X4KA0NHnAYgTagLYlctQ abe/C3bnFEcWLncfeW7z5dGrqy5xp0MRHvvpX6rT+6qMFa5WyovGQoGr1TXgqHRhcnG21YeX+nAb twllrmAXKT5++iKQEBzXvYu3T5t6w/CIzYNz8j4GddBrD5KrNTtiF0AEtSIyykH4dI58PLJPndyO iT0ByJMYZseiGEiaT/4ROLsWCsbYX24zjKO1VQZ+4PU3X896IqMukt98PXpglBYx+sR+3PIE7cic VLBrtqWMU3I1nD4UVMwa1rFtignrc9r+aR676vE5NVo29t3fAj8GCobUJfgIL6YN2bpTxY/vTg3C 03ZqB7DObtV89mgRYG+fz3+BHbLSQbXbOEnpXAEmv7+PytVs7jle0a89PEg7FYxDgr79l7p8AdwQ cjRh0p2OdsZOTMC5ZxdsPkWsuqjs6RyC5gjMywtwjz+HFU6ve+B7Bge/r7p8IiBvTqMeMmpbbIZ4 wQclhz1K9gnzfvqMf9dZP27mw4L1/zHLF/+cST5hKgaaNh4+rH5iuXbXAHuEeRpwO3e4hd2h+axy ZZw7VklKPEfd9VzcUboCxVbxpAigLNnv64GDUqoPvd/WZclH16QCC1nu43HsVGCmlvH8ek3Mnjj4 ICvExDZbUKzayevJ+4Qv1NFnO5Ow2Tf0c+c6NzErmd0mJfQFhTsOf/j442nYb0IwjgudHm9FHu83 INwnMG6oiRM+pQ9T6Cld/nH10d66+AQ1GQEmIqzJ1iVsJxBs4gj9a/BARMg7sOVjdtyhL9ZycTOT lDqAbIpdnaD4W3yNmNiMAj//S8UrSmKDmSzSGmnFjjdmH67qbEHnI5UE/0qnCmPqECUEcPhvlcbX Ykydlxh60txI0anbuNTeZ1HmmJwq6mR5cJ0shfy1jlPc1svVCnDBwyv9KuLhKQIl3nFOAyctKrmo y6TaAglileuzP0p/cBrOtzzRsYckH/MwATEh4kh8wmnjeybc0pDLBAf8Ew+cJO67sYOTrBDRc3if 5TMcdUY5vlNGqnsuT4+D9gg5ABgBUJj/aKIjd/4bSa/cA0Zac5eoqCU9UrqRhpycMYQynmCkg3/T T58RXd4awPJ6GMvr3Vhet7G87sXy2sfyejeWrkjgwtqglZGEvsBV+1ijN9/GjTnxMKfxYs3tMPcT czwBoijMBtvIFKdAe5EtPt8jIKS2nQNnetjkzyScVFrmHALXIJH78RBLb+ZN8rrTmbJxdGeeinFn h3KI/L4HUUSpYnPqzvK2jKs48uTiOs3nILYW3WkDYCra6UQcK81uZ3OO7rYs1ejiPz//8PEDNkdQ I5PeQN1wEdGw4FTGz+PyWnWlqdn8FcCO1NJPxKFuGuDeIyNrPMoe//OOMjyQccQdZSjkogAPgLK6 bDM39ykMW891kpR+zkzOh03HYpRVo2ZSA0Q6ubh4d/L5ZEQhv9H/jlyBMbT1pcPFx7SwDbr+m9vc Uhz7gFDr2FZj/Nw5ebRuOOJhG2vAdjzf1oPDxxjs3jCBP8t/KqVgSYBQkQ7+PoVQj945/Kb9UIc+ hhE7yX/uyRo7K/adI3uOi+KIft+xQ3sA/7AT9xgzIIB2ocZmZ9DslVtK35rXHRR1gD7S1/vNe832 1qu9k/EpaifR4wA6lLXNht0/75yGjZ6S1ZvT788+nJ+9uTj5/IPjAqIr9/HTwaE4/fGLoPwQNGDs E8WYGlFhJhIYFrfQSSxz+K/GyM+yrjhIDL3enZ/rk5oNlrpg7jPanAiecxqThcZBM45C24c6/wgx SvUGyakponQdqjnC/dKG61lUrvOjqVRpjs5qrbdeulbM1JTRuXYE0geNXVIwCE4xg1eUxV6ZXWHJ J4C6zqoHKW2jbWJISkHBTrqAc/5lTle8QCl1hidNZ63oL0MX1/AqUkWawE7udWhlSXfD9JiGcfRD e8DNePVpQKc7jKwb8qwHsUCr9Trkuen+k4bRfq0Bw4bB3sG8M0npIZSBjcltIsRGfJITynv4apde r4GCBcODvgoX0TBdArOPYXMt1glsIIAn12B9cZ8AEFor4R8IHDnRAZljdkb4drPc/3OoCeK3/vnn nuZVme7/TRSwCxKcShT2ENNt/A42PpGMxOnH95OQkaPUXPHnGssDwCGhAKgj7ZS/xCfos7GS6Urn l/j6AF9oP4Fet7qXsih1937XOEQJeKbG5DU8U4Z+IaZ7WdhTnMqkBRorHyxmWEHopiGYz574tJZp qvPdz96dn4LviMUYKEF87nYKw3G8BI/QdfIdVzi2QOEBO7wukY1LdGEpyWIZec16g9YoctTby8uw 60SB4W6vThS4jBPloj3GaTMsU04QISvDWphlZdZutUEKu22I4igzzBKzi5ISWH2eAF6mpzFviWCv hKUeJgLPp8hJVpmMxTRZgB4FlQsKdQpCgsTFekbivDzjGHheKlMGBQ+LbZlcrys83YDOEZVgYPMf T76cn32gsoTDV43X3cOcU9oJTDmJ5BhTBDHaAV/ctD/kqtmsj2f1K4SB2gf+tF9xdsoxD9Dpx4FF /NN+xXVox85OkGcACqou2uKBGwCnW5/cNLLAuNp9MH7cFMAGMx8MxSKx7EUnerjz63KibdkyJRT3 MS+fcICzKmxKmu7spqS1P3qOqwLPuZbj/kbwtk+2zGcOXW86b4aS39xPRwqxJBYw6rb2xzDZYZ2m ejoOsw1xC21rtY39OXNipU67RYaiDEQcu50nLpP1K2HdnDnQS6PuABPfanSNJPaq8tHP2Uh7GB4m ltidfYrpSGUsZAQwkiF17U8NPhRaBFAglP07diR3Onl+6M3RsQYPz1HrLrCNP4Ai1Lm4VOORl8CJ 8OVXdhz5FaGFevRIhI6nkskst3li+Llbo1f50p9jrwxQEBPFroyzazlmWFMD8yuf2AMhWNK2Hqkv k6s+wyLOwDm9H+Dwrlz0H5wY1FqM0Gl3I7dtdeSTBxv0loLsJJgPvozvQPcXdTXmlRw4h+6tpRuG +jBEzD6Epvr0fRxiOObXcGB9GsC91NCw0MP7deDsktfGOLLWPraqmkL7QnuwixK2ZpWiYxmnONH4 otYLaAzucWPyR/apThSyv3vqxJyYkAXKg7sgvbmNdINWOGHE5UpcOZpQOnxTTaPfLeWtTMFogJEd Y7XDL7baYRLZcEpvHthvxu5ie7Htx43eNJgdmXIMRIAKMXoDPbsQanDAFf5Z70Ti7Iac47d/PZuK tx9+gn/fyI9gQbHmcSr+BqOLt3kJ20ou2qXbFLCAo+L9Yl4rLIwkaHRCwRdPoLd24ZEXT0N0ZYlf UmIVpMBk2nLDt50AijxBKmRv3ANTLwG/TUFXywk1DmLfWoz0S6TBcI0L1oUc6JbRutqkaCac4Eiz iJej87O3px8+nUbVPTK2+Tlygid+HhZORx8Nl3gMNhX2yaLGJ1eOv/yDTIsed1nvNU29DO41RQjb kcLuL/kmjdjuKeISAwai2C7zRYQtgdO5RK+6A/954mwrH7TvnnFFWOOJPjxrnHh8DNQQP7f1zwga Uh89J+pJCMVzrBXjx9Go3wJPBUW04c/zm7ulGxDXRT80wTamzazHfnerAtdMZw3PchLhdWyXwdSB pkmsNvOFWx/4MRP6IhRQbnS8IVdxnVZCZrCVor093UgBCt4t6WMJYVZhK0Z1bhSdSe/irXJyj2Il RjjqiIrq8RyGAoWw9f4xvmEzgLWGouYSaIBOiNK2KXe6qnqxZgnmnRBRryff4C7JXrnJL5rCPChv jBeN/wrzRG+RMbqWlZ4/PxhPLl82CQ4UjF54Bb2LAoydyyZ7oDGL58+fj8S/Pez0MCpRmuc34I0B 7F5n5ZxeDxhsPTm7Wl2H3ryJgB8Xa3kJD64oaG6f1xlFJHd0pQWR9q+BEeLahJYZTfuWOeZYXcnn y9yCz6m0wfhLltB1RxhRkqhs9a1RGG0y0kQsCYohjNUiSUKOTsB6bPMaa/Ewuqj5Rd4DxycIZopv 8WCMd9hrdCwpb9Zyj0XnWIwI8IhSyng0KmamajTAc3ax1WjOzrKkaspIXrhnpvoKgMreYqT5SsR3 KBlmHi1iOGWdHqs2jnW+k0W9jUq+uHTjjK1Z8uuHcAfWBknLVyuDKTw0i7TIZbkw5hRXLFkklQPG tEM43JkubyLrEwU9KI1AvZNVWFqJtm//YNfFxfQjHR/vm5F01lBlL8TimFCctfIKo6gZn6JPlpCW b82XCYzygaLZ2hPwxhJ/0LFUrCHw7u1wyxnrTN/HwWkbzSUdAIfugLIK0rKjpyOci8csfGbagVs0 8EM7c8LtNimrOk5n+tqHGfppM3uervG0ZXA7CzyttwK+fQ6O777O2AfHwSTXID0x49ZUZByLlY5M RG5lmV+EVeTo5R2yrwQ+BVJmOTP10CZ2dGnZ1Raa6gRHR8UjqK9M8dKAQ26qZjoFJy7mU0pvMuUO A86zn29JV1eI78T41VQctnY+i2KLNzkBss+Woe+KUTeYihMMMHNs34shvjsW45dT8ccd0KOBAY4O 3RHa+9gWhEEgr66eTMY0mRPZwr4U9of76hxG0PSM4+SqTf4umb4lKv1ri0pcIagTlV+2E5VbYw/u WzsfH8lwA4pjlcjl/jOFJNRIN7p5mMEJPyyg37M5Wrp2vKmoocK5OWxG7ho96GhE4zbbQUxRulZf XL+LuoYNp71zwKTJtFIV7S1zmMao0WsRFQDM+o7S8Bve7QLvNSlc/2zwiFUXAViwPREEXenJB2ZN w0ZQH3QEn6QBHmAUEeJhaqMoXMl6goiEdA8OMdFXrUNsh+N/d+bhEoOho9AOlt98vQtPVzB7izp6 FnR3pYUnsra8ollu8+kPzHmM0tf1NwmMA6URHXBWzVWV5GYeYfYy30GT2yzmDV4GSSfTaBJT6bpN vJXmW7/Qj6HYASWTwVqAJ1Wv8CD5lu62PFGU9IZX1Hx9+HJqKoMZkJ7Aq+jVV/oKSOpmLj/wfeyp 3rvBS93vMPoXB1hS+b3tq85uhqZ13LoLyh8spOjZJJpZOjSG6eE6kGbNYoF3JjbEZN/aXgDyHryd Ofg55vLTHBw22JBGfei6GqOR3iHVNiDAD5uMIcl5VNdGkSLSu4RtSHnuUpxPFgXdq9+CYAgBOX8d 8xt0BeviyIbYjE3Bk8+xm82Jn+qmt+6M7Qka2+om3DV97r9r7rpFYGdukhk6c/frS10a6L7DVrSP Bhze0IR4VIlEo/H7jYlrB6Y6h6Y/Qq8/SH63E850wKw8BMZk7GC8n9hTY2/M/iZeuN8xIWyfL2R2 y4l7nY3WtDs2o83xj/EUOPkFn9sbBiijaak5kPdLdMPejHNkZ/L6Ws1ivN1xRptsyufq7J7Mtu09 Xc4nY7U1uy28tAhAGG7Smbducj0wBuhKvmWa06Gc22kEDU1Jw04WskqWbBL01g7ARRwxpf4mEM9p xKNUYqBb1WVRwm54pO8i5jydvtTmBqgJ4G1idWNQNz2m+mpaUqyUHGZKkDlO20ryASKwEe+YhtnM vgNeedFcs5BMLTPIrN7IMq6aK4b8jIAENl3NCFR0jovrhOcaqWxxiYtYYnnDQQoDZPb7V7Cx9DbV O+5VmFht93h2oh465PuUKxscY2S4OLm31wu611ot6Wpr1zu0zRqus1cqwTKYu/JIR+pYGb/V93fx HbMcyUf/0uEfkHe38tLPQrfqjL1bi4bzzFUI3Qub8MYAMs599zB2OKB742JrA2zH9/WFZZSOhznQ 2FJR++S9CqcZbdJEkDBh9IEIkl8U8MQIkgf/kREkfWsmGBqNj9YDvWUCD4SaWD24V1A2jAB9ZkAk PMBuXWBoTOXYTbovcpXcj+yF0qwrnUo+Yx6QI7t3kxEIvmpSuRnK3lVwuyJIvnTR4+/PP745OSda zC5O3v7HyfeUlIXHJS1b9egQW5bvM7X3vfRvN9ymE2n6Bm+w7bkhlmuYNITO+04OQg+E/nq1vgVt KzL39VCHTt1PtxMgvnvaLahDKrsXcscv0zUmbvpMK0870E85qdb8cjITzCNzUsfi0JzEmffN4YmW 0U5seWjhnPTWrjrR/qq+BXQg7j2xSda0Anhmgvxlj0xMxYwNzLOD0v7ffFBmOFYbmht0QAoX0rnJ kS5xZFCV//8TKUHZxbi3Y0dxau/mpnZ8PKTspfN49ruQkSGIV+436s7PFfalTAeoEASs8PQ9hYyI 0X/6QNWmHzxT4nKfCov3Udlc2V+4Ztq5/WuCSQaVve9LcYISH7NC41WduokDtk+nAzl9dBqVr5xK FtB8B0DnRjwVsDf6S6wQ51sRwsZRu2SYHEt01Jf1Ocij3XSwN7R6IfaHyk7dskshXg43XLYqO3WP Q+6hHuihalPc51hgzNIcqicV3xFkPs4UdMGX53zgGbre9sPX28uXR/ZwAfkdXzuKhLLJRo5hv3Sy MXdeKul0J2Ypp5Suh3s1JySsW1w5UNknGNrbdEpSBvY/Js+BIY289/0hM9PDu3p/1MbUst4RTEmM n6kJTcsp4tG42yeT7nQbtdUFwgVJjwDSUYEAC8F0dKOTILrlLO/xC70bnNd0Ha97whQ6UkHJYj5H cA/j+zX4tbtTIfGjujOKpj83aHOgXnIQbvYduNXEC4UMm4T21Bs+GHABuCa7v//LR/TvpjHa7oe7 /Grb6lVvHSD7spj5iplBLRKZxxEYGdCbY9LWWC5hBB2voWno6DJUMzfkC3T8KJsWL9umDQY5szPt AVijEPwfucjncQ== """) ##file activate.sh ACTIVATE_SH = convert(""" eJytVVFvokAQfudXTLEPtTlLeo9tvMSmJpq02hSvl7u2wRUG2QR2DSxSe7n/frOACEVNLlceRHa+ nfl25pvZDswCnoDPQ4QoTRQsENIEPci4CsBMZBq7CAsuLOYqvmYKTTj3YxnBgiXBudGBjUzBZUJI BXEqgCvweIyuCjeG4eF2F5x14bcB9KQiQQWrjSddI1/oQIx6SYYeoFjzWIoIhYI1izlbhJjkKO7D M/QEmKfO9O7WeRo/zr4P7pyHwWxkwitcgwpQ5Ej96OX+PmiFwLeVjFUOrNYKaq1Nud3nR2n8nI2m k9H0friPTGVsUdptaxGrTEfpNVFEskxpXtUkkCkl1UNF9cgLBkx48J4EXyALuBtAwNYIjF5kcmUU abMKmMq1ULoiRbgsDEkTSsKSGFCJ6Z8vY/2xYiSacmtyAfCDdCNTVZoVF8vSTQOoEwSnOrngBkws MYGMBMg8/bMBLSYKS7pYEXP0PqT+ZmBT0Xuy+Pplj5yn4aM9nk72JD8/Wi+Gr98sD9eWSMOwkapD BbUv91XSvmyVkICt2tmXR4tWmrcUCsjWOpw87YidEC8i0gdTSOFhouJUNxR+4NYBG0MftoCTD9F7 2rTtxG3oPwY1b2HncYwhrlmj6Wq924xtGDWqfdNxap+OYxplEurnMVo9RWks+rH8qKEtx7kZT5zJ 4H7oOFclrN6uFe+d+nW2aIUsSgs/42EIPuOhXq+jEo3S6tX6w2ilNkDnIpHCWdEQhFgwj9pkk7FN l/y5eQvRSIQ5+TrL05lewxWpt/Lbhes5cJF3mLET1MGhcKCF+40tNWnUulxrpojwDo2sObdje3Bz N3QeHqf3D7OjEXMVV8LN3ZlvuzoWHqiUcNKHtwNd0IbvPGKYYM31nPKCgkUILw3KL+Y8l7aO1ArS Ad37nIU0fCj5NE5gQCuC5sOSu+UdI2NeXg/lFkQIlFpdWVaWZRfvqGiirC9o6liJ9FXGYrSY9mI1 D/Ncozgn13vJvsznr7DnkJWXsyMH7e42ljdJ+aqNDF1bFnKWFLdj31xtaJYK6EXFgqmV/ymD/ROG +n8O9H8f5vsGOWXsL1+1k3g= """) ##file activate.fish ACTIVATE_FISH = convert(""" eJydVW2P2jgQ/s6vmAZQoVpA9/WkqqJaTou0u6x2uZVOVWWZZEKsS+yc7UDpr+84bziQbauLxEvs eXnsZ56ZIWwTYSAWKUJWGAs7hMJgBEdhEwiMKnSIsBNywUMrDtziPBYmCeBDrFUG7v8HmCTW5n8u Fu7NJJim81Bl08EQTqqAkEupLOhCgrAQCY2hTU+DQVxIiqgkRNiEBphFEKy+kd1BaFvwFOUBuIxA oy20BKtAKp3xFMo0QNtCK5mhtMEA6BmSpUELKo38TThwLfguRVNaiRgs0llnEoIR29zfstf18/bv 5T17Wm7vAiiN3ONCzfbfwC3DtWXXDqHfAGX0q6z/bO82j3ebh1VwnbrduwTQbvwcRtesAfMGor/W L3fs6Xnz8LRlm9fV8/P61sM0LDNwCZjl9gSpCokJRzpryGQ5t8kNGFUt51QjOZGu0Mj35FlYlXEr yC09EVOp4lEXfF84Lz1qbhBsgl59vDedXI3rTV03xipduSgt9kLytI3XmBp3aV6MPoMQGNUU62T6 uQdeefTy1Hfj10zVHg2pq8fXDoHBiOv94csfXwN49xECqWREy7pwukKfvxdMY2j23vXDPuuxxeE+ JOdCOhxCE3N44B1ZeSLuZh8Mmkr2wEPAmPfKWHA2uxIRjEopdbQYjDz3BWOf14/scfmwoki1eQvX ExBdF60Mqh+Y/QcX4uiH4Amwzx79KOVFtbL63sXJbtcvy8/3q5rupmO5CnE91wBviQAhjUUegYpL vVEbpLt2/W+PklRgq5Ku6mp+rpMhhCo/lXthQTxJ2ysO4Ka0ad97S7VT/n6YXus6fzk3fLnBZW5C KDC6gSO62QDqgFqLCCtPmjegjnLeAdArtSE8VYGbAJ/aLb+vnQutFhk768E9uRbSxhCMzdgEveYw IZ5ZqFKl6+kz7UR4U+buqQZXu9SIujrAfD7f0FXpozB4Q0gwp31H9mVTZGGC4b871/wm7lvyDLu1 FUyvTj/yvD66k3UPTs08x1AQQaGziOl0S1qRkPG9COtBTSTWM9NzQ4R64B+Px/l3tDzCgxv5C6Ni e+QaF9xFWrxx0V/G5uvYQOdiZzvYpQUVQSIsTr1TTghI33GnPbTA7/GCqcE3oE3GZurq4HeQXQD6 32XS1ITj/qLjN72ob0hc5C9bzw8MhfmL """) ##file activate.csh ACTIVATE_CSH = convert(""" eJx9VG1P2zAQ/u5fcYQKNgTNPtN1WxlIQ4KCUEGaxuQ6yYVYSuzKdhqVX7+zk3bpy5YPUXL3PPfc ne98DLNCWshliVDV1kGCUFvMoJGugMjq2qQIiVSxSJ1cCofD1BYRnOVGV0CfZ0N2DD91DalQSjsw tQLpIJMGU1euvPe7QeJlkKzgWixlhnAt4aoUVsLnLBiy5NtbJWQ5THX1ZciYKKWwkOFaE04dUm6D r/zh7pq/3D7Nnid3/HEy+wFHY/gEJydg0aFaQrBFgz1c5DG1IhTs+UZgsBC2GMFBlaeH+8dZXwcW VPvCjXdlAvCfQsE7al0+07XjZvrSCUevR5dnkVeKlFYZmUztG4BdzL2u9KyLVabTU0bdfg7a0hgs cSmUg6UwUiQl2iHrcbcVGNvPCiLOe7+cRwG13z9qRGgx2z6DHjfm/Op2yqeT+xvOLzs0PTKHDz2V tkckFHoQfQRXoGJAj9el0FyJCmEMhzgMS4sB7KPOE2ExoLcSieYwDvR+cP8cg11gKkVJc2wRcm1g QhYFlXiTaTfO2ki0fQoiFM4tLuO4aZrhOzqR4dIPcWx17hphMBY+Srwh7RTyN83XOWkcSPh1Pg/k TXX/jbJTbMtUmcxZ+/bbqOsy82suFQg/BhdSOTRhMNBHlUarCpU7JzBhmkKmRejKOQzayQe6MWoa n1wqWmuh6LZAaHxcdeqIlVLhIBJdO9/kbl0It2oEXQj+eGjJOuvOIR/YGRqvFhttUB2XTvLXYN2H 37CBdbW2W7j2r2+VsCn0doVWcFG1/4y1VwBjfwAyoZhD """) ##file activate.bat ACTIVATE_BAT = convert(""" eJx9UdEKgjAUfW6wfxjiIH+hEDKUFHSKLCMI7kNOEkIf9P9pTJ3OLJ/03HPPPed4Es9XS9qqwqgT PbGKKOdXL4aAFS7A4gvAwgijuiKlqOpGlATS2NeMLE+TjJM9RkQ+SmqAXLrBo1LLIeLdiWlD6jZt r7VNubWkndkXaxg5GO3UaOOKS6drO3luDDiO5my3iA0YAKGzPRV1ack8cOdhysI0CYzIPzjSiH5X 0QcvC8Lfaj0emsVKYF2rhL5L3fCkVjV76kShi59NHwDniAHzkgDgqBcwOgTMx+gDQQqXCw== """) ##file deactivate.bat DEACTIVATE_BAT = convert(""" eJxzSE3OyFfIT0vj4ipOLVEI8wwKCXX0iXf1C7Pl4spMU0hJTcvMS01RiPf3cYmHyQYE+fsGhCho cCkAAUibEkTEVhWLMlUlLk6QGixStlyaeCyJDPHw9/Pw93VFsQguim4ZXAJoIUw5DhX47XUM8UCx EchHtwsohN1bILUgw61c/Vy4AJYPYm4= """) ##file activate.ps1 ACTIVATE_PS = convert(""" eJylWdmO41hyfW+g/0FTU7C7IXeJIqmtB/3AnZRIStxF2kaBm7gv4ipyMF/mB3+Sf8GXVGVl1tLT 43ECSqR4b5wbETeWE8z/+a///vNCDaN6cYtSf5G1dbNw/IVXNIu6aCvX9xa3qsgWl0IJ/7IYinbh 2nkOVqs2X0TNjz/8eeFFle826fBhQRaLBkD9uviw+LCy3Sbq7Mb/UNbrH3+YNtLcVaB+Xbipb+eL tly0eVsD/M6u6g8//vC+dquobH5VWU75eMFUdvHb4n02RHlXuHYTFfmHbHCLLLNz70NpN+GrBI4p 1EeSk4FAXaZR88u0vPip8usi7fznt3fvP+OuPnx49/Pil4td+XnzigIAPoqYQH2J8v4z+C+8b98m Q25t7k76LIK0cOz0V89/MXXx0+Lf6z5q3PA/F+/FIif9uqnaadFf/PzXSXYBfqIb2NeApecJwPzI dlL/149nnvyoc7KqYfzTAT8v/voUmX7e+3n364tffl/oVaDyswKY/7J18e6bve8Wv9RuUfqfLHmK /u139Hwx+9ePRep97KKqae30YwmCo2y+0vTz1k+rv7159B3pb1SOGj97Pe8/flfkC1Vn/7xYR4n6 lypNEGDDV5f7lcjil3S+4++p881Wv6qKyn5GQg1yJwcp4BZ5E+Wt/z1P/umbiHir4J8Xip/eFt6n 9T/9gU9eY+7zUX97Jlmb136ziKrKT/3OzpvP8VX/+MObSP0lL3LvVZlJ9v1b8357jXyw8rXxYPXN 11n4UzJ8G8S/vUbuJ6RPj999DbtS5kys//JusXwrNLnvT99cFlBNwXCe+niRz8JF/ezNr9Pze+H6 18W7d5PPvozW7+387Zto/v4pL8BvbxTzvIW9KCv/Fj0WzVQb/YXbVlPZWTz3/9vCaRtQbPN/Bb+j 2rUrDxTVD68gfQXu/ZewAFX53U/vf/rD2P3558W7+W79Po1y/xXoX/6RFHyNIoVjgAG4H0RTcAe5 3bSVv3DSwk2mZYHjFB8zj6fC4sLOFTHJJQrwzFYJgso0ApOoBzFiRzzQKjIQCCbQMIFJGCKqGUyS 8AkjiF2wTwmMEbcEUvq8Nj+X0f4YcCQmYRiOY7eRbAJDqzm1chOoNstbJ8oTBhZQ2NcfgaB6QjLp U4+SWFjQGCZpyqby8V4JkPGs9eH1BscXIrTG24QxXLIgCLYNsIlxSYLA6SjAeg7HAg4/kpiIB8k9 TCLm0EM4gKIxEj8IUj2dQeqSxEwYVH88qiRlCLjEYGuNIkJB1BA5dHOZdGAoUFk54WOqEojkuf4Q Ig3WY+96TDlKLicMC04h0+gDCdYHj0kz2xBDj9ECDU5zJ0tba6RKgXBneewhBG/xJ5m5FX+WSzsn wnHvKhcOciw9NunZ0BUF0n0IJAcJMdcLqgQb0zP19dl8t9PzmMBjkuIF7KkvHgqEovUPOsY0PBB1 HCtUUhch83qEJPjQcNQDsgj0cRqx2ZbnnlrlUjE1EX2wFJyyDa/0GLrmKDEFepdWlsbmVU45Wiwt eFM6mfs4kxg8yc4YmKDy67dniLV5FUeO5AKNPZaOQQ++gh+dXE7dbJ1aTDr7S4WPd8sQoQkDyODg XnEu/voeKRAXZxB/e2xaJ4LTFLPYEJ15Ltb87I45l+P6OGFA5F5Ix8A4ORV6M1NH1uMuZMnmFtLi VpYed+gSq9JDBoHc05J4OhKetrk1p0LYiKipxLMe3tYS7c5V7O1KcPU8BJGdLfcswhoFCSGQqJ8f ThyQKy5EWFtHVuNhvTnkeTc8JMpN5li3buURh0+3ZGuzdwM55kon+8urbintjdQJf9U1D0ah+hNh i1XNu4fSKbTC5AikGEaj0CYM1dpuli7EoqUt7929f1plxGGNZnixFSFP2qzhlZMonu2bB9OWSqYx VuHKWNGJI8kqUhMTRtk0vJ5ycZ60JlodlmN3D9XiEj/cG2lSt+WV3OtMgt1Tf4/Z+1BaCus740kx Nvj78+jMd9tq537Xz/mNFyiHb0HdwHytJ3uQUzKkYhK7wjGtx3oKX43YeYoJVtqDSrCnQFzMemCS 2bPSvP+M4yZFi/iZhAjL4UOeMfa7Ex8HKBqw4umOCPh+imOP6yVTwG2MplB+wtg97olEtykNZ6wg FJBNXSTJ3g0CCTEEMdUjjcaBDjhJ9fyINXgQVHhA0bjk9lhhhhOGzcqQSxYdj3iIN2xGEOODx4qj Q2xikJudC1ujCVOtiRwhga5nPdhe1gSa649bLJ0wCuLMcEYIeSy25YcDQHJb95nfowv3rQnin0fE zIXFkM/EwSGxvCCMgEPNcDp/wph1gMEa8Xd1qAWOwWZ/KhjlqzgisBpDDDXz9Cmov46GYBKHC4zZ 84HJnXoTxyWNBbXV4LK/r+OEwSN45zBp7Cub3gIYIvYlxon5BzDgtPUYfXAMPbENGrI+YVGSeTQ5 i8NMB5UCcC+YRGIBhgs0xhAGwSgYwywpbu4vpCSTdEKrsy8osXMUnHQYenQHbOBofLCNNTg3CRRj A1nXY2MZcjnXI+oQ2Zk+561H4CqoW61tbPKv65Y7fqc3TDUF9CA3F3gM0e0JQ0TPADJFJXVzphpr 2FzwAY8apGCju1QGOiUVO5KV6/hKbtgVN6hRVwpRYtu+/OC6w2bCcGzZQ8NCc4WejNEjFxOIgR3o QqR1ZK0IaUxZ9nbL7GWJIjxBARUhAMnYrq/S0tVOjzlOSYRqeIZxaSaOBX5HSR3MFekOXVdUPbjX nru61fDwI8HRYPUS7a6Inzq9JLjokU6P6OzT4UCH+Nha+JrU4VqEo4rRHQJhVuulAnvFhYz5NWFT aS/bKxW6J3e46y4PLagGrCDKcq5B9EmP+s1QMCaxHNeM7deGEV3WPn3CeKjndlygdPyoIcNaL3dd bdqPs47frcZ3aNWQ2Tk+rjFR01Ul4XnQQB6CSKA+cZusD0CP3F2Ph0e78baybgioepG12luSpFXi bHbI6rGLDsGEodMObDG7uyxfCeU+1OiyXYk8fnGu0SpbpRoEuWdSUlNi5bd9nBxYqZGrq7Qa7zV+ VLazLcelzzP9+n6+xUtWx9OVJZW3gk92XGGkstTJ/LreFVFF2feLpXGGuQqq6/1QbWPyhJXIXIMs 7ySVlzMYqoPmnmrobbeauMIxrCr3sM+qs5HpwmmFt7SM3aRNQWpCrmeAXY28EJ9uc966urGKBL9H 18MtDE5OX97GDOHxam11y5LCAzcwtkUu8wqWI1dWgHyxGZdY8mC3lXzbzncLZ2bIUxTD2yW7l9eY gBUo7uj02ZI3ydUViL7oAVFag37JsjYG8o4Csc5R7SeONGF8yZP+7xxi9scnHvHPcogJ44VH/LMc Yu6Vn3jEzCFw9Eqq1ENQAW8aqbUwSiAqi+nZ+OkZJKpBL66Bj8z+ATqb/8qDIJUeNRTwrI0YrVmb 9FArKVEbCWUNSi8ipfVv+STgkpSsUhcBg541eeKLoBpLGaiHTNoK0r4nn3tZqrcIULtq20Df+FVQ Sa0MnWxTugMuzD410sQygF4qdntbswiJMqjs014Irz/tm+pd5oygJ0fcdNbMg165Pqi7EkYGAXcB dwxioCDA3+BY9+JjuOmJu/xyX2GJtaKSQcOZxyqFzTaa6/ot21sez0BtKjirROKRm2zuai02L0N+ ULaX8H5P6VwsGPbYOY7sAy5FHBROMrMzFVPYhFHZ7M3ZCZa2hsT4jGow6TGtG8Nje9405uMUjdF4 PtKQjw6yZOmPUmO8LjFWS4aPCfE011N+l3EdYq09O3iQJ9a01B3KXiMF1WmtZ+l1gmyJ/ibAHZil vQzdOl6g9PoSJ4TM4ghTnTndEVMOmsSSu+SCVlGCOLQRaw9oLzamSWP62VuxPZ77mZYdfTRGuNBi KyhZL32S2YckO/tU7y4Bf+QKKibQSKCTDWPUwWaE8yCBeL5FjpbQuAlb53mGX1jptLeRotREbx96 gnicYz0496dYauCjpTCA4VA0cdLJewzRmZeTwuXWD0talJsSF9J1Pe72nkaHSpULgNeK1+o+9yi0 YpYwXZyvaZatK2eL0U0ZY6ekZkFPdC8JTF4Yo1ytawNfepqUKEhwznp6HO6+2l7L2R9Q3N49JMIe Z+ax1mVaWussz98QbNTRPo1xu4W33LJpd9H14dd66ype7UktfEDi3oUTccJ4nODjwBKFxS7lYWiq XoHu/b7ZVcK5TbRD0F/2GShg2ywwUl07k4LLqhofKxFBNd1grWY+Zt/cPtacBpV9ys2z1moMLrT3 W0Elrjtt5y/dvDQYtObYS97pqj0eqmwvD3jCPRqamGthLiF0XkgB6IdHLBBwDGPiIDh7oPaRmTrN tYA/yQKFxRiok+jM6ciJq/ZgiOi5+W4DEmufPEubeSuYJaM3/JHEevM08yJAXUQwb9LS2+8FOfds FfOe3Bel6EDSjIEIKs4o9tyt67L1ylQlzhe0Q+7ue/bJnWMcD3q6wDSIQi8ThnRM65aqLWesi/ZM xhHmQvfKBbWcC194IPjbBLYR9JTPITbzwRcu+OSFHDHNSYCLt29sAHO6Gf0h/2UO9Xwvhrjhczyx Ygz6CqP4IwxQj5694Q1Pe2IR+KF/yy+5PvCL/vgwv5mPp9n4kx7fnY/nmV++410qF/ZVCMyv5nAP pkeOSce53yJ6ahF4aMJi52by1HcCj9mDT5i+7TF6RoPaLL+cN1hXem2DmX/mdIbeeqwQOLD5lKO/ 6FM4x77w6D5wMx3g0IAfa2D/pgY9a7bFQbinLDPz5dZi9ATIrd0cB5xfC0BfCCZO7TKP0jQ2Meih nRXhkA3smTAnDN9IW2vA++lsgNuZ2QP0UhqyjUPrDmgfWP2bWWiKA+YiEK7xou8cY0+d3/bk0oHR QLrq4KzDYF/ljQDmNhBHtkVNuoDey6TTeaD3SHO/Bf4d3IwGdqQp6FuhmwFbmbQBssDXVKDBYOpk Jy7wxOaSRwr0rDmGbsFdCM+7XU/84JPu3D/gW7QXgzlvbjixn99/8CpWFUQWHFEz/RyXvzNXTTOd OXLNNFc957Jn/YikNzEpUdRNxXcC6b76ccTwMGoKj5X7c7TvHFgc3Tf4892+5A+iR+D8OaaE6ACe gdgHcyCoPm/xiDCWP+OZRjpzfj5/2u0i4qQfmIEOsTV9Hw6jZ3Agnh6hiwjDtGYxWvt5TiWEuabN 77YCyRXwO8P8wdzG/8489KwfFBZWI6Vvx76gmlOc03JI1HEfXYZEL4sNFQ3+bqf7e2hdSWQknwKF ICJjGyDs3fdmnnxubKXebpQYLjPgEt9GTzKkUgTvOoQa1J7N3nv4sR6uvYFLhkXZ+pbCoU3K9bfq gF7W82tNutRRZExad+k4GYYsCfmEbvizS4jsRr3fdzqjEthpEwm7pmN7OgVzRbrktjrFw1lc0vM8 V7dyTJ71qlsd7v3KhmHzeJB35pqEOk2pEe5uPeCToNkmedmxcKbIj+MZzjFSsvCmimaMQB1uJJKa +hoWUi7aEFLvIxKxJavqpggXBIk2hr0608dIgnfG5ZEprqmH0b0YSy6jVXTCuIB+WER4d5BPVy9Q M4taX0RIlDYxQ2CjBuq78AAcHQf5qoKP8BXHnDnd/+ed5fS+csL4g3eWqECaL+8suy9r8hx7c+4L EegEWdqAWN1w1NezP34xsxLkvRRI0DRzKOg0U+BKfQY128YlYsbwSczEg2LqKxRmcgiwHdhc9MQJ IwKQHlgBejWeMGDYYxTOQUiJOmIjJbzIzHH6lAMP+y/fR0v1g4wx4St8fcqTt3gz5wc+xXFZZ3qI JpXI5iJk7xmNL2tYsDpcqu0375Snd5EKsIvg8u5szTOyZ4v06Ny2TZXRpHUSinh4IFp8Eoi7GINJ 02lPJnS/9jSxolJwp2slPMIEbjleWw3eec4XaetyEnSSqTPRZ9fVA0cPXMqzrPYQQyrRux3LaAh1 wujbgcObg1nt4iiJ5IMbc/WNPc280I2T4nTkdwG8H6iS5xO2WfsFsruBwf2QkgZlb6w7om2G65Lr r2Gl4dk63F8rCEHoUJ3fW+pU2Srjlmcbp+JXY3DMifEI22HcHAvT7zzXiMTr7VbUR5a2lZtJkk4k 1heZZFdru8ucCWMTr3Z4eNnjLm7LW7rcN7QjMpxrsCzjxndeyFUX7deIs3PQkgyH8k6luI0uUyLr va47TBjM4JmNHFzGPcP6BV6cYgQy8VQYZe5GmzZHMxyBYhGiUdekZQ/qwyxC3WGylQGdUpSf9ZCP a7qPdJd31fPRC0TOgzupO7nLuBGr2A02yuUQwt2KQG31sW8Gd9tQiHq+hPDt4OzJuY4pS8XRsepY tsd7dVEfJFmc15IYqwHverrpWyS1rFZibDPW1hUUb+85CGUzSBSTK8hpvee/ZxonW51TUXekMy3L uy25tMTg4mqbSLQQJ+skiQu2toIfBFYrOWql+EQipgfT15P1aq6FDK3xgSjIGWde0BPftYchDTdM i4QdudHFkN0u6fSKiT09QLv2mtSblt5nNzBR6UReePNs+khE4rHcXuoK21igUKHl1c3MXMgPu7y8 rKQDxR6N/rffXv+lROXet/9Q+l9I4D1U """) ##file distutils-init.py DISTUTILS_INIT = convert(""" eJytV1uL4zYUfvevOE0ottuMW9q3gVDa3aUMXXbLMlDKMBiNrSTqOJKRlMxkf33PkXyRbGe7Dw2E UXTu37lpxLFV2oIyifAncxmOL0xLIfcG+gv80x9VW6maw7o/CANSWWBwFtqeWMPlGY6qPjV8A0bB C4eKSTgZ5LRgFeyErMEeOBhbN+Ipgeizhjtnhkn7DdyjuNLPoCS0l/ayQTG0djwZC08cLXozeMss aG5EzQ0IScpnWtHSTXuxByV/QCmxE7y+eS0uxWeoheaVVfqSJHiU7Mhhi6gULbOHorshkrEnKxpT 0n3A8Y8SMpuwZx6aoix3ouFlmW8gHRSkeSJ2g7hU+kiHLDaQw3bmRDaTGfTnty7gPm0FHbIBg9U9 oh1kZzAFLaue2R6htPCtAda2nGlDSUJ4PZBgCJBGVcwKTAMz/vJiLD+Oin5Z5QlvDPdulC6EsiyE NFzb7McNTKJzbJqzphx92VKRFY1idenzmq3K0emRcbWBD0ryqc4NZGmKOOOX9Pz5x+/l27tP797c f/z0d+4NruGNai8uAM0bfsYaw8itFk8ny41jsfpyO+BWlpqfhcG4yxLdi/0tQqoT4a8Vby382mt8 p7XSo7aWGdPBc+b6utaBmCQ7rQKQoWtAuthQCiold2KfJIPTT8xwg9blPumc+YDZC/wYGdAyHpJk vUbHbHWAp5No6pK/WhhLEWrFjUwtPEv1Agf8YmnsuXUQYkeZoHm8ogP16gt2uHoxcEMdf2C6pmbw hUMsWGhanboh4IzzmsIpWs134jVPqD/c74bZHdY69UKKSn/+KfVhxLgUlToemayLMYQOqfEC61bh cbhwaqoGUzIyZRFHPmau5juaWqwRn3mpWmoEA5nhzS5gog/5jbcFQqOZvmBasZtwYlG93k5GEiyw buHhMWLjDarEGpMGB2LFs5nIJkhp/nUmZneFaRth++lieJtHepIvKgx6PJqIlD9X2j6pG1i9x3pZ 5bHuCPFiirGHeO7McvoXkz786GaKVzC9DSpnOxJdc4xm6NSVq7lNEnKdVlnpu9BNYoKX2Iq3wvgh gGEUM66kK6j4NiyoneuPLSwaCWDxczgaolEWpiMyDVDb7dNuLAbriL8ig8mmeju31oNvQdpnvEPC 1vAXbWacGRVrGt/uXN/gU0CDDwgooKRrHfTBb1/s9lYZ8ZqOBU0yLvpuP6+K9hLFsvIjeNhBi0KL MlOuWRn3FRwx5oHXjl0YImUx0+gLzjGchrgzca026ETmYJzPD+IpuKzNi8AFn048Thd63OdD86M6 84zE8yQm0VqXdbbgvub2pKVnS76icBGdeTHHXTKspUmr4NYo/furFLKiMdQzFjHJNcdAnMhltBJK 0/IKX3DVFqvPJ2dLE7bDBkH0l/PJ29074+F0CsGYOxsb7U3myTUncYfXqnLLfa6sJybX4g+hmcjO kMRBfA1JellfRRKJcyRpxdS4rIl6FdmQCWjo/o9Qz7yKffoP4JHjOvABcRn4CZIT2RH4jnxmfpVG qgLaAvQBNfuO6X0/Ux02nb4FKx3vgP+XnkX0QW9pLy/NsXgdN24dD3LxO2Nwil7Zlc1dqtP3d7/h kzp1/+7hGBuY4pk0XD/0Ao/oTe/XGrfyM773aB7iUhgkpy+dwAMalxMP0DrBcsVw/6p25+/hobP9 GBknrWExDhLJ1bwt1NcCNblaFbMKCyvmX0PeRaQ= """) ##file distutils.cfg DISTUTILS_CFG = convert(""" eJxNj00KwkAMhfc9xYNuxe4Ft57AjYiUtDO1wXSmNJnK3N5pdSEEAu8nH6lxHVlRhtDHMPATA4uH xJ4EFmGbvfJiicSHFRzUSISMY6hq3GLCRLnIvSTnEefN0FIjw5tF0Hkk9Q5dRunBsVoyFi24aaLg 9FDOlL0FPGluf4QjcInLlxd6f6rqkgPu/5nHLg0cXCscXoozRrP51DRT3j9QNl99AP53T2Q= """) ##file activate_this.py ACTIVATE_THIS = convert(""" eJyNU01v2zAMvetXEB4K21jnDOstQA4dMGCHbeihlyEIDMWmE62yJEiKE//7kXKdpEWLzYBt8evx kRSzLPs6wiEoswM8YdMpjUXcq1Dz6RZa1cSiTkJdr86GsoTRHuCotBayiWqQEYGtMCgfD1KjGYBe 5a3p0cRKiEe2NtLAFikftnDco0ko/SFEVgEZ8aRCZDIPY9xbA8pE9M4jfW/B2CjiHq9zbJVZuOQq siwTIvpxKYCembPAU4Muwi/Z4zfvrZ/MXipKeB8C+qisSZYiWfjJfs+0/MFMdWn1hJcO5U7G/SLa xVx8zU6VG/PXLXvfsyyzUqjeWR8hjGE+2iCE1W1tQ82hsCJN9dzKaoexyB/uH79TnjwvxcW0ntSb yZ8jq1Z5Q1UXsyy3gf9nbjTEj7NzQMfCJa/YSmrQ+2D/BqfiOi6sclrGzvoeVivIj8rcfcmnIQRF 7XCyeZI7DFe5/lhlCs5PRf5QW66VXT/NrlQ46oD/D6InkOmi3IQcbhKxAX2g4a+Xd5s3UtCtG2py m8eg6WYWqR6SL5OjKMGfSrYt/6kxxQtOpeAgj1LXBNmpE2ElmCSIy5H0zFd8gJ924HWijWhb2hRC 6wNEm1QdDZtuSZcEprIUBo/XRNcbQe1OUbQ/r3hPTaPJJDNtFLu8KHV5XoNr3Eo6h6YtOKw8e8yw VF5PnJ+ts3a9/Mz38RpG/AUSzYUW """) MH_MAGIC = 0xfeedface MH_CIGAM = 0xcefaedfe MH_MAGIC_64 = 0xfeedfacf MH_CIGAM_64 = 0xcffaedfe FAT_MAGIC = 0xcafebabe BIG_ENDIAN = '>' LITTLE_ENDIAN = '<' LC_LOAD_DYLIB = 0xc maxint = majver == 3 and getattr(sys, 'maxsize') or getattr(sys, 'maxint') class fileview(object): """ A proxy for file-like objects that exposes a given view of a file. Modified from macholib. """ def __init__(self, fileobj, start=0, size=maxint): if isinstance(fileobj, fileview): self._fileobj = fileobj._fileobj else: self._fileobj = fileobj self._start = start self._end = start + size self._pos = 0 def __repr__(self): return '' % ( self._start, self._end, self._fileobj) def tell(self): return self._pos def _checkwindow(self, seekto, op): if not (self._start <= seekto <= self._end): raise IOError("%s to offset %d is outside window [%d, %d]" % ( op, seekto, self._start, self._end)) def seek(self, offset, whence=0): seekto = offset if whence == os.SEEK_SET: seekto += self._start elif whence == os.SEEK_CUR: seekto += self._start + self._pos elif whence == os.SEEK_END: seekto += self._end else: raise IOError("Invalid whence argument to seek: %r" % (whence,)) self._checkwindow(seekto, 'seek') self._fileobj.seek(seekto) self._pos = seekto - self._start def write(self, bytes): here = self._start + self._pos self._checkwindow(here, 'write') self._checkwindow(here + len(bytes), 'write') self._fileobj.seek(here, os.SEEK_SET) self._fileobj.write(bytes) self._pos += len(bytes) def read(self, size=maxint): assert size >= 0 here = self._start + self._pos self._checkwindow(here, 'read') size = min(size, self._end - here) self._fileobj.seek(here, os.SEEK_SET) bytes = self._fileobj.read(size) self._pos += len(bytes) return bytes def read_data(file, endian, num=1): """ Read a given number of 32-bits unsigned integers from the given file with the given endianness. """ res = struct.unpack(endian + 'L' * num, file.read(num * 4)) if len(res) == 1: return res[0] return res def mach_o_change(path, what, value): """ Replace a given name (what) in any LC_LOAD_DYLIB command found in the given binary with a new name (value), provided it's shorter. """ def do_macho(file, bits, endian): # Read Mach-O header (the magic number is assumed read by the caller) cputype, cpusubtype, filetype, ncmds, sizeofcmds, flags = read_data(file, endian, 6) # 64-bits header has one more field. if bits == 64: read_data(file, endian) # The header is followed by ncmds commands for n in range(ncmds): where = file.tell() # Read command header cmd, cmdsize = read_data(file, endian, 2) if cmd == LC_LOAD_DYLIB: # The first data field in LC_LOAD_DYLIB commands is the # offset of the name, starting from the beginning of the # command. name_offset = read_data(file, endian) file.seek(where + name_offset, os.SEEK_SET) # Read the NUL terminated string load = file.read(cmdsize - name_offset).decode() load = load[:load.index('\0')] # If the string is what is being replaced, overwrite it. if load == what: file.seek(where + name_offset, os.SEEK_SET) file.write(value.encode() + '\0'.encode()) # Seek to the next command file.seek(where + cmdsize, os.SEEK_SET) def do_file(file, offset=0, size=maxint): file = fileview(file, offset, size) # Read magic number magic = read_data(file, BIG_ENDIAN) if magic == FAT_MAGIC: # Fat binaries contain nfat_arch Mach-O binaries nfat_arch = read_data(file, BIG_ENDIAN) for n in range(nfat_arch): # Read arch header cputype, cpusubtype, offset, size, align = read_data(file, BIG_ENDIAN, 5) do_file(file, offset, size) elif magic == MH_MAGIC: do_macho(file, 32, BIG_ENDIAN) elif magic == MH_CIGAM: do_macho(file, 32, LITTLE_ENDIAN) elif magic == MH_MAGIC_64: do_macho(file, 64, BIG_ENDIAN) elif magic == MH_CIGAM_64: do_macho(file, 64, LITTLE_ENDIAN) assert(len(what) >= len(value)) do_file(open(path, 'r+b')) if __name__ == '__main__': main() ## TODO: ## Copy python.exe.manifest ## Monkeypatch distutils.sysconfig ================================================ FILE: scripts/pylintrc ================================================ # lint Python modules using external checkers. # # This is the main checker controling the other ones and the reports # generation. It is itself both a raw checker and an astng checker in order # to: # * handle message activation / deactivation at the module level # * handle some basic but necessary stats'data (number of classes, methods...) # [MASTER] # Specify a configuration file. #rcfile= # Profiled execution. profile=no # Add to the black list. It should be a base name, not a # path. You may set this option multiple times. ignore=.svn # Pickle collected data for later comparisons. persistent=yes # Set the cache size for astng objects. cache-size=500 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. load-plugins= [MESSAGES CONTROL] # Enable only checker(s) with the given id(s). This option conflict with the # disable-checker option #enable-checker= # Enable all checker(s) except those with the given id(s). This option conflict # with the disable-checker option #disable-checker= # Enable all messages in the listed categories. #enable-msg-cat= # Disable all messages in the listed categories. #disable-msg-cat= # Enable the message(s) with the given id(s). #enable-msg= # Disable the message(s) with the given id(s). disable-msg=C0323,W0142,C0301,C0103,C0111,E0213,C0302,C0203,W0703,R0201 [REPORTS] # set the output format. Available formats are text, parseable, colorized and # html output-format=colorized # Include message's id in output include-ids=yes # Put messages in a separate file for each module / package specified on the # command line instead of printing them on stdout. Reports (if any) will be # written in a file name "pylint_global.[txt|html]". files-output=no # Tells wether to display a full report or only the messages reports=yes # Python expression which should return a note less than 10 (10 is the highest # note).You have access to the variables errors warning, statement which # respectivly contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Add a comment according to your evaluation note. This is used by the global # evaluation report (R0004). comment=no # Enable the report(s) with the given id(s). #enable-report= # Disable the report(s) with the given id(s). #disable-report= # checks for # * unused variables / imports # * undefined variables # * redefinition of variable from builtins or from an outer scope # * use of variable before assigment # [VARIABLES] # Tells wether we should check for unused import in __init__ files. init-import=no # A regular expression matching names used for dummy variables (i.e. not used). dummy-variables-rgx=_|dummy # List of additional names supposed to be defined in builtins. Remember that # you should avoid to define new builtins when possible. additional-builtins= # try to find bugs in the code using type inference # [TYPECHECK] # Tells wether missing members accessed in mixin class should be ignored. A # mixin class is detected if its name ends with "mixin" (case insensitive). ignore-mixin-members=yes # When zope mode is activated, consider the acquired-members option to ignore # access to some undefined attributes. zope=no # List of members which are usually get through zope's acquisition mecanism and # so shouldn't trigger E0201 when accessed (need zope=yes to be considered). acquired-members=REQUEST,acl_users,aq_parent # checks for : # * doc strings # * modules / classes / functions / methods / arguments / variables name # * number of arguments, local variables, branchs, returns and statements in # functions, methods # * required module attributes # * dangerous default values as arguments # * redefinition of function / method / class # * uses of the global statement # [BASIC] # Required attributes for module, separated by a comma required-attributes= # Regular expression which should only match functions or classes name which do # not require a docstring no-docstring-rgx=__.*__ # Regular expression which should only match correct module names module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ # Regular expression which should only match correct module level names const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ # Regular expression which should only match correct class names class-rgx=[A-Z_][a-zA-Z0-9]+$ # Regular expression which should only match correct function names function-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct method names method-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct instance attribute names attr-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct argument names argument-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct variable names variable-rgx=[a-z_][a-z0-9_]{2,30}$ # Regular expression which should only match correct list comprehension / # generator expression variable names inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ # Good variable names which should always be accepted, separated by a comma good-names=i,j,k,ex,Run,_ # Bad variable names which should always be refused, separated by a comma bad-names=foo,bar,baz,toto,tutu,tata # List of builtins function names that should not be used, separated by a comma bad-functions=apply,input # checks for sign of poor/misdesign: # * number of methods, attributes, local variables... # * size, complexity of functions, methods # [DESIGN] # Maximum number of arguments for function / method max-args=12 # Maximum number of locals for function / method body max-locals=30 # Maximum number of return / yield for function / method body max-returns=12 # Maximum number of branch for function / method body max-branchs=30 # Maximum number of statements in function / method body max-statements=60 # Maximum number of parents for a class (see R0901). max-parents=7 # Maximum number of attributes for a class (see R0902). max-attributes=20 # Minimum number of public methods for a class (see R0903). min-public-methods=0 # Maximum number of public methods for a class (see R0904). max-public-methods=20 # checks for # * external modules dependencies # * relative / wildcard imports # * cyclic imports # * uses of deprecated modules # [IMPORTS] # Deprecated modules which should not be used, separated by a comma deprecated-modules=regsub,string,TERMIOS,Bastion,rexec # Create a graph of every (i.e. internal and external) dependencies in the # given file (report R0402 must not be disabled) import-graph= # Create a graph of external dependencies in the given file (report R0402 must # not be disabled) ext-import-graph= # Create a graph of internal dependencies in the given file (report R0402 must # not be disabled) int-import-graph= # checks for : # * methods without self as first argument # * overridden methods signature # * access only to existant members via self # * attributes not defined in the __init__ method # * supported interfaces implementation # * unreachable code # [CLASSES] # List of interface methods to ignore, separated by a comma. This is used for # instance to not check methods defines in Zope's Interface base class. ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__,__new__,setUp # checks for similarities and duplicated code. This computation may be # memory / CPU intensive, so you should disable it if you experiments some # problems. # [SIMILARITIES] # Minimum lines number of a similarity. min-similarity-lines=10 # Ignore comments when computing similarities. ignore-comments=yes # Ignore docstrings when computing similarities. ignore-docstrings=yes # checks for: # * warning notes in the code like FIXME, XXX # * PEP 263: source code with non ascii character but no encoding declaration # [MISCELLANEOUS] # List of note tags to take in consideration, separated by a comma. notes=FIXME,XXX,TODO # checks for : # * unauthorized constructions # * strict indentation # * line length # * use of <> instead of != # [FORMAT] # Maximum number of characters on a single line. max-line-length=90 # Maximum number of lines in a module max-module-lines=1000 # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 # tab). indent-string=' ' ================================================ FILE: setup.cfg ================================================ [egg_info] tag_build = dev tag_date = true [bdist_wheel] universal = 1 [nosetests] # constrain nosetests to the tests dir for now: 0.10 ends up # overwriting pylons.config with the actual module where=tests cover-package=pylons cover-erase=True with-doctest=True nocapture=1 ================================================ FILE: setup.py ================================================ import sys try: from setuptools import setup, find_packages except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages version = '1.0.3' tests_require = ['nose', 'Jinja2>=2.2.1'] if not sys.platform.startswith('java'): tests_require.extend(['Genshi', 'coverage>=2.85']) setup( name="Pylons", version=version, description='Pylons Web Framework', long_description=""" Pylons ====== The Pylons web framework is designed for building web applications and sites in an easy and concise manner. They can range from as small as a single Python module, to a substantial directory layout for larger and more complex web applications. Pylons comes with project templates that help boot-strap a new web application project, or you can start from scratch and set things up exactly as desired. Example `Hello World` --------------------- .. from paste.httpserver import serve from pylons import Configurator, Response class Hello(object): def __init__(self, request): self.request = request def index(self): return Response(body="Hello World!") if __name__ == '__main__': config = Configurator() config.begin() config.add_handler('home', '/', handler=Hello, action='index') config.end() serve(config.make_wsgi_app(), host='0.0.0.0') Core Features ------------- * A framework to make writing web applications in Python easy * Utilizes a minimalist, component-based philosophy that makes it easy to expand on * Harness existing knowledge about Python * Extensible application design * Fast and efficient, an incredibly small per-request call-stack providing top performance * Uses existing and well tested Python packages Current Status -------------- Pylons 1.0 series is stable and production ready, but in maintenance-only mode. The Pylons Project now maintains the Pyramid web framework for future development. Pylons 1.0 users should strongly consider using Pyramid for their next project. Download and Installation ------------------------- Pylons can be installed with `Easy Install `_ by typing:: > easy_install Pylons Development Version ------------------- Pylons development uses the git distributed version control system (DVCS) with GitHub hosting the main repository here: `Pylons GitHub repository `_ Documentation ------------- http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/ """, keywords='web wsgi lightweight framework sqlalchemy formencode mako templates', license='BSD', author='Ben Bangert, Philip Jenvey, James Gardner', author_email='ben@groovie.org, pjenvey@underboss.org', url='https://github.com/Pylons/pylons', packages=find_packages(exclude=['ez_setup', 'tests', 'tests.*']), zip_safe=False, include_package_data=True, test_suite='nose.collector', tests_require=tests_require, install_requires=[ "Routes>=1.12.3", "WebHelpers>=0.6.4", "Beaker>=1.5.4", "Paste>=1.7.5.1", "PasteDeploy>=1.5.0", "PasteScript>=1.7.4.2", "FormEncode>=1.2.4", "simplejson>=2.2.1", "decorator>=3.3.2", "nose>=1.1.2", "Mako>=0.5.0", "WebError>=0.10.3", "WebTest>=1.3.1", "Tempita>=0.5.1", "MarkupSafe>=0.15", "WebOb>=1.1.1", ], classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Framework :: Pylons", "Programming Language :: Python", "Programming Language :: Python :: 2 :: Only", "Topic :: Internet :: WWW/HTTP", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Software Development :: Libraries :: Python Modules", ], extras_require={ 'genshi': ['Genshi>=0.6'], 'jinja2': ['Jinja2'], }, entry_points=""" [paste.paster_command] controller = pylons.commands:ControllerCommand restcontroller = pylons.commands:RestControllerCommand routes = pylons.commands:RoutesCommand shell = pylons.commands:ShellCommand [paste.paster_create_template] pylons = pylons.util:PylonsTemplate pylons_minimal = pylons.util:MinimalPylonsTemplate [paste.filter_factory] debugger = pylons.middleware:debugger_filter_factory [paste.filter_app_factory] debugger = pylons.middleware:debugger_filter_app_factory """, ) ================================================ FILE: test_files/__init__.py ================================================ ================================================ FILE: test_files/event_file.py ================================================ from pylons.events import NewRequest, NewResponse, subscriber @subscriber(NewRequest) def add_reggy(event): event.request.reg = True @subscriber(NewResponse) def add_respy(event): event.response.reg = True ================================================ FILE: test_files/sample_controllers/__init__.py ================================================ # ================================================ FILE: test_files/sample_controllers/controllers/__init__.py ================================================ # ================================================ FILE: test_files/sample_controllers/controllers/goodbye.py ================================================ import logging from pylons import request, response, session, tmpl_context as c, url from pylons.controllers import WSGIController from pylons.controllers.util import abort, redirect from webob import Response from webob.exc import HTTPNotFound log = logging.getLogger(__name__) class Smithy(WSGIController): def __init__(self): self._pylons_log_debug = True def index(self): return 'Hello World' __controller__ = 'Smithy' ================================================ FILE: test_files/sample_controllers/controllers/hello.py ================================================ import logging from pylons import request, response, session, tmpl_context as c, url from pylons.controllers import WSGIController from pylons.controllers.util import abort, redirect from pylons.templating import render_mako from webob import Response from webob.exc import HTTPNotFound log = logging.getLogger(__name__) class HelloController(WSGIController): def __init__(self): self._pylons_log_debug = True def index(self): return 'Hello World' def oops(self): raise Exception('oops') def abort(self): abort(404) def intro_template(self): return render_mako('/hello.html') def time_template(self): return render_mako('/time.html', cache_key='fred', cache_expire=20) def special_controller(environ, start_response): return HTTPNotFound() def empty_wsgi(environ, start_response): return def a_view(request): return Response('A View') ================================================ FILE: test_files/sample_controllers/controllers/i18nc.py ================================================ import datetime from pylons import request, response, session, url from pylons import tmpl_context as c from pylons import app_globals from pylons.i18n import _, get_lang, set_lang, LanguageError from pylons.controllers import WSGIController from pylons.controllers.util import abort, redirect class I18NcController(WSGIController): def set_lang(self): return self._set_lang(_) def set_lang_pylonscontext(self, pylons): return self._set_lang(lambda *args: pylons.translator.ugettext(*args)) def _set_lang(self, gettext): lang = request.GET['lang'] try: set_lang(lang) except (LanguageError, IOError), e: resp_unicode = gettext('Could not set language to "%(lang)s"') % {'lang': lang} else: session['lang'] = lang session.save() resp_unicode = gettext('Set language to "%(lang)s"') % {'lang': lang} return resp_unicode def i18n_index(self): obj = request._current_obj() locale_list = request.languages set_lang(request.languages) return unicode(_('basic index page')) def no_lang(self): set_lang(None) response.write(_('No language')) set_lang([]) response.write(_('No languages')) return '' def langs(self): locale_list = request.languages set_lang(request.languages) return str(get_lang()) ================================================ FILE: test_files/sample_controllers/i18n/es/LC_MESSAGES/sample_controllers.po ================================================ # -*- coding: utf-8 -*- msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2010-06-25 18:12-0700\n" "PO-Revision-Date: 2010-06-25 18:12-0700\n" "Last-Translator: FULL NAME \n" "Language-Team: es \n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.4\n" msgid "Hello" msgstr "¡Hola!" ================================================ FILE: test_files/sample_controllers/i18n/fr/LC_MESSAGES/sample_controllers.po ================================================ # -*- coding: utf-8 -*- msgid "" msgstr "" "Project-Id-Version: PROJECT VERSION\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "POT-Creation-Date: 2010-06-25 18:12-0700\n" "PO-Revision-Date: 2010-06-25 18:12-0700\n" "Last-Translator: FULL NAME \n" "Language-Team: fr \n" "Plural-Forms: nplurals=2; plural=(n > 1)\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Generated-By: Babel 0.9.4\n" msgid "Hello" msgstr "Bonjour" ================================================ FILE: test_files/sample_controllers/i18n/ja/LC_MESSAGES/sample_controllers.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-02-05 12:36+0900\n" "PO-Revision-Date: 2007-02-05 13:06+0900\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" #: controller_sample.py:7 msgid "basic index page" msgstr "根本インデクスページ" #: controller_sample.py:59 #, python-format msgid "Could not set language to \"%(lang)s\"" msgstr "「%(lang)s」に言語設定が変更できません" #: controller_sample.py:63 #, python-format msgid "Set language to \"%(lang)s\"" msgstr "言語設定を「%(lang)s」に変更しました" ================================================ FILE: test_files/sample_controllers/templates/hello.html ================================================ Hi there ${4 + 2} ================================================ FILE: test_files/sample_controllers/templates/time.html ================================================ <%! from datetime import datetime %> Hello, the time is ${datetime.now()} ================================================ FILE: tests/__init__.py ================================================ # ================================================ FILE: tests/conftest.py ================================================ import sys import os import shutil import pkg_resources here = os.path.dirname(__file__) base = os.path.dirname(here) sys.path.append(here) sys.path.insert(0, base) here = os.path.dirname(__file__) pkg_resources.working_set.add_entry(base) if not os.environ.get('PASTE_TESTING'): output_dir = os.path.join(here, 'test_webapps', 'output') if os.path.exists(output_dir): shutil.rmtree(output_dir) ================================================ FILE: tests/test_units/__init__.py ================================================ import json import os import sys from unittest import TestCase from urllib import quote_plus from xmlrpclib import loads, dumps data_dir = os.path.dirname(os.path.abspath(__file__)) try: shutil.rmtree(data_dir) except: pass cur_dir = os.path.dirname(os.path.abspath(__file__)) pylons_root = os.path.dirname(os.path.dirname(cur_dir)) test_root = os.path.join(pylons_root, 'test_files') sys.path.append(test_root) class TestMiddleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): if 'paste.testing_variables' not in environ: environ['paste.testing_variables'] = {} testenv = environ['paste.testing_variables'] testenv['environ'] = environ return self.app(environ, start_response) class TestWSGIController(TestCase): def setUp(self): import pylons from pylons.util import ContextObj, PylonsContext c = ContextObj() py_obj = PylonsContext() py_obj.tmpl_context = c py_obj.request = py_obj.response = None self.environ = {'pylons.routes_dict':dict(action='index'), 'paste.config':dict(global_conf=dict(debug=True)), 'pylons.pylons':py_obj} pylons.tmpl_context._push_object(c) def tearDown(self): import pylons pylons.tmpl_context._pop_object() def get_response(self, **kargs): test_args = kargs.pop('test_args', {}) url = kargs.pop('_url', '/') self.environ['pylons.routes_dict'].update(kargs) return self.app.get(url, extra_environ=self.environ, **test_args) def post_response(self, **kargs): url = kargs.pop('_url', '/') self.environ['pylons.routes_dict'].update(kargs) return self.app.post(url, extra_environ=self.environ, params=kargs) def xmlreq(self, method, args=None): if args is None: args = () ee = dict(CONTENT_TYPE='text/xml') data = dumps(args, methodname=method) self.response = response = self.app.post('/', params = data, extra_environ=ee) return loads(response.body)[0][0] def jsonreq(self, method, args=()): assert(isinstance(args, list) or isinstance(args, tuple) or isinstance(args, dict)) ee = dict(CONTENT_TYPE='application/json') data = json.dumps(dict(id='test', method=method, params=args)) self.response = response = self.app.post('/', params=quote_plus(data), extra_environ=ee) return json.loads(response.body) ================================================ FILE: tests/test_units/test_basic_app.py ================================================ import os import re import sys from nose.tools import raises from __init__ import test_root def make_app(global_conf, full_stack=True, static_files=True, include_cache_middleware=False, attribsafe=False, **app_conf): import pylons import pylons.configuration as configuration from beaker.cache import CacheManager from beaker.middleware import SessionMiddleware, CacheMiddleware from nose.tools import raises from paste.registry import RegistryManager from paste.deploy.converters import asbool from pylons.decorators import jsonify from pylons.middleware import ErrorHandler, StatusCodeRedirect from pylons.wsgiapp import PylonsApp from routes import Mapper from routes.middleware import RoutesMiddleware paths = dict(root=os.path.join(test_root, 'sample_controllers'), controllers=os.path.join(test_root, 'sample_controllers', 'controllers')) config = configuration.pylons_config config.init_app(global_conf, app_conf, package='sample_controllers', paths=paths) map = Mapper(directory=config['pylons.paths']['controllers']) map.connect('/{controller}/{action}') map.connect('/test_func', controller='sample_controllers.controllers.hello:special_controller') map.connect('/test_empty', controller='sample_controllers.controllers.hello:empty_wsgi') config['routes.map'] = map class AppGlobals(object): def __init__(self): self.cache = 'Nothing here but a string' config['pylons.app_globals'] = AppGlobals() if attribsafe: config['pylons.strict_tmpl_context'] = False app = PylonsApp(config=config) app = RoutesMiddleware(app, config['routes.map'], singleton=False) if include_cache_middleware: app = CacheMiddleware(app, config) app = SessionMiddleware(app, config) if asbool(full_stack): app = ErrorHandler(app, global_conf, **config['pylons.errorware']) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [401, 403, 404, 500]) app = RegistryManager(app) app.config = config return app class TestWsgiApp(object): def setUp(self): from paste.fixture import TestApp from routes.util import URLGenerator app = make_app({}) self.app = TestApp(app) self.url = URLGenerator(app.config['routes.map'], {}) def test_testvars(self): resp = self.app.get('/_test_vars', extra_environ={'paste.testing_variables': True}) assert re.match(r'^\d+$', resp.body) def test_exception_resp_attach(self): resp = self.app.get('/test_func', expect_errors=True) assert resp.status == 404 @raises(Exception) def test_no_content(self): resp = self.app.get('/test_empty', expect_errors=True) assert 'wontgethre' def test_middleware_cache_obj_instance(self): from paste.fixture import TestApp app = TestApp(make_app({}, include_cache_middleware=True)) resp = app.get('/hello/index') assert resp.cache def test_attribsafe_tmpl_context(self): from paste.fixture import TestApp app = TestApp(make_app({}, attribsafe=True)) resp = app.get('/hello/index') assert 'Hello World' in resp def test_cache_obj_appglobals(self): resp = self.app.get('/hello/index', extra_environ={'paste.testing_variables': True}) assert resp.cache == 'Nothing here but a string' def test_controller_name_override(self): resp = self.app.get('/goodbye/index') assert 'Hello World' in resp class TestJsonifyDecorator(object): def setUp(self): from paste.fixture import TestApp from routes.util import URLGenerator app = make_app({}) self.config = app.config self.app = TestApp(app) self.url = URLGenerator(app.config['routes.map'], {}) def test_basic_response(self): response = self.app.get('/hello/index') assert 'Hello World' in response def test_config(self): import pylons import pylons.configuration as configuration assert pylons.config == configuration.config @raises(AssertionError) def test_eval(self): from paste.fixture import TestApp app = TestApp(make_app(dict(debug='True'))) app.get('/hello/oops', status=500, extra_environ={'paste.throw_errors': False}) def test_set_lang(self): self._test_set_lang('set_lang') def test_set_lang_pylonscontext(self): self._test_set_lang('set_lang_pylonscontext') def _test_set_lang(self, action): response = self.app.get(self.url(controller='i18nc', action=action, lang='ja')) assert u'\u8a00\u8a9e\u8a2d\u5b9a\u3092\u300cja\u300d\u306b\u5909\u66f4\u3057\u307e\u3057\u305f'.encode('utf-8') in response response = self.app.get(self.url(controller='i18nc', action=action, lang='ch')) assert 'Could not set language to "ch"' in response def test_detect_lang(self): response = self.app.get(self.url(controller='i18nc', action='i18n_index'), headers={ 'Accept-Language':'fr;q=0.6, en;q=0.1, ja;q=0.3'}) # expect japanese fallback for nonexistent french. assert u'\u6839\u672c\u30a4\u30f3\u30c7\u30af\u30b9\u30da\u30fc\u30b8'.encode('utf-8') in response def test_no_lang(self): response = self.app.get(self.url(controller='i18nc', action='no_lang')) assert 'No language' in response assert 'No languages' in response def test_langs(self): response = self.app.get(self.url(controller='i18nc', action='langs'), headers={ 'Accept-Language':'fr;q=0.6, en;q=0.1, ja;q=0.3'}) assert "['fr', 'ja', 'en-us']" in response ================================================ FILE: tests/test_units/test_controller.py ================================================ # -*- coding: utf-8 -*- from paste.fixture import TestApp from paste.registry import RegistryManager from webob.exc import status_map import pylons from pylons.controllers import WSGIController from pylons.testutil import SetupCacheGlobal, ControllerWrap from __init__ import TestWSGIController, TestMiddleware class BasicWSGIController(WSGIController): def __init__(self): self._pylons_log_debug = True def __before__(self): pylons.response.headers['Cache-Control'] = 'private' def __after__(self): pylons.response.set_cookie('big_message', 'goodbye') def index(self): return 'hello world' def yield_fun(self): def its(): x = 0 while x < 100: yield 'hi' x += 1 return its() def strme(self): return "hi there" def use_redirect(self): pylons.response.set_cookie('message', 'Hello World') exc = status_map[301] raise exc('/elsewhere') def use_customnotfound(self): exc = status_map[404] raise exc('Custom not found') def header_check(self): pylons.response.headers['Content-Type'] = 'text/plain' return "Hello all!" def nothing(self): return def params(self): items = pylons.request.params.mixed().items() items.sort() return str(items) def list(self): return ['from', ' a ', 'list'] class FilteredWSGIController(WSGIController): def __init__(self): self.before = 0 self.after = 0 def __before__(self): self.before += 1 def __after__(self): self.after += 1 action = pylons.request.environ['pylons.routes_dict'].get('action') if action in ('after_response', 'after_string_response'): pylons.response.write(' from __after__') def index(self): return 'hi all, before is %s' % self.before def after_response(self): return 'hi' def after_string_response(self): return 'hello' class TestBasicWSGI(TestWSGIController): def __init__(self, *args, **kargs): TestWSGIController.__init__(self, *args, **kargs) self.baseenviron = {} app = ControllerWrap(BasicWSGIController) app = self.sap = SetupCacheGlobal(app, self.baseenviron) app = TestMiddleware(app) app = RegistryManager(app) self.app = TestApp(app) def setUp(self): TestWSGIController.setUp(self) self.baseenviron.update(self.environ) def test_wsgi_call(self): resp = self.get_response() assert 'hello world' in resp def test_yield_wrapper(self): resp = self.get_response(action='yield_fun') assert 'hi' * 100 in resp def test_404(self): self.environ['paste.config']['global_conf']['debug'] = False self.environ['pylons.routes_dict']['action'] = 'notthere' resp = self.app.get('/', status=404) assert resp.status == 404 def test_404exception(self): self.environ['paste.config']['global_conf']['debug'] = False self.environ['pylons.routes_dict']['action'] = 'use_customnotfound' resp = self.app.get('/', status=404) assert 'pylons.controller.exception' in resp.environ exc = resp.environ['pylons.controller.exception'] assert exc.detail == 'Custom not found' assert resp.status == 404 def test_private_func(self): self.baseenviron['pylons.routes_dict']['action'] = '_private' resp = self.app.get('/', status=404) assert resp.status == 404 def test_strme_func(self): self.baseenviron['pylons.routes_dict']['action'] = 'strme' resp = self.app.get('/') assert "hi there" in resp def test_header_check(self): self.baseenviron['pylons.routes_dict']['action'] = 'header_check' resp = self.app.get('/') assert "Hello all!" in resp assert resp.response.headers['Content-Type'] == 'text/plain' assert resp.response.headers['Cache-Control'] == 'private' assert resp.header('Content-Type') == 'text/plain' def test_head(self): self.baseenviron['pylons.routes_dict']['action'] = 'header_check' resp = self.app._gen_request('HEAD', '/') assert '' == resp.body assert resp.header('Content-Type') == 'text/plain' def test_redirect(self): self.baseenviron['pylons.routes_dict']['action'] = 'use_redirect' resp = self.app.get('/', status=301) def test_nothing(self): self.baseenviron['pylons.routes_dict']['action'] = 'nothing' resp = self.app.get('/') assert '' == resp.body assert resp.response.headers['Cache-Control'] == 'private' def test_unicode_action(self): self.baseenviron['pylons.routes_dict']['action'] = u'ОбсуждениеКомпаний' resp = self.app.get('/', status=404) def test_params(self): self.baseenviron['pylons.routes_dict']['action'] = u'params' resp = self.app.get('/?foo=bar') assert "'foo', u'bar')]" in resp, str(resp) resp = self.app.post('/?foo=bar', params=dict(snafu='snafoo')) assert "'foo', u'bar')" in resp, str(resp) assert "'snafu', u'snafoo')]" in resp, str(resp) resp = self.app.put('/?foo=bar', params=dict(snafu='snafoo')) assert "'foo', u'bar')" in resp, str(resp) assert "'snafu', u'snafoo')]" in resp, str(resp) def test_list(self): self.baseenviron['pylons.routes_dict']['action'] = 'list' assert 'from a list' in self.app.get('/') class TestFilteredWSGI(TestWSGIController): def __init__(self, *args, **kargs): TestWSGIController.__init__(self, *args, **kargs) self.baseenviron = {} app = ControllerWrap(FilteredWSGIController) app = self.sap = SetupCacheGlobal(app, self.baseenviron) app = RegistryManager(app) self.app = TestApp(app) def setUp(self): TestWSGIController.setUp(self) self.baseenviron.update(self.environ) def test_before(self): resp = self.get_response(action='index') assert 'hi' in resp assert 'before is 1' in resp def test_after_response(self): resp = self.get_response(action='after_response') assert 'hi from __after__' in resp def test_after_string_response(self): resp = self.get_response(action='after_string_response') assert 'hello from __after__' in resp def test_start_response(self): self.baseenviron['pylons.routes_dict']['action'] = 'start_response' self.app.get('/', status=404) ================================================ FILE: tests/test_units/test_decorator_authenticate_form.py ================================================ # -*- coding: utf-8 -*- import logging import logging.handlers import os from beaker.middleware import SessionMiddleware from paste.fixture import TestApp from paste.registry import RegistryManager from routes import request_config from __init__ import data_dir, TestWSGIController session_dir = os.path.join(data_dir, 'session') try: import shutil shutil.rmtree(session_dir) except: pass # Eat the logging handler messages my_logger = logging.getLogger() my_logger.setLevel(logging.INFO) # Add the log message handler to the logger class NullHandler(logging.Handler): def emit(self, record): pass my_logger.addHandler(NullHandler()) def make_protected(): from pylons.controllers import WSGIController from pylons.decorators.secure import authenticate_form from webhelpers.pylonslib import secure_form from pylons import request class ProtectedController(WSGIController): def form(self): request_config().environ = request.environ return secure_form.authentication_token() @authenticate_form def protected(self): request_config().environ = request.environ return 'Authenticated' return ProtectedController class TestAuthenticateFormDecorator(TestWSGIController): def setUp(self): from pylons.testutil import ControllerWrap, SetupCacheGlobal ProtectedController = make_protected() TestWSGIController.setUp(self) app = ControllerWrap(ProtectedController) app = SetupCacheGlobal(app, self.environ, setup_session=True) app = SessionMiddleware(app, {}, data_dir=session_dir) app = RegistryManager(app) self.app = TestApp(app) def test_unauthenticated(self): from pylons.decorators.secure import csrf_detected_message self.environ['pylons.routes_dict']['action'] = 'protected' response = self.app.post('/protected', extra_environ=self.environ, expect_errors=True) assert response.status == 403 assert csrf_detected_message in response def test_authenticated(self): from webhelpers.pylonslib import secure_form self.environ['pylons.routes_dict']['action'] = 'form' response = self.app.get('/form', extra_environ=self.environ) token = response.body self.environ['pylons.routes_dict']['action'] = 'protected' response = self.app.post('/protected', params={secure_form.token_key: token}, extra_environ=self.environ, expect_errors=True) assert 'Authenticated' in response self.environ['pylons.routes_dict']['action'] = 'protected' response = self.app.put('/protected', params={secure_form.token_key: token}, extra_environ=self.environ, expect_errors=True) assert 'Authenticated' in response # GET with token_key in query string response = self.app.get('/protected', params={secure_form.token_key: token}, extra_environ=self.environ, expect_errors=True) assert 'Authenticated' in response # POST with token_key in query string response = self.app.post('/protected?' + secure_form.token_key + '=' + token, extra_environ=self.environ, expect_errors=True) assert 'Authenticated' in response ================================================ FILE: tests/test_units/test_decorator_cache.py ================================================ import os import shutil import time from webtest import TestApp from paste.registry import RegistryManager from beaker.middleware import CacheMiddleware from __init__ import data_dir, TestWSGIController environ = {} sap = None def make_cache_controller(): global sap import pylons from pylons.decorators.cache import beaker_cache, create_cache_key from pylons.controllers import WSGIController, XMLRPCController from pylons.testutil import SetupCacheGlobal, ControllerWrap class CacheController(WSGIController): @beaker_cache(key=None, invalidate_on_startup=True) def test_default_cache_decorator_invalidate(self): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter @beaker_cache(key=None) def test_default_cache_decorator(self): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter def test_default_cache_decorator_func(self): def func(): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter func = beaker_cache(key=None)(func) return func() def test_response_cache_func(self, use_cache_status=True): pylons.response.status_int = 404 def func(): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter if use_cache_status: func = beaker_cache(key=None)(func) else: func = beaker_cache(key=None, cache_response=False)(func) return func() @beaker_cache(key=None, type='dbm') def test_dbm_cache_decorator(self): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter @beaker_cache(key="param", query_args=True) def test_get_cache_decorator(self): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter @beaker_cache(query_args=True) def test_get_cache_default(self): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter @beaker_cache(expire=1) def test_expire_cache_decorator(self): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter @beaker_cache(expire=1) def test_expire_dbm_cache_decorator(self): pylons.app_globals.counter += 1 return 'Counter=%s' % pylons.app_globals.counter @beaker_cache(key="id") def test_key_cache_decorator(self, id): pylons.app_globals.counter += 1 return 'Counter=%s, id=%s' % (pylons.app_globals.counter, id) @beaker_cache(key=["id", "id2"]) def test_keyslist_cache_decorator(self, id, id2="123"): pylons.app_globals.counter += 1 return 'Counter=%s, id=%s' % (pylons.app_globals.counter, id) def test_invalidate_cache(self): ns, key = create_cache_key(CacheController.test_default_cache_decorator) c = pylons.cache.get_cache(ns) c.remove_value(key) def test_invalidate_dbm_cache(self): ns, key = create_cache_key(CacheController.test_dbm_cache_decorator) c = pylons.cache.get_cache(ns, type='dbm') c.remove_value(key) @beaker_cache(cache_headers=('content-type','content-length', 'x-powered-by')) def test_header_cache(self): pylons.response.headers['Content-Type'] = 'application/special' pylons.response.headers['x-powered-by'] = 'pylons' pylons.response.headers['x-dont-include'] = 'should not be included' return "Hello folks, time is %s" % time.time() @beaker_cache(query_args=True) def test_cache_key_dupe(self): return "Hello folks, time is %s" % time.time() app = ControllerWrap(CacheController) app = sap = SetupCacheGlobal(app, environ, setup_cache=True) app = CacheMiddleware(app, {}, data_dir=cache_dir) app = RegistryManager(app) app = TestApp(app) # This one is missing cache middleware and the cache object to miss on purpsoe bad_app = ControllerWrap(CacheController) bad_app = SetupCacheGlobal(bad_app, environ, setup_cache=False) bad_app = RegistryManager(bad_app) bad_app = TestApp(bad_app) return app, bad_app cache_dir = os.path.join(data_dir, 'cache') try: shutil.rmtree(cache_dir) except: pass class TestBadCacheDecorator(TestWSGIController): def setUp(self): app, bad_app = make_cache_controller() self.app = bad_app TestWSGIController.setUp(self) environ.update(self.environ) def test_no_cache(self): self.assertRaises(Exception, lambda: self.get_response(action='test_default_cache_decorator')) class TestCacheDecorator(TestWSGIController): def setUp(self): app, bad_app = make_cache_controller() self.app = app TestWSGIController.setUp(self) environ.update(self.environ) def test_default_cache_decorator(self): sap.g.counter = 0 self.get_response(action='test_default_cache_decorator_invalidate') response = self.get_response(action='test_default_cache_decorator_invalidate') assert 'text/html' in response.headers['content-type'] assert 'Counter=1' in response response = self.get_response(action='test_default_cache_decorator_invalidate') assert 'Counter=1' in response def test_default_cache_decorator(self): sap.g.counter = 0 self.get_response(action='test_invalidate_cache') response = self.get_response(action='test_default_cache_decorator') assert 'text/html' in response.headers['content-type'] assert 'Counter=1' in response response = self.get_response(action='test_default_cache_decorator') assert 'Counter=1' in response response = self.get_response(action='test_get_cache_decorator', _url='/?param=123') assert 'Counter=2' in response response = self.get_response(action='test_get_cache_decorator', _url="/?param=123") assert 'Counter=2' in response response = self.get_response(action='test_expire_cache_decorator') assert 'Counter=3' in response response = self.get_response(action='test_expire_cache_decorator') assert 'Counter=3' in response time.sleep(2) response = self.get_response(action='test_expire_cache_decorator') assert 'Counter=4' in response response = self.get_response(action='test_key_cache_decorator', id=1) assert 'Counter=5' in response response = self.get_response(action='test_key_cache_decorator', id=2) assert 'Counter=6' in response response = self.get_response(action='test_key_cache_decorator', id=1) assert 'Counter=5' in response response = self.get_response(action='test_keyslist_cache_decorator', id=1, id2=2) assert 'Counter=7' in response response = self.get_response(action='test_keyslist_cache_decorator', id=1, id2=2) assert 'Counter=7' in response response = self.get_response(action='test_get_cache_default', _url='/?param=1243') assert 'Counter=8' in response response = self.get_response(action='test_get_cache_default', _url="/?param=1243") assert 'Counter=8' in response response = self.get_response(action='test_get_cache_default', _url="/?param=123") assert 'Counter=9' in response response = self.get_response(action='test_default_cache_decorator_func') assert 'text/html' in response.headers['content-type'] assert 'Counter=10' in response response = self.get_response(action='test_default_cache_decorator_func') assert 'Counter=10' in response response = self.get_response(action='test_response_cache_func', use_cache_status=True) assert 'Counter=10' in response response = self.get_response(action='test_response_cache_func', use_cache_status=False, test_args=dict(status=404)) assert 'Counter=10' in response def test_dbm_cache_decorator(self): sap.g.counter = 0 self.get_response(action="test_invalidate_dbm_cache") response = self.get_response(action="test_dbm_cache_decorator") assert "Counter=1" in response response = self.get_response(action="test_dbm_cache_decorator") assert "Counter=1" in response self.get_response(action="test_invalidate_dbm_cache") response = self.get_response(action="test_dbm_cache_decorator") assert "Counter=2" in response sap.g.counter = 0 response = self.get_response(action="test_expire_dbm_cache_decorator") assert "Counter=1" in response response = self.get_response(action="test_expire_dbm_cache_decorator") assert "Counter=1" in response time.sleep(2) response = self.get_response(action="test_expire_dbm_cache_decorator") assert "Counter=2" in response def test_cache_key(self): from pylons.decorators.cache import beaker_cache, create_cache_key key = create_cache_key(TestCacheDecorator.test_default_cache_decorator) assert key == ('%s.TestCacheDecorator' % self.__module__, 'test_default_cache_decorator') response = self.get_response(action='test_invalidate_cache') response = self.get_response(action='test_default_cache_decorator') assert 'Counter=1' in response response = self.get_response(action='test_default_cache_decorator') assert 'Counter=1' in response response = self.get_response(action='test_invalidate_cache') response = self.get_response(action='test_default_cache_decorator') assert 'Counter=2' in response def test_cache_key_dupe(self): response = self.get_response(action='test_cache_key_dupe', _url='/test_cache_key_dupe?id=1') time.sleep(0.1) response2 = self.get_response(action='test_cache_key_dupe', _url='/test_cache_key_dupe?id=2&id=1') assert str(response) != str(response2) def test_header_cache(self): response = self.get_response(action='test_header_cache') assert response.headers['content-type'] == 'application/special' assert response.headers['x-powered-by'] == 'pylons' assert 'x-dont-include' not in response.headers output = response.body time.sleep(1) response = self.get_response(action='test_header_cache') assert response.body == output assert response.headers['content-type'] == 'application/special' assert response.headers['x-powered-by'] == 'pylons' assert 'x-dont-include' not in response.headers def test_nocache(self): import pylons sap.g.counter = 0 pylons.config['cache_enabled'] = 'False' response = self.get_response(action='test_default_cache_decorator') assert 'Counter=1' in response response = self.get_response(action='test_default_cache_decorator') assert 'Counter=2' in response pylons.config['cache_enabled'] = 'True' ================================================ FILE: tests/test_units/test_decorator_https.py ================================================ from paste.fixture import TestApp from paste.registry import RegistryManager from routes.middleware import RoutesMiddleware from __init__ import TestWSGIController def make_httpscontroller(): from pylons import request, url from pylons.controllers import WSGIController from pylons.decorators.secure import https class HttpsController(WSGIController): @https('/pylons') def index(self): return 'index page' @https(lambda: url(controller='auth', action='login')) def login2(self): return 'login2 page' @https(lambda: request.url) def secure(self): return 'secure page' @https() def get(self): return 'get page' return HttpsController class TestHttpsDecorator(TestWSGIController): def setUp(self): from pylons.testutil import ControllerWrap, SetupCacheGlobal HttpsController = make_httpscontroller() TestWSGIController.setUp(self) from routes import Mapper map = Mapper() map.connect('/:action') map.connect('/:action/:id') map.connect('/:controller/:action/:id') map.connect('/:controller/:action') app = ControllerWrap(HttpsController) app = SetupCacheGlobal(app, self.environ, setup_cache=False) app = RoutesMiddleware(app, map) app = RegistryManager(app) self.app = TestApp(app) def test_https_explicit_path(self): self.environ['pylons.routes_dict']['action'] = 'index' response = self.app.get('/index', status=302) assert response.header_dict.get('location') == \ 'https://localhost/pylons' self.environ['wsgi.url_scheme'] = 'https' response = self.app.get('/index', status=200) assert 'location' not in response.header_dict assert 'index page' in response def test_https_disallows_post(self): self.environ['pylons.routes_dict']['action'] = 'index' response = self.app.post('/index', status=405) def test_https_callable(self): self.environ['pylons.routes_dict']['action'] = 'login2' response = self.app.get('/login2', status=302) assert response.header_dict.get('location') == \ 'https://localhost/auth/login' self.environ['wsgi.url_scheme'] = 'https' response = self.app.get('/login2', status=200) assert 'location' not in response.header_dict assert 'login2 page' in response def test_https_callable_current(self): self.environ['pylons.routes_dict']['action'] = 'secure' response = self.app.get('/secure', status=302) assert response.header_dict.get('location') == \ 'https://localhost/secure' self.environ['wsgi.url_scheme'] = 'https' response = self.app.get('/secure', status=200) assert 'location' not in response.header_dict assert 'secure page' in response def test_https_redirect_to_self(self): self.environ['pylons.routes_dict']['action'] = 'get' response = self.app.get('/get', status=302) assert response.header_dict.get('location') == \ 'https://localhost/get' self.environ['wsgi.url_scheme'] = 'https' response = self.app.get('/get', status=200) assert 'location' not in response.header_dict assert 'get page' in response ================================================ FILE: tests/test_units/test_decorator_jsonify.py ================================================ import warnings from paste.fixture import TestApp from paste.registry import RegistryManager from __init__ import TestWSGIController def make_cache_controller_app(): from pylons.testutil import ControllerWrap, SetupCacheGlobal from pylons.decorators import jsonify from pylons.controllers import WSGIController class CacheController(WSGIController): @jsonify def test_bad_json(self): return ["this is neat"] @jsonify def test_bad_json2(self): return ("this is neat",) @jsonify def test_good_json(self): return dict(fred=42) environ = {} app = ControllerWrap(CacheController) app = sap = SetupCacheGlobal(app, environ) app = RegistryManager(app) app = TestApp(app) return app, environ class TestJsonifyDecorator(TestWSGIController): def setUp(self): self.app, environ = make_cache_controller_app() TestWSGIController.setUp(self) environ.update(self.environ) warnings.simplefilter('error', Warning) def tearDown(self): warnings.simplefilter('always', Warning) def test_bad_json(self): for action in 'test_bad_json', 'test_bad_json2': try: response = self.get_response(action=action) except Warning, msg: assert 'JSON responses with Array envelopes are' in msg[0] def test_good_json(self): response = self.get_response(action='test_good_json') assert '{"fred": 42}' in response assert response.header('Content-Type') == 'application/json; charset=utf-8' ================================================ FILE: tests/test_units/test_decorator_validate.py ================================================ # -*- coding: utf-8 -*- import formencode from formencode.htmlfill import html_quote from paste.fixture import TestApp from paste.registry import RegistryManager from __init__ import TestWSGIController def custom_error_formatter(error): return '

%s

\n' % html_quote(error) class NetworkForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = True new_network = formencode.validators.URL(not_empty=True) class HelloForm(formencode.Schema): hello = formencode.ForEach(formencode.validators.Int()) def make_validating_controller(): from pylons.decorators import validate from pylons.controllers import WSGIController class ValidatingController(WSGIController): def new_network(self): return """
Network
""" @validate(schema=NetworkForm, form='new_network') def network(self): return 'Your network is: %s' % self.form_result.get('new_network') def view_hello(self): return """
Hello Bad Hello! 
""" @validate(schema=HelloForm(), post_only=False, form='view_hello') def hello(self): return str(self.form_result) @validate(schema=HelloForm(), post_only=False, form='view_hello', auto_error_formatter=custom_error_formatter) def hello_custom(self): return str(self.form_result) @validate(schema=NetworkForm, form='hello_recurse') def hello_recurse(self, environ): if environ['REQUEST_METHOD'] == 'GET': return self.new_network() else: return 'Your network is: %s' % self.form_result.get('new_network') return ValidatingController class TestValidateDecorator(TestWSGIController): def setUp(self): from pylons.testutil import ControllerWrap, SetupCacheGlobal ValidatingController = make_validating_controller() TestWSGIController.setUp(self) app = SetupCacheGlobal(ControllerWrap(ValidatingController), self.environ) app = RegistryManager(app) self.app = TestApp(app) def test_network_validated(self): response = self.post_response(action='network', new_network='http://pylonshq.com/') assert 'Your network is: http://pylonshq.com/' in response def test_network_failed_validation_non_ascii(self): response = self.post_response(action='network', new_network='Росси́я') assert 'You must provide a full domain name' in response assert 'Росси́я' in response def test_recurse_validated(self): response = self.post_response(action='hello_recurse', new_network='http://pylonshq.com/') assert 'Your network is: http://pylonshq.com/' in response def test_hello(self): self.environ['pylons.routes_dict']['action'] = 'hello' response = self.app.post('/hello?hello=1&hello=2&hello=3', extra_environ=self.environ) assert "'hello': [1, 2, 3]" in response def test_hello_failed(self): self.environ['pylons.routes_dict']['action'] = 'hello' response = self.app.post('/hello?hello=1&hello=2&hello=hi', extra_environ=self.environ) assert 'Bad Hello! ' in response assert "[None, None, u'Please enter an integer value']" in response def test_hello_custom_failed(self): self.environ['pylons.routes_dict']['action'] = 'hello_custom' response = \ self.app.post('/hello_custom?hello=1&hello=2&hello=hi', extra_environ=self.environ) assert 'Bad Hello! ' in response assert "[None, None, u'Please enter an integer value']" in response assert ("""

[None, None, u'Please enter """ """an integer value']

""") in response ================================================ FILE: tests/test_units/test_helpers.py ================================================ import warnings from unittest import TestCase from paste.fixture import TestApp from paste.httpexceptions import HTTPMovedPermanently from paste.registry import RegistryManager from __init__ import TestWSGIController def make_helperscontroller(): import pylons from pylons.controllers import WSGIController from pylons.controllers.util import etag_cache class HelpersController(WSGIController): def test_etag_cache(self): etag_cache('test') return "from etag_cache" return HelpersController class TestHelpers(TestWSGIController): def __init__(self, *args, **kargs): from pylons.testutil import ControllerWrap, SetupCacheGlobal HelpersController = make_helperscontroller() TestWSGIController.__init__(self, *args, **kargs) self.baseenviron = {} app = ControllerWrap(HelpersController) app = self.sap = SetupCacheGlobal(app, self.baseenviron) app = RegistryManager(app) self.app = TestApp(app) def setUp(self): TestWSGIController.setUp(self) self.baseenviron.update(self.environ) warnings.simplefilter('error', DeprecationWarning) def tearDown(self): warnings.simplefilter('always', DeprecationWarning) def test_return_etag_cache(self): self.baseenviron['pylons.routes_dict']['action'] = 'test_etag_cache' response = self.app.get('/') assert '"test"' == response.header('Etag') assert 'from etag_cache' in response ================================================ FILE: tests/test_units/test_i18n.py ================================================ # -*- coding: utf-8 -*- import os import sys from paste.fixture import TestApp from __init__ import test_root lang_setup = None def setup_py_trans(): global lang_setup import pylons from pylons.i18n.translation import _get_translator root = os.path.join(test_root, 'sample_controllers') lang_setup = {'pylons.paths': {'root': root}, 'pylons.package': 'sample_controllers'} sys.path.append(test_root) pylons.translator._push_object(_get_translator(None, pylons_config=lang_setup)) glob_set = [] class TestI18N(object): def setUp(self): setup_py_trans() def test_lazify(self): from pylons.i18n.translation import lazify def show_str(st): return '%s%s' % (st, len(glob_set)) lazy_show_str = lazify(show_str) result1 = lazy_show_str('fred') result2 = show_str('fred') assert str(result1) == str(result2) glob_set.append('1') assert str(result1) != str(result2) def test_noop(self): import pylons from pylons.i18n.translation import _, N_, set_lang foo = N_('Hello') class Bar(object): def __init__(self): self.local_foo = _(foo) assert Bar().local_foo == 'Hello' t = set_lang('fr', set_environ=False, pylons_config=lang_setup) pylons.translator._push_object(t) assert Bar().local_foo == 'Bonjour' t = set_lang('es', set_environ=False, pylons_config=lang_setup) pylons.translator._push_object(t) assert Bar().local_foo == u'¡Hola!' assert foo == 'Hello' ================================================ FILE: tests/test_units/test_jsonrpc.py ================================================ # -*- coding: utf-8 -*- from paste.fixture import TestApp from paste.registry import RegistryManager import webob.exc as exc import json from __init__ import TestWSGIController def make_basejsonrpc(): from pylons.controllers import JSONRPCController, JSONRPCError class BaseJSONRPCController(JSONRPCController): def __init__(self): self._pylons_log_debug = True def echo(self, message): return message def int_arg_check(self, arg): if not isinstance(arg, int): raise JSONRPCError(1, 'That is not an integer') else: return 'got an integer' def return_garbage(self): return JSONRPCController def subtract(self, x, y): if not isinstance(x, int) and not isinstance(y, int): raise JSONRPCError(1, 'That is not an integer') else: return x - y def v2_echo(self, message='Default message'): return message def v2_int_arg_check(self, arg=99): if not isinstance(arg, int): raise JSONRPCError(1, 'That is not an integer') else: return 'got an integer' def v2_decrement(self, x, y=1): """Like subtract, but decrements by default.""" if not isinstance(x, int) and not isinstance(y, int): raise JSONRPCError(1, 'That is not an integer') else: return x - y def _private(self): return 'private method' return BaseJSONRPCController class TestJSONRPCController(TestWSGIController): def __init__(self, *args, **kwargs): from pylons.testutil import ControllerWrap, SetupCacheGlobal BaseJSONRPCController = make_basejsonrpc() TestWSGIController.__init__(self, *args, **kwargs) self.baseenviron = {} self.baseenviron['pylons.routes_dict'] = {} app = ControllerWrap(BaseJSONRPCController) app = self.sap = SetupCacheGlobal(app, self.baseenviron) app = RegistryManager(app) self.app = TestApp(app) def test_echo(self): response = self.jsonreq('echo', args=('hello, world',)) assert dict(jsonrpc='2.0', id='test', result='hello, world') == response def test_int_arg_check(self): response = self.jsonreq('int_arg_check', args=('1',)) assert dict(jsonrpc='2.0', id='test', error={'code': 1, 'message': 'That is not an integer'}) == response def test_return_garbage(self): response = self.jsonreq('return_garbage') assert dict(jsonrpc='2.0', id='test', error={'code': -32603, 'message': "Internal error"}) == response def test_private_method(self): response = self.jsonreq('_private') assert dict(jsonrpc='2.0', id='test', error={'code': -32601, 'message': "Method not found"}) == response def test_content_type(self): response = self.jsonreq('echo', args=('foo',)) assert self.response.header('Content-Type') == 'application/json' def test_missing_method(self): response = self.jsonreq('foo') assert dict(jsonrpc='2.0', id='test', error={'code': -32601, 'message': "Method not found"}) == response def test_no_content_length(self): data = json.dumps(dict(jsonrpc='2.0', id='test', method='echo', args=('foo',))) self.assertRaises(exc.HTTPLengthRequired, lambda: self.app.post('/', extra_environ=\ dict(CONTENT_LENGTH=''))) def test_zero_content_length(self): data = json.dumps(dict(jsonrpc='2.0', id='test', method='echo', args=('foo',))) self.assertRaises(exc.HTTPLengthRequired, lambda: self.app.post('/', extra_environ=\ dict(CONTENT_LENGTH='0'))) def test_positional_params(self): response = self.jsonreq('subtract', args=[4, 2]) assert dict(jsonrpc='2.0', id='test', result=2) == response def test_missing_positional_param(self): response = self.jsonreq('subtract', args=[1]) assert dict(jsonrpc='2.0', id='test', error={'code': -32602, 'message': "Invalid params"}) == response def test_wrong_param_type(self): response = self.jsonreq('subtract', args=['1', '2']) assert dict(jsonrpc='2.0', id='test', error={'code': 1, 'message': "That is not an integer"}) == response def test_v2_echo(self): response = self.jsonreq('v2_echo', args={'message': 'hello, world'}) assert dict(jsonrpc='2.0', id='test', result='hello, world') == response def test_v2_echo_default(self): response = self.jsonreq('v2_echo', args={}) assert dict(jsonrpc='2.0', id='test', result='Default message') == response def test_v2_int_arg_check_valid(self): response = self.jsonreq('v2_int_arg_check', args={'arg': 5}) assert dict(jsonrpc='2.0', id='test', result='got an integer') def test_v2_int_arg_check_default_keyword_argument(self): response = self.jsonreq('v2_int_arg_check', args={}) assert dict(jsonrpc='2.0', id='test', result='got an integer') def test_v2_int_arg_check(self): response = self.jsonreq('v2_int_arg_check', args={'arg': 'abc'}) assert dict(jsonrpc='2.0', id='test', error={'code': 1, 'message': "That is not an integer"}) == response def test_v2_decrement(self): response = self.jsonreq('v2_decrement', args={'x': 50, 'y': 100}) assert dict(jsonrpc='2.0', id='test', result=-50) == response def test_v2_decrement_default_keywoard_argument(self): response = self.jsonreq('v2_decrement', args={'x': 50}) assert dict(jsonrpc='2.0', id='test', result=49) == response def test_v2_decrement_missing_keyword_argument(self): response = self.jsonreq('v2_decrement', args={}) assert dict(jsonrpc='2.0', id='test', error={'code': -32602, 'message': "Invalid params"}) == response ================================================ FILE: tests/test_units/test_middleware.py ================================================ # -*- coding: utf-8 -*- from webtest import TestApp def simple_app(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) return ['Hello world!'] def simple_exception_app(environ, start_response): if environ['PATH_INFO'].startswith('/error/document'): start_response('200 OK', [('Content-type', 'text/plain')]) return ['Made it to the error'] else: start_response('404 Not Found', [('Content-type', 'text/plain')]) return ['No page found!'] def test_plain_wrap(): from pylons.middleware import StatusCodeRedirect app = TestApp(StatusCodeRedirect(simple_app)) res = app.get('/') assert res.status_int == 200 def test_status_intercept(): from pylons.middleware import StatusCodeRedirect app = TestApp(StatusCodeRedirect(simple_exception_app)) res = app.get('/', status=404) assert 'Made it to the error' in res def test_original_path(): from pylons.middleware import StatusCodeRedirect app = TestApp(StatusCodeRedirect(simple_exception_app)) res = app.get('/', status=404) if getattr(res, 'environ', None) is not None: # webob<1.2 assert res.environ['PATH_INFO'] == '/' def test_retains_response(): from pylons.middleware import StatusCodeRedirect app = TestApp(StatusCodeRedirect(simple_exception_app)) res = app.get('/', status=404) if getattr(res, 'environ', None) is not None: # webob<1.2 assert 'pylons.original_response' in res.environ assert 'No page found!' in res.environ['pylons.original_response'].body def test_retains_request(): from pylons.middleware import StatusCodeRedirect app = TestApp(StatusCodeRedirect(simple_exception_app)) res = app.get('/fredrick', status=404) if getattr(res, 'environ', None) is not None: # webob<1.2 assert 'pylons.original_request' in res.environ assert '/fredrick' == res.environ['pylons.original_request'].path_info ================================================ FILE: tests/test_units/test_templating.py ================================================ import os import re import sys from beaker.cache import CacheManager from beaker.middleware import SessionMiddleware, CacheMiddleware from mako.lookup import TemplateLookup from nose.tools import raises from paste.fixture import TestApp from paste.registry import RegistryManager from paste.deploy.converters import asbool from routes import Mapper from routes.middleware import RoutesMiddleware from nose.tools import raises from __init__ import test_root def make_app(global_conf, full_stack=True, static_files=True, include_cache_middleware=False, attribsafe=False, **app_conf): import pylons import pylons.configuration as configuration from pylons import url from pylons.decorators import jsonify from pylons.middleware import ErrorHandler, StatusCodeRedirect from pylons.error import handle_mako_error from pylons.wsgiapp import PylonsApp root = os.path.dirname(os.path.abspath(__file__)) paths = dict(root=os.path.join(test_root, 'sample_controllers'), controllers=os.path.join(test_root, 'sample_controllers', 'controllers'), templates=os.path.join(test_root, 'sample_controllers', 'templates')) sys.path.append(test_root) config = configuration.PylonsConfig() config.init_app(global_conf, app_conf, package='sample_controllers', paths=paths) map = Mapper(directory=config['pylons.paths']['controllers']) map.connect('/{controller}/{action}') config['routes.map'] = map class AppGlobals(object): pass config['pylons.app_globals'] = AppGlobals() config['pylons.app_globals'].mako_lookup = TemplateLookup( directories=paths['templates'], imports=['from markupsafe import escape'] ) if attribsafe: config['pylons.strict_tmpl_context'] = False app = PylonsApp(config=config) app = RoutesMiddleware(app, config['routes.map'], singleton=False) if include_cache_middleware: app = CacheMiddleware(app, config) app = SessionMiddleware(app, config) if asbool(full_stack): app = ErrorHandler(app, global_conf, **config['pylons.errorware']) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [401, 403, 404, 500]) app = RegistryManager(app) app.config = config return app class TestTemplatingApp(object): def setUp(self): self.app = TestApp(make_app({'cache_dir': os.path.join(os.path.dirname(__file__), 'cache')}, include_cache_middleware=True)) def test_testvars(self): resp = self.app.get('/hello/intro_template') assert 'Hi there 6' in resp def test_template_cache(self): resp = self.app.get('/hello/time_template') resp2 = self.app.get('/hello/time_template') assert resp.body == resp2.body ================================================ FILE: tests/test_units/test_xmlrpc.py ================================================ # -*- coding: utf-8 -*- from paste.fixture import TestApp from paste.registry import RegistryManager import webob.exc as exc import xmlrpclib from __init__ import TestWSGIController def make_basexmlrpc(): from pylons.controllers import XMLRPCController class BaseXMLRPCController(XMLRPCController): def __init__(self): self._pylons_log_debug = True foo = 'bar' def userstatus(self): return 'basic string' userstatus.signature = [ ['string'] ] def docs(self): "This method has a docstring" return dict(mess='a little somethin', a=1, b=[1,2,3], c=('all','the')) docs.signature = [ ['struct'] ] def uni(self): "This method has a docstring" return dict(mess=u'A unicode string, oh boy') uni.signature = [ ['struct'] ] def intargcheck(self, arg): if not isinstance(arg, int): return xmlrpclib.Fault(0, 'Integer required') else: return "received int" intargcheck.signature = [ ['string', 'int'] ] def nosig(self): return 'not much' def structured_methodname(self, arg): "This method has a docstring" return 'Transform okay' structured_methodname.signature = [ ['string', 'string'] ] def longdoc(self): """This function has multiple lines in it""" return "hi all" def _private(self): return 'private method' return BaseXMLRPCController class TestXMLRPCController(TestWSGIController): def __init__(self, *args, **kargs): from pylons.testutil import ControllerWrap, SetupCacheGlobal BaseXMLRPCController = make_basexmlrpc() TestWSGIController.__init__(self, *args, **kargs) self.baseenviron = {} self.baseenviron['pylons.routes_dict'] = {} app = ControllerWrap(BaseXMLRPCController) app = self.sap = SetupCacheGlobal(app, self.baseenviron) app = RegistryManager(app) self.app = TestApp(app) def test_index(self): response = self.xmlreq('userstatus') assert response == 'basic string' def test_structure(self): response = self.xmlreq('docs') assert dict(mess='a little somethin', a=1, b=[1,2,3], c=['all','the']) == response def test_methodhelp(self): response = self.xmlreq('system.methodHelp', ('docs',)) assert "This method has a docstring" in response def test_methodhelp_with_structured_methodname(self): response = self.xmlreq('system.methodHelp', ('structured.methodname',)) assert "This method has a docstring" in response def test_methodsignature(self): response = self.xmlreq('system.methodSignature', ('docs',)) assert [['struct']] == response def test_methodsignature_with_structured_methodname(self): response = self.xmlreq('system.methodSignature', ('structured.methodname',)) assert [['string', 'string']] == response def test_listmethods(self): response = self.xmlreq('system.listMethods') assert response == ['docs', 'intargcheck', 'longdoc', 'nosig', 'structured.methodname', 'system.listMethods', 'system.methodHelp', 'system.methodSignature', 'uni', 'userstatus'] def test_unicode(self): response = self.xmlreq('uni') assert 'A unicode string' in response['mess'] def test_unicode_method(self): data = xmlrpclib.dumps((), methodname=u'ОбсуждениеКомпаний') self.response = response = self.app.post('/', params=data, extra_environ=dict(CONTENT_TYPE='text/xml')) def test_no_length(self): data = xmlrpclib.dumps((), methodname=u'ОбсуждениеКомпаний') self.assertRaises(exc.HTTPLengthRequired, lambda: self.app.post('/', extra_environ=dict(CONTENT_LENGTH=''))) def test_too_big(self): data = xmlrpclib.dumps((), methodname=u'ОбсуждениеКомпаний') self.assertRaises(exc.HTTPRequestEntityTooLarge, lambda: self.app.post('/', extra_environ=dict(CONTENT_LENGTH='4194314'))) def test_badargs(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, 'system.methodHelp') def test_badarity(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, 'system.methodHelp') # Unsure whether this is actually picked up by xmlrpclib, but what the hey def test_bad_paramval(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, 'intargcheck', (12.5,)) def test_missingmethod(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, 'doesntexist') def test_nosignature(self): response = self.xmlreq('system.methodSignature', ('nosig',)) assert response == '' def test_nosignature_unicode(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, 'system.methodSignature', (u'ОбсуждениеКомпаний',)) def test_nodocs(self): response = self.xmlreq('system.methodHelp', ('nosig',)) assert response == '' def test_nodocs_unicode(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, 'system.methodHelp', (u'ОбсуждениеКомпаний',)) def test_multilinedoc(self): response = self.xmlreq('system.methodHelp', ('longdoc',)) assert 'This function\nhas multiple lines\nin it' in response def test_contenttype(self): response = self.xmlreq('system.methodHelp', ('longdoc',)) assert self.response.header('Content-Type') == 'text/xml' def test_start_response(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, 'start_response') def test_private_func(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, '_private') def test_var(self): self.assertRaises(xmlrpclib.Fault, self.xmlreq, 'foo') ================================================ FILE: tests/test_webapps/__init__.py ================================================ ================================================ FILE: tests/test_webapps/filestotest/app_globals.py ================================================ """The application's Globals object""" from pylons import config from beaker.cache import CacheManager from beaker.util import parse_cache_config_options class Globals(object): """Globals acts as a container for objects available throughout the life of the application """ def __init__(self, config): """One instance of Globals is created during application initialization and is available during requests via the 'app_globals' variable """ self.cache = CacheManager(**parse_cache_config_options(config)) self.message = 'Hello' self.counter = 0 ================================================ FILE: tests/test_webapps/filestotest/base_with_xmlrpc.py ================================================ from pylons import tmpl_context as c, app_globals, cache, request, session from pylons.controllers import WSGIController, XMLRPCController from pylons.controllers.util import abort, etag_cache, redirect from pylons.decorators import jsonify, validate from pylons.templating import render_mako as render from pylons.i18n import N_, _, ungettext import projectname.model as model import projectname.lib.helpers as h class BaseController(WSGIController): def __call__(self, environ, start_response): # Insert any code to be run per request here. The Routes match # is under environ['pylons.routes_dict'] should you want to check # the action or route vars here return WSGIController.__call__(self, environ, start_response) # Include the '_' function in the public names __all__ = [__name for __name in locals().keys() if not __name.startswith('_') \ or __name == '_'] ================================================ FILE: tests/test_webapps/filestotest/cache_controller.py ================================================ from pylons import app_globals from pylons.decorators.cache import beaker_cache from projectname.lib.base import BaseController class CacheController(BaseController): @beaker_cache(key=None) def test_default_cache_decorator(self): app_globals.counter += 1 return 'Counter=%s' % app_globals.counter @beaker_cache(key="param", query_args=True) def test_get_cache_decorator(self): app_globals.counter += 1 return 'Counter=%s' % app_globals.counter @beaker_cache(expire=4) def test_expire_cache_decorator(self): app_globals.counter += 1 return 'Counter=%s' % app_globals.counter @beaker_cache(key="id") def test_key_cache_decorator(self, id): app_globals.counter += 1 return 'Counter=%s, id=%s' % (app_globals.counter, id) @beaker_cache(key=["id", "id2"]) def test_keyslist_cache_decorator(self, id, id2="123"): app_globals.counter += 1 return 'Counter=%s, id=%s' % (app_globals.counter, id) ================================================ FILE: tests/test_webapps/filestotest/controller_sample.py ================================================ import datetime from projectname.lib.base import * import projectname.lib.helpers as h from pylons import request, response, session, url from pylons import tmpl_context as c from pylons import app_globals from pylons.decorators import rest from pylons.i18n import _, get_lang, set_lang, LanguageError from pylons.templating import render_mako, render_genshi, render_jinja2 from pylons.controllers.util import abort, redirect class SampleController(BaseController): def index(self): return 'basic index page' def session_increment(self): session.setdefault('counter', -1) session['counter'] += 1 session.save() return 'session incrementer' def globalup(self): return app_globals.message def global_store(self, id=None): if id: app_globals.counter += int(id) return str(app_globals.counter) def myself(self): return request.url def myparams(self): return str(request.params) def testdefault(self): c.test = "This is in c var" return render_genshi('testgenshi.html') def test_template_caching(self): return render_mako('/test_mako.html', cache_expire='never') @rest.dispatch_on(GET='test_only_get') @rest.restrict('POST') def test_only_post(self): return 'It was a post!' @rest.restrict('GET') def test_only_get(self): return 'It was a get!' @rest.restrict('POST') @rest.dispatch_on(POST='test_only_post') def impossible(self): return 'This should never be shown' def testjinja2(self): c.test = "This is in c var" c.now = datetime.datetime.now return render_jinja2('testjinja2.html') def set_lang(self): return self._set_lang(_) def set_lang_pylonscontext(self, pylons): return self._set_lang(lambda *args: pylons.translator.ugettext(*args)) def _set_lang(self, gettext): lang = request.GET['lang'] try: set_lang(lang) except (LanguageError, IOError), e: resp_unicode = gettext('Could not set language to "%(lang)s"') % {'lang': lang} else: session['lang'] = lang session.save() resp_unicode = gettext('Set language to "%(lang)s"') % {'lang': lang} return resp_unicode def i18n_index(self): locale_list = request.languages set_lang(request.languages) return unicode(_('basic index page')) def no_lang(self): set_lang(None) response.write(_('No language')) set_lang([]) response.write(_('No languages')) return '' ================================================ FILE: tests/test_webapps/filestotest/controller_sqlatest.py ================================================ import datetime from projectname.lib.base import * try: import sqlalchemy as sa from projectname.model.meta import Session, Base from projectname.model import Foo SQLAtesting = True except: SQLAtesting = False import projectname.lib.helpers as h from pylons import request, response, session from pylons import tmpl_context as c from pylons import app_globals from pylons.decorators import rest from pylons.i18n import _, get_lang, set_lang, LanguageError from pylons.templating import render_mako, render_genshi, render_jinja2 from pylons.controllers.util import abort, redirect class SampleController(BaseController): def index(self): return 'basic index page' def testsqlalchemy(self): if SQLAtesting: c.foos = Session.query(Foo).all() return render_mako('test_sqlalchemy.html') pass def set_lang(self): return self._set_lang(_) def set_lang_pylonscontext(self, pylons): return self._set_lang(lambda *args: pylons.translator.ugettext(*args)) def _set_lang(self, gettext): lang = request.GET['lang'] try: set_lang(lang) except (LanguageError, IOError), e: resp_unicode = gettext('Could not set language to "%(lang)s"') % {'lang': lang} else: session['lang'] = lang session.save() resp_unicode = gettext('Set language to "%(lang)s"') % {'lang': lang} return resp_unicode def i18n_index(self): locale_list = request.languages set_lang(request.languages) return unicode(_('basic index page')) def no_lang(self): set_lang(None) response.write(_('No language')) set_lang([]) response.write(_('No languages')) return '' ================================================ FILE: tests/test_webapps/filestotest/controller_xmlrpc.py ================================================ from projectname.lib.base import * from pylons.controllers import XMLRPCController class XmlrpcController(XMLRPCController): def userstatus(self): return 'basic string' userstatus.signature = [ ['string'] ] def docs(self): "This method has a docstring" return dict(mess='a little somethin', a=1, b=[1,2,3], c=('all','the')) docs.signature = [ ['struct'] ] def uni(self): "This method has a docstring" return dict(mess=u'A unicode string, oh boy') docs.signature = [ ['struct'] ] ================================================ FILE: tests/test_webapps/filestotest/development.ini ================================================ [DEFAULT] debug = true email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@exceptions.com [server:main] use = egg:Paste#http host = 127.0.0.1 port = 5000 [app:main] use = egg:projectname cache_dir = %(here)s/data beaker.session.key = projectname beaker.session.secret = somesecret # If you'd like to fine-tune the individual locations of the cache data dirs # for Myghty, the Cache data, or the Session saves, un-comment the desired # settings here: #beaker.cache_data_dir = %(here)s/data/cache #beaker.session_data_dir = %(here)s/data/sessions # If you are using SQLAlchemy you will need to specify a # dburi. You can do this with a line similar to the # one below but adjusted for your database connection # according to the SQLAlchemy documentation. The %(here)s # part is replaced with the current directory which is # useful when using sqlite on UNIX based platforms. # For Windows you should look at the SQLAlchemy # documentation for a special syntax to use because the # path returned by %(here)s contains a : character. # SQLAlchemy database URL #sqlalchemy.url = sqlite:///%(here)s/development.db # Do not set debug to true or uncomment the line below # on a production environment otherwise in the event of # an error occurring the visitor will be presented with # interactive debugging tools and these could be used to # execute malicious code. # For development purposes debug should be set to true # to enable the debugging code but be sure to set it back # to false before releasing your application. #set debug = false ================================================ FILE: tests/test_webapps/filestotest/development_sqlatesting.ini ================================================ [DEFAULT] debug = true email_to = you@yourdomain.com smtp_server = localhost error_email_from = paste@exceptions.com [server:main] use = egg:Paste#http host = 127.0.0.1 port = 5000 [app:main] use = egg:projectname cache_dir = %(here)s/data beaker.session.key = projectname beaker.session.secret = somesecret # If you'd like to fine-tune the individual locations of the cache data dirs # for Myghty, the Cache data, or the Session saves, un-comment the desired # settings here: #beaker.cache_data_dir = %(here)s/data/cache #beaker.session_data_dir = %(here)s/data/sessions # If you are using SQLAlchemy you will need to specify a # dburi. You can do this with a line similar to the # one below but adjusted for your database connection # according to the SQLAlchemy documentation. The %(here)s # part is replaced with the current directory which is # useful when using sqlite on UNIX based platforms. # For Windows you should look at the SQLAlchemy # documentation for a special syntax to use because the # path returned by %(here)s contains a : character. # SQLAlchemy database URL sqlalchemy.url = sqlite:///%(here)s/development.db sqlalchemy.echo = True # Do not set debug to true or uncomment the line below # on a production environment otherwise in the event of # an error occurring the visitor will be presented with # interactive debugging tools and these could be used to # execute malicious code. # For development purposes debug should be set to true # to enable the debugging code but be sure to set it back # to false before releasing your application. #set debug = false ================================================ FILE: tests/test_webapps/filestotest/environment_def_engine.py ================================================ """Pylons environment configuration""" import os from mako.lookup import TemplateLookup from genshi.template import TemplateLoader from jinja2 import ChoiceLoader, Environment, FileSystemLoader from pylons.configuration import PylonsConfig from pylons.error import handle_mako_error import projectname.lib.app_globals as app_globals import projectname.lib.helpers from projectname.config.routing import make_map def load_environment(global_conf, app_conf): """Configure the Pylons environment via the ``pylons.config`` object """ config = PylonsConfig() # Pylons paths root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) paths = dict(root=root, controllers=os.path.join(root, 'controllers'), static_files=os.path.join(root, 'public'), templates=[os.path.join(root, 'templates')]) # Initialize config with the basic options config.init_app(global_conf, app_conf, package='projectname', paths=paths) config['routes.map'] = make_map(config) config['pylons.app_globals'] = app_globals.Globals(config) config['pylons.h'] = projectname.lib.helpers # Setup cache object as early as possible import pylons pylons.cache._push_object(config['pylons.app_globals'].cache) # Create the Mako TemplateLookup, with the default auto-escaping config['pylons.app_globals'].mako_lookup = TemplateLookup( directories=paths['templates'], error_handler=handle_mako_error, module_directory=os.path.join(app_conf['cache_dir'], 'templates'), input_encoding='utf-8', default_filters=['escape'], imports=['from webhelpers.html import escape']) # Create the Genshi TemplateLoader config['pylons.app_globals'].genshi_loader = TemplateLoader( paths['templates'], auto_reload=True) # Create the Jinja2 Environment config['pylons.app_globals'].jinja2_env = Environment(loader=ChoiceLoader( [FileSystemLoader(path) for path in paths['templates']])) # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) return config ================================================ FILE: tests/test_webapps/filestotest/environment_def_sqlamodel.py ================================================ """Pylons environment configuration""" import os from mako.lookup import TemplateLookup from genshi.template import TemplateLoader from jinja2 import ChoiceLoader, Environment, FileSystemLoader from pylons.configuration import PylonsConfig from pylons.error import handle_mako_error from sqlalchemy import engine_from_config import projectname.lib.app_globals as app_globals import projectname.lib.helpers from projectname.config.routing import make_map from projectname.model import init_model def load_environment(global_conf, app_conf): """Configure the Pylons environment via the ``pylons.config`` object """ config = PylonsConfig() # Pylons paths root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) paths = dict(root=root, controllers=os.path.join(root, 'controllers'), static_files=os.path.join(root, 'public'), templates=[os.path.join(root, 'templates')]) # Initialize config with the basic options config.init_app(global_conf, app_conf, package='projectname', paths=paths) config['routes.map'] = make_map(config) config['pylons.app_globals'] = app_globals.Globals(config) config['pylons.h'] = projectname.lib.helpers # Setup cache object as early as possible import pylons pylons.cache._push_object(config['pylons.app_globals'].cache) # Create the Mako TemplateLookup, with the default auto-escaping config['pylons.app_globals'].mako_lookup = TemplateLookup( directories=paths['templates'], error_handler=handle_mako_error, module_directory=os.path.join(app_conf['cache_dir'], 'templates'), input_encoding='utf-8', default_filters=['escape'], imports=['from webhelpers.html import escape']) # Create the Genshi TemplateLoader config['pylons.app_globals'].genshi_loader = TemplateLoader( paths['templates'], auto_reload=True) # Create the Jinja2 Environment config['pylons.app_globals'].jinja2_env = Environment(loader=ChoiceLoader( [FileSystemLoader(path) for path in paths['templates']])) # CONFIGURATION OPTIONS HERE (note: all config options will override # any Pylons config options) engine = engine_from_config(config, 'sqlalchemy.') init_model(engine) return config ================================================ FILE: tests/test_webapps/filestotest/functional_controller_cache_decorator.py ================================================ import time from projectname.tests import * class TestCacheController(TestController): def test_default_cache_decorator(self): response = self.app.get(url(controller='cache', action='test_default_cache_decorator')) assert 'Counter=1' in response response = self.app.get(url(controller='cache', action='test_default_cache_decorator')) assert 'Counter=1' in response response = self.app.get(url(controller='cache', action='test_get_cache_decorator', param="123")) assert 'Counter=2' in response response = self.app.get(url(controller='cache', action='test_get_cache_decorator', param="123")) assert 'Counter=2' in response response = self.app.get(url(controller='cache', action='test_expire_cache_decorator')) assert 'Counter=3' in response response = self.app.get(url(controller='cache', action='test_expire_cache_decorator')) assert 'Counter=3' in response time.sleep(8) response = self.app.get(url(controller='cache', action='test_expire_cache_decorator')) assert 'Counter=4' in response response = self.app.get(url(controller='cache', action='test_key_cache_decorator', id=1)) assert 'Counter=5' in response response = self.app.get(url(controller='cache', action='test_key_cache_decorator', id=2)) assert 'Counter=6' in response response = self.app.get(url(controller='cache', action='test_key_cache_decorator', id=1)) assert 'Counter=5' in response response = self.app.get(url(controller='cache', action='test_keyslist_cache_decorator', id=1, id2=2)) assert 'Counter=7' in response response = self.app.get(url(controller='cache', action='test_keyslist_cache_decorator', id=1, id2=2)) assert 'Counter=7' in response ================================================ FILE: tests/test_webapps/filestotest/functional_controller_xmlrpc.py ================================================ from projectname.tests import * from xmlrpclib import loads, dumps class TestXmlrpcController(TestController): xmlurl = None def xmlreq(self, method, args=None): if args is None: args = () ee = dict(CONTENT_TYPE='text/xml') data = dumps(args, methodname=method) response = self.app.post(self.xmlurl, params = data, extra_environ=ee) return loads(response.body)[0][0] def setUp(self): self.xmlurl = url(controller='xmlrpc', action='index') def test_index(self): response = self.xmlreq('userstatus') assert response == 'basic string' def test_structure(self): response = self.xmlreq('docs') assert dict(mess='a little somethin', a=1, b=[1,2,3], c=['all','the']) == response def test_methodhelp(self): response = self.xmlreq('system.methodHelp', ('docs',)) assert "This method has a docstring" in response def test_methodsignature(self): response = self.xmlreq('system.methodSignature', ('docs',)) assert [['struct']] == response def test_listmethods(self): response = self.xmlreq('system.listMethods') assert response == ['docs', 'system.listMethods', 'system.methodHelp', 'system.methodSignature', 'uni', 'userstatus'] def test_unicode(self): response = self.xmlreq('uni') assert 'A unicode string' in response['mess'] ================================================ FILE: tests/test_webapps/filestotest/functional_sample_controller_i18n.py ================================================ from projectname.tests import * class TestSampleController(TestController): def test_set_lang(self): self._test_set_lang('set_lang') def test_set_lang_pylonscontext(self): self._test_set_lang('set_lang_pylonscontext') def _test_set_lang(self, action): response = self.app.get(url(controller='sample', action=action, lang='ja')) assert u'\u8a00\u8a9e\u8a2d\u5b9a\u3092\u300cja\u300d\u306b\u5909\u66f4\u3057\u307e\u3057\u305f'.encode('utf-8') in response response = self.app.get(url(controller='sample', action=action, lang='fr')) assert 'Could not set language to "fr"' in response def test_detect_lang(self): response = self.app.get(url(controller='sample', action='i18n_index'), headers={ 'Accept-Language':'fr;q=0.6, en;q=0.1, ja;q=0.3'}) # expect japanese fallback for nonexistent french. assert u'\u6839\u672c\u30a4\u30f3\u30c7\u30af\u30b9\u30da\u30fc\u30b8'.encode('utf-8') in response def test_no_lang(self): response = self.app.get(url(controller='sample', action='no_lang')) assert 'No language' in response assert 'No languages' in response ================================================ FILE: tests/test_webapps/filestotest/functional_sample_controller_jinja2.py ================================================ from projectname.tests import * class TestJinja2Controller(TestController): def test_jinja2(self): response = self.app.get(url(controller='sample', action='testjinja2')) assert 'Hello from Jinja2' in response assert 'This is in c var' in response ================================================ FILE: tests/test_webapps/filestotest/functional_sample_controller_mako.py ================================================ from projectname.tests import * class TestMakoController(TestController): def test_mako(self): response = self.app.get(url(controller='sample', action='testmako')) assert 'Hello, 5+5 is 10' in response ================================================ FILE: tests/test_webapps/filestotest/functional_sample_controller_sample1.py ================================================ import pylons from projectname.tests import * class TestSampleController(TestController): def test_conf_with_app_globals(self): assert 'pylons.app_globals' in pylons.config assert hasattr(pylons.app_globals, 'cache') def test_root_index(self): response = self.app.get('/') assert 'Welcome' in response # Test response... def test_index(self): response = self.app.get(url(controller='sample', action='index')) assert 'basic index page' in response def test_session(self): response = self.app.get(url(controller='sample', action='session_increment')) assert response.session.has_key('counter') assert response.session['counter'] == 0 response = self.app.get(url(controller='sample', action='session_increment')) assert response.session['counter'] == 1 assert 'session incrementer' in response def test_global(self): response = self.app.get(url(controller='sample', action='globalup')) assert 'Hello' in response def test_global_persistence(self): response = self.app.get(url(controller='sample', action='global_store')) assert '0' in response response = self.app.get(url(controller='sample', action='global_store', id=2)) assert '2' in response response = self.app.get(url(controller='sample', action='global_store')) assert '2' in response response = self.app.get(url(controller='sample', action='global_store', id=3)) assert '5' in response response = self.app.get(url(controller='sample', action='global_store')) assert '5' in response def test_helper_urlfor(self): response = self.app.get(url(controller='sample', action='myself')) assert 'sample/myself' in response def test_params(self): response = self.app.get(url(controller='sample', action='myparams', extra='something', data=4)) assert 'extra' in response assert 'something' in response assert 'data' in response ================================================ FILE: tests/test_webapps/filestotest/functional_sample_controller_sample2.py ================================================ from projectname.tests import * class TestSample2Controller(TestController): def test_session(self): response = self.app.get(url(controller='sample', action='session_increment')) assert response.session.has_key('counter') assert response.session['counter'] == 0 response = self.app.get(url(controller='sample', action='session_increment')) assert response.session['counter'] == 1 assert 'session incrementer' in response def test_genshi_default(self): self._test_genshi_default('testdefault') def _test_genshi_default(self, action): response = self.app.get(url(controller='sample', action=action)) assert 'Hello from Genshi' in response assert 'This is in c var' in response ================================================ FILE: tests/test_webapps/filestotest/functional_sample_controller_sample3.py ================================================ from projectname.tests import * class TestSample2Controller(TestController): def test_session(self): response = self.app.get(url(controller='sample', action='session_increment')) assert response.session.has_key('counter') assert response.session['counter'] == 0 response = self.app.get(url(controller='sample', action='session_increment')) assert response.session['counter'] == 1 assert 'session incrementer' in response def test_default(self): response = self.app.get(url(controller='sample', action='test_template_caching')) assert 'Hi everyone!' in response ================================================ FILE: tests/test_webapps/filestotest/functional_sample_controller_sample4.py ================================================ from projectname.tests import * class TestSample2Controller(TestController): def test_get(self): response = self.app.get(url(controller='sample', action='test_only_get')) assert 'It was a get' in response def test_redir_get(self): response = self.app.get(url(controller='sample', action='test_only_post')) assert 'It was a get' in response def test_post(self): response = self.app.post(url(controller='sample', action='test_only_post'), params={'id':4}) assert 'It was a post' in response def test_head(self): response = self.app._gen_request('HEAD', url(controller='sample', action='index')) assert '' == response.body ================================================ FILE: tests/test_webapps/filestotest/functional_sample_controller_sqlatesting.py ================================================ from projectname.tests import * try: from sqlalchemy.exceptions import IntegrityError except ImportError: from sqlalchemy.exc import IntegrityError from projectname.model.meta import Session, Base from projectname.model import Foo class TestSQLAlchemyController(TestController): def setUp(self): Base.metadata.create_all(bind=Session.bind) f = Foo(id = 1, bar = u"Wabbit") Session.add(f) Session.commit() assert f.bar == u"Wabbit" def tearDown(self): Base.metadata.drop_all(bind=Session.bind) def test_sqlalchemy(self): response = self.app.get(url(controller='sample', action='testsqlalchemy')) assert 'foos = [Foo:1]' in response # def test_exception(self): # me = Foo(id=3, bar='giuseppe') # me_again = Foo(id=3, bar='giuseppe') # self.assertRaises(IntegrityError, Session.commit) # Session.rollback() ================================================ FILE: tests/test_webapps/filestotest/helpers_sample.py ================================================ """Helper functions Consists of functions to typically be used within templates, but also available to Controllers. This module is available to both as 'h'. """ ================================================ FILE: tests/test_webapps/filestotest/messages.ja.po ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-02-05 12:36+0900\n" "PO-Revision-Date: 2007-02-05 13:06+0900\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" #: controller_sample.py:7 msgid "basic index page" msgstr "根本インデクスページ" #: controller_sample.py:59 #, python-format msgid "Could not set language to \"%(lang)s\"" msgstr "「%(lang)s」に言語設定が変更できません" #: controller_sample.py:63 #, python-format msgid "Set language to \"%(lang)s\"" msgstr "言語設定を「%(lang)s」に変更しました" ================================================ FILE: tests/test_webapps/filestotest/messages.pot ================================================ # SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2007-02-05 12:36+0900\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=CHARSET\n" "Content-Transfer-Encoding: 8bit\n" #: controller_sample.py:7 msgid "basic index page" msgstr "" #: controller_sample.py:59 #, python-format msgid "Could not set language to \"%(lang)s\"" msgstr "" #: controller_sample.py:63 #, python-format msgid "Set language to \"%(lang)s\"" msgstr "" ================================================ FILE: tests/test_webapps/filestotest/middleware_mako.py ================================================ """Pylons middleware initialization""" from beaker.middleware import SessionMiddleware from paste.cascade import Cascade from paste.registry import RegistryManager from paste.urlparser import StaticURLParser from paste.deploy.converters import asbool from pylons.middleware import ErrorHandler, StatusCodeRedirect from pylons.wsgiapp import PylonsApp from routes.middleware import RoutesMiddleware from projectname.config.environment import load_environment def make_app(global_conf, full_stack=True, static_files=True, **app_conf): """Create a Pylons WSGI application and return it ``global_conf`` The inherited configuration for this application. Normally from the [DEFAULT] section of the Paste ini file. ``full_stack`` Whether this application provides a full WSGI stack (by default, meaning it handles its own exceptions and errors). Disable full_stack when this application is "managed" by another WSGI middleware. ``static_files`` Whether this application serves its own static files; disable when another web server is responsible for serving them. ``app_conf`` The application's local configuration. Normally specified in the [app:] section of the Paste ini file (where defaults to main). """ # Configure the Pylons environment config = load_environment(global_conf, app_conf) # The Pylons WSGI app app = PylonsApp(config=config) # Routing/Session/Cache Middleware app = RoutesMiddleware(app, config['routes.map'], singleton=False) app = SessionMiddleware(app, config) # CUSTOM MIDDLEWARE HERE (filtered by error handling middlewares) if asbool(full_stack): # Handle Python exceptions app = ErrorHandler(app, global_conf, **config['pylons.errorware']) # Display error documents for 401, 403, 404 status codes (and # 500 when debug is disabled) if asbool(config['debug']): app = StatusCodeRedirect(app) else: app = StatusCodeRedirect(app, [400, 401, 403, 404, 500]) # Establish the Registry for this application app = RegistryManager(app) if asbool(static_files): # Serve static files static_app = StaticURLParser(config['pylons.paths']['static_files']) app = Cascade([static_app, app]) app.config = config return app ================================================ FILE: tests/test_webapps/filestotest/model__init__.py ================================================ """The application's model objects""" from sqlalchemy import types, Column from projectname.model.meta import Base, Session def init_model(engine): """Call me before using any of the tables or classes in the model""" Session.configure(bind=engine) class Foo(Base): __tablename__ = 'foo' id = Column(types.Integer, primary_key=True) bar = Column(types.String(255), nullable=False) def __repr__(self): return "Foo:%s" % self.id ================================================ FILE: tests/test_webapps/filestotest/rest_routing.py ================================================ """Routes configuration The more specific and detailed routes should be defined first so they may take precedent over the more generic routes. For more information refer to the routes manual at http://routes.groovie.org/docs/ """ from routes import Mapper def make_map(config): """Create, configure and return the routes Mapper""" map = Mapper(directory=config['pylons.paths']['controllers'], always_scan=config['debug']) map.minimization = False map.explicit = False # The ErrorController route (handles 404/500 error pages); it should # likely stay at the top, ensuring it can always be resolved map.connect('/error/{action}', controller='error') map.connect('/error/{action}/{id}', controller='error') # CUSTOM ROUTES HERE map.resource('restsample', 'restsamples') map.resource('restsample', 'restsamples', controller='mysubdir/restsamples', path_prefix='/mysubdir', name_prefix='mysubdir_') map.connect('/{controller}/{action}') map.connect('/{controller}/{action}/{id}') return map ================================================ FILE: tests/test_webapps/filestotest/test_mako.html ================================================ <% from datetime import datetime mytime = datetime.now() %> Hi everyone! The time is ${mytime} ================================================ FILE: tests/test_webapps/filestotest/test_sqlalchemy.html ================================================ foos = ${c.foos} ================================================ FILE: tests/test_webapps/filestotest/testgenshi.html ================================================ Hello from Genshi

Hello from Genshi

You visited the URL ${request.url} at ${datetime.datetime.now()}

${c.test}

================================================ FILE: tests/test_webapps/filestotest/testjinja2.html ================================================ #import datetime Hello from Jinja2

Hello from Jinja2

You visited the URL {{ request.url }} at {{ c.now() }}

{{ c.test }}

================================================ FILE: tests/test_webapps/filestotest/tests__init__.py ================================================ """Pylons application test package This package assumes the Pylons environment is already loaded, such as when this script is imported from the `nosetests --with-pylons=test.ini` command. This module initializes the application via ``websetup`` (`paster setup-app`) and provides the base testing objects. """ from unittest import TestCase from paste.deploy import loadapp from paste.script.appinstall import SetupCommand from pylons import url from routes.util import URLGenerator from webtest import TestApp import pylons.test __all__ = ['environ', 'url', 'TestController'] try: from sqlalchemy import engine_from_config from projectname.model.meta import Session, Base from projectname.model import Foo, init_model SQLAtesting = True except: SQLAtesting = False import os import pylons from pylons.i18n.translation import _get_translator from paste.deploy import appconfig from projectname.config.environment import load_environment here_dir = os.path.dirname(__file__) conf_dir = os.path.dirname(os.path.dirname(here_dir)) test_file = os.path.join(conf_dir, 'test.ini') conf = appconfig('config:' + test_file) config = load_environment(conf.global_conf, conf.local_conf) if SQLAtesting: engine = engine_from_config(config, 'sqlalchemy.') init_model(engine) # Invoke websetup with the current config file SetupCommand('setup-app').run([test_file]) environ = {} class TestController(TestCase): def __init__(self, *args, **kwargs): wsgiapp = loadapp('config:test.ini', relative_to=conf_dir) config = wsgiapp.config pylons.app_globals._push_object(config['pylons.app_globals']) pylons.config._push_object(config) # Initialize a translator for tests that utilize i18n translator = _get_translator(pylons.config.get('lang')) pylons.translator._push_object(translator) url._push_object(URLGenerator(config['routes.map'], environ)) self.app = TestApp(wsgiapp) TestCase.__init__(self, *args, **kwargs) ================================================ FILE: tests/test_webapps/filestotest/websetup.py ================================================ """Setup the projectname application""" import logging import pylons.test from projectname.config.environment import load_environment from projectname.model.meta import Session, Base from projectname.model import Foo from sqlalchemy import engine_from_config log = logging.getLogger(__name__) def setup_app(command, conf, vars): """Place any commands to setup projectname here""" config = load_environment(conf.global_conf, conf.local_conf) # Create the tables if they don't already exist Base.metadata.create_all(bind=Session.bind) ================================================ FILE: tests/test_webapps/test_make_project.py ================================================ """Tests against full Pylons projects created from scratch""" import os import sys import shutil import re import pkg_resources from nose import SkipTest from paste.fixture import TestFileEnvironment if os.environ.get('SKIP_INTEGRATED', 'False') != '0': raise SkipTest() import pylons import pylons.test try: import sqlalchemy as sa SQLAtesting = True except ImportError: SQLAtesting = False # SQLAtesting = False is_jython = sys.platform.startswith('java') TEST_OUTPUT_DIRNAME = 'output' for spec in ['PasteScript', 'Paste', 'PasteDeploy', 'pylons']: pkg_resources.require(spec) template_path = os.path.join( os.path.dirname(__file__), 'filestotest').replace('\\','/') test_environ = os.environ.copy() test_environ['PASTE_TESTING'] = 'true' testenv = TestFileEnvironment( os.path.join(os.path.dirname(__file__), TEST_OUTPUT_DIRNAME).replace('\\','/'), template_path=template_path, environ=test_environ) projenv = None def _get_script_name(script): if sys.platform == 'win32' and not script.lower().endswith('.exe'): script += '.exe' return script def svn_repos_setup(): res = testenv.run(_get_script_name('svnadmin'), 'create', 'REPOS', printresult=False) path = testenv.base_path.replace('\\','/').replace(' ','%20') base = 'file://' if ':' in path: base = 'file:///' testenv.svn_url = base + path + '/REPOS' assert 'REPOS' in res.files_created testenv.ignore_paths.append('REPOS') def paster_create(template_engine='mako', overwrite=False, sqlatesting=False): global projenv paster_args = ['create', '--verbose', '--no-interactive'] if overwrite: paster_args.append('-f') paster_args.extend(['--template=pylons', 'ProjectName', 'version=0.1', 'sqlalchemy=%s' % sqlatesting, 'zip_safe=False', 'template_engine=%s' % template_engine]) res = testenv.run(_get_script_name('paster'), *paster_args) expect_fn = ['projectname', 'development.ini', 'setup.cfg', 'README.txt', 'setup.py'] for fn in expect_fn: fn = os.path.join('ProjectName', fn) if not overwrite: assert fn in res.files_created.keys() assert fn in res.stdout if not overwrite: setup = res.files_created[os.path.join('ProjectName','setup.py')] setup.mustcontain('0.1') setup.mustcontain('projectname.config.middleware:make_app') setup.mustcontain('main = pylons.util:PylonsInstaller') setup.mustcontain("include_package_data=True") assert '0.1' in setup testenv.run(_get_script_name(sys.executable)+' setup.py egg_info', cwd=os.path.join(testenv.cwd, 'ProjectName').replace('\\','/'), expect_stderr=True) #testenv.run(_get_script_name('svn'), 'commit', '-m', 'Created project', 'ProjectName') # A new environment with a new projenv = TestFileEnvironment( os.path.join(testenv.base_path, 'ProjectName').replace('\\','/'), start_clear=False, template_path=template_path, environ=test_environ) projenv.environ['PYTHONPATH'] = ( projenv.environ.get('PYTHONPATH', '') + ':' + projenv.base_path) def make_controller(): res = projenv.run(_get_script_name('paster')+' controller sample') assert os.path.join('projectname','controllers','sample.py') in res.files_created assert os.path.join('projectname','tests','functional','test_sample.py') in res.files_created #res = projenv.run(_get_script_name('svn')+' status') # Make sure all files are added to the repository: assert '?' not in res.stdout def make_controller_subdirectory(): res = projenv.run(_get_script_name('paster')+' controller mysubdir/sample') assert os.path.join('projectname','controllers', 'mysubdir', 'sample.py') in res.files_created assert os.path.join('projectname','tests','functional','test_mysubdir_sample.py') in res.files_created #res = projenv.run(_get_script_name('svn')+' status') # Make sure all files are added to the repository: assert '?' not in res.stdout def make_restcontroller(): res = projenv.run(_get_script_name('paster')+' restcontroller restsample restsamples') assert os.path.join('projectname','controllers','restsamples.py') in res.files_created assert os.path.join('projectname','tests','functional','test_restsamples.py') in res.files_created #res = projenv.run(_get_script_name('svn')+' status') # Make sure all files are added to the repository: assert '?' not in res.stdout def make_restcontroller_subdirectory(): res = projenv.run(_get_script_name('paster')+' restcontroller mysubdir/restsample mysubdir/restsamples') assert os.path.join('projectname','controllers','mysubdir', 'restsamples.py') in res.files_created assert os.path.join('projectname','tests','functional','test_mysubdir_restsamples.py') in res.files_created #res = projenv.run(_get_script_name('svn')+' status') # Make sure all files are added to the repository: assert '?' not in res.stdout def _do_proj_test(copydict, emptyfiles=None, match_routes_output=None): """Given a dict of files, where the key is a filename in filestotest, the value is the destination in the new projects dir. emptyfiles is a list of files that should be created and empty.""" if pylons.test.pylonsapp: pylons.test.pylonsapp = None if not emptyfiles: emptyfiles = [] for original, newfile in copydict.iteritems(): projenv.writefile(newfile, frompath=original) for fi in emptyfiles: projenv.writefile(fi) # here_dir = os.getcwd() # test_dir = os.path.join(testenv.cwd, 'ProjectName').replace('\\','/') # os.chdir(test_dir) # sys.path.append(test_dir) # nose.run(argv=['nosetests', '-d', test_dir]) # # sys.path.pop(-1) # os.chdir(here_dir) res = projenv.run(_get_script_name('nosetests')+' -d', expect_stderr=True, cwd=os.path.join(testenv.cwd, 'ProjectName').replace('\\','/')) if match_routes_output: res = projenv.run(_get_script_name('paster')+' routes', expect_stderr=False, cwd=os.path.join(testenv.cwd, 'ProjectName').replace('\\','/')) for pattern in match_routes_output: assert re.compile(pattern).search(res.stdout) def do_nosetests(): _do_proj_test({'development.ini':'development.ini'}) def do_knowntest(): copydict = { 'helpers_sample.py':'projectname/lib/helpers.py', 'controller_sample.py':'projectname/controllers/sample.py', 'app_globals.py':'projectname/lib/app_globals.py', 'functional_sample_controller_sample1.py':'projectname/tests/functional/test_sample.py', } _do_proj_test(copydict) def do_i18ntest(): copydict = { 'functional_sample_controller_i18n.py':'projectname/tests/functional/test_i18n.py', 'messages.ja.po':'projectname/i18n/ja/LC_MESSAGES/projectname.po', 'messages.ja.mo':'projectname/i18n/ja/LC_MESSAGES/projectname.mo', } _do_proj_test(copydict) def do_genshi(): paster_create(template_engine='genshi', overwrite=True) reset = { 'helpers_sample.py':'projectname/lib/helpers.py', 'app_globals.py':'projectname/lib/app_globals.py', 'rest_routing.py':'projectname/config/routing.py', 'development.ini':'development.ini', } copydict = { 'testgenshi.html':'projectname/templates/testgenshi.html', 'environment_def_engine.py':'projectname/config/environment.py', 'functional_sample_controller_sample2.py':'projectname/tests/functional/test_sample2.py' } copydict.update(reset) empty = ['projectname/templates/__init__.py', 'projectname/tests/functional/test_cache.py'] _do_proj_test(copydict, empty) def do_two_engines(): copydict = { 'middleware_two_engines.py':'projectname/config/middleware.py', 'test_mako.html':'projectname/templates/test_mako.html', 'functional_sample_controller_sample3.py':'projectname/tests/functional/test_sample2.py', } _do_proj_test(copydict) def do_crazy_decorators(): _do_proj_test({'functional_sample_controller_sample4.py':'projectname/tests/functional/test_sample3.py'}) def do_jinja2(): paster_create(template_engine='jinja2', overwrite=True) reset = { 'helpers_sample.py':'projectname/lib/helpers.py', 'app_globals.py':'projectname/lib/app_globals.py', 'rest_routing.py':'projectname/config/routing.py', 'development.ini':'development.ini', } copydict = { 'controller_sample.py':'projectname/controllers/sample.py', 'testjinja2.html':'projectname/templates/testjinja2.html', 'environment_def_engine.py':'projectname/config/environment.py', 'functional_sample_controller_jinja2.py':'projectname/tests/functional/test_jinja2.py', } copydict.update(reset) empty = [ 'projectname/templates/__init__.py', 'projectname/tests/functional/test_sample.py', 'projectname/tests/functional/test_sample2.py', 'projectname/tests/functional/test_sample3.py', 'projectname/tests/functional/test_cache.py' ] _do_proj_test(copydict, empty) def do_cache_decorator(): copydict = { 'middleware_mako.py':'projectname/config/middleware.py', 'app_globals.py':'projectname/lib/app_globals.py', 'cache_controller.py':'projectname/controllers/cache.py', 'functional_controller_cache_decorator.py':'projectname/tests/functional/test_cache.py', } empty = [ 'projectname/tests/functional/test_mako.py', 'projectname/tests/functional/test_jinja2.py', 'projectname/tests/functional/test_sample.py', 'projectname/tests/functional/test_sample2.py', 'projectname/tests/functional/test_sample3.py' ] _do_proj_test(copydict, empty) def do_xmlrpc(): copydict = { 'middleware_mako.py':'projectname/config/middleware.py', 'base_with_xmlrpc.py':'projectname/lib/base.py', 'controller_xmlrpc.py':'projectname/controllers/xmlrpc.py', 'functional_controller_xmlrpc.py':'projectname/tests/functional/test_xmlrpc.py' } empty = [ 'projectname/tests/functional/test_cache.py', 'projectname/tests/functional/test_jinja2.py', ] _do_proj_test(copydict, empty) def make_tag(): global tagenv #res = projenv.run(_get_script_name('svn')+' commit -m "updates"') # Space at the end needed so run() doesn't add \n causing svntag to complain #res = projenv.run(_get_script_name(sys.executable)+' setup.py svntag --version=0.5 ') # XXX Still fails => setuptools problem on win32? assert 'Tagging 0.5 version' in res.stdout assert 'Auto-update of version strings' in res.stdout res = testenv.run(_get_script_name('svn')+' co %s/ProjectName/tags/0.5 Proj-05 ' % testenv.svn_url) setup = res.files_created['Proj-05/setup.py'] setup.mustcontain('0.5') assert 'Proj-05/setup.cfg' not in res.files_created tagenv = TestFileEnvironment( os.path.join(testenv.base_path, 'Proj-05').replace('\\','/'), start_clear=False, template_path=template_path) def do_sqlaproject(): paster_create(template_engine='mako', overwrite=True, sqlatesting=True) reset = { 'helpers_sample.py':'projectname/lib/helpers.py', 'app_globals.py':'projectname/lib/app_globals.py', 'rest_routing.py':'projectname/config/routing.py', 'development_sqlatesting.ini':'development.ini', 'websetup.py':'projectname/websetup.py', 'model__init__.py':'projectname/model/__init__.py', 'environment_def_sqlamodel.py':'projectname/config/environment.py', 'tests__init__.py':'projectname/tests/__init__.py', } copydict = { 'controller_sqlatest.py':'projectname/controllers/sample.py', 'test_mako.html':'projectname/templates/test_mako.html', 'test_sqlalchemy.html':'projectname/templates/test_sqlalchemy.html', 'functional_sample_controller_sqlatesting.py':'projectname/tests/functional/test_sqlalchemyproject.py', } copydict.update(reset) empty = [ 'projectname/templates/__init__.py', 'projectname/tests/functional/test_sample.py', 'projectname/tests/functional/test_sample2.py', 'projectname/tests/functional/test_sample3.py', 'projectname/tests/functional/test_cache.py' ] _do_proj_test(copydict, empty) # res = projenv.run(_get_script_name('paster')+' setup-app development.ini', expect_stderr=True,) # assert '?' not in res.stdout # Unfortunately, these are ordered, so be careful def test_project_paster_create(): paster_create() def test_project_make_controller(): make_controller() def test_project_make_controller_subdirectory(): make_controller_subdirectory() def test_project_do_nosetests(): do_nosetests() def test_project_do_knowntest(): do_knowntest() def test_project_do_i18ntest(): do_i18ntest() def test_project_make_restcontroller(): make_restcontroller() def test_project_make_restcontroller_subdirectory(): make_restcontroller_subdirectory() def test_project_do_rest_nosetests(): copydict = { 'rest_routing.py':'projectname/config/routing.py', 'development.ini':'development.ini', } match_routes_output = [ 'Route name +Methods +Path', 'restsamples +GET +/restsamples' ] _do_proj_test(copydict, match_routes_output) # Tests with templating plugin dependencies def test_project_do_crazy_decorators(): do_crazy_decorators() def test_project_do_cache_decorator(): do_cache_decorator() def test_project_do_genshi_default(): if is_jython: raise SkipTest('Jython does not currently support Genshi') do_genshi() def test_project_do_jinja2(): do_jinja2() def test_project_do_xmlrpc(): do_xmlrpc() #def test_project_make_tag(): # make_tag() def test_project_do_sqlaproject(): if SQLAtesting: do_sqlaproject() else: pass def teardown(): dir_to_clean = os.path.join(os.path.dirname(__file__), TEST_OUTPUT_DIRNAME) cov_dir = os.path.join(dir_to_clean, 'ProjectName') main_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) # Scan and move the coverage files # for name in os.listdir(cov_dir): # if name.startswith('.coverage.'): # shutil.move(os.path.join(cov_dir, name), main_dir) # shutil.rmtree(dir_to_clean)