Chula-0.7.0/0000755000175000017500000000000011412546122013536 5ustar jmcfarlanejmcfarlaneChula-0.7.0/sql/0000755000175000017500000000000011412546122014335 5ustar jmcfarlanejmcfarlaneChula-0.7.0/sql/test/0000755000175000017500000000000011412546122015314 5ustar jmcfarlanejmcfarlaneChula-0.7.0/sql/test/reload0000755000175000017500000000030111370740256016511 0ustar jmcfarlanejmcfarlane#!/bin/bash SRC=${0%/*} SQL=/tmp/chula-gen-db.sql cd $SRC cat /dev/null > $SQL files=`ls *.sql` for file in $files ; do cat $file >> $SQL done psql --quiet -U postgres template1 < $SQL Chula-0.7.0/sql/test/schema.sql0000644000175000017500000000067711370740256017316 0ustar jmcfarlanejmcfarlaneDROP DATABASE chula_test; CREATE DATABASE chula_test WITH OWNER = postgres TEMPLATE = template0 ENCODING = 'UTF-8'; CREATE USER chula; ALTER USER chula WITH PASSWORD 'chula'; \c chula_test -- Test table CREATE TABLE cars ( uid serial NOT NULL, make character varying, model character varying ); GRANT ALL ON cars TO chula; GRANT ALL ON cars_uid_seq TO chula; INSERT INTO cars (make, model) VALUES('Honda', 'Civic'); Chula-0.7.0/sql/session/0000755000175000017500000000000011412546122016020 5ustar jmcfarlanejmcfarlaneChula-0.7.0/sql/session/reload0000755000175000017500000000030111370740256017215 0ustar jmcfarlanejmcfarlane#!/bin/bash SRC=${0%/*} SQL=/tmp/chula-gen-db.sql cd $SRC cat /dev/null > $SQL files=`ls *.sql` for file in $files ; do cat $file >> $SQL done psql --quiet -U postgres template1 < $SQL Chula-0.7.0/sql/session/schema.sql0000644000175000017500000000374111370740256020015 0ustar jmcfarlanejmcfarlaneDROP DATABASE chula_session; CREATE DATABASE chula_session WITH OWNER = postgres TEMPLATE = template0 ENCODING = 'UTF-8'; CREATE USER chula; ALTER USER chula WITH PASSWORD 'chula'; \c chula_session -- Install LANGUAGE plpgsql; CREATE FUNCTION plpgsql_call_handler() RETURNS language_handler AS '$libdir/plpgsql' LANGUAGE C; CREATE FUNCTION plpgsql_validator(oid) RETURNS void AS '$libdir/plpgsql' LANGUAGE C; CREATE TRUSTED PROCEDURAL LANGUAGE plpgsql HANDLER plpgsql_call_handler VALIDATOR plpgsql_validator; -- Create the session table CREATE TABLE session ( id SERIAL PRIMARY KEY, guid VARCHAR(64) UNIQUE NOT NULL, created TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, updated TIMESTAMPTZ, active BOOLEAN NOT NULL DEFAULT TRUE, values TEXT ); GRANT ALL ON TABLE session TO chula; GRANT ALL ON TABLE session_id_seq TO chula; --Create an index on the guid CREATE INDEX session_guid_idx ON session (guid); -- Create the function used to persist session -- Usage: SELECT session_set('abcdguid', '[1,2,3]', FALSE) CREATE OR REPLACE FUNCTION session_set ( _guid VARCHAR(64), _values TEXT, _active BOOLEAN ) RETURNS INT4 AS $session_set$ DECLARE result BOOLEAN; rs RECORD; BEGIN IF LENGTH(_guid) != 64 THEN RAISE EXCEPTION 'The guid "%" was not a char(64)', _guid; RETURN -1; ELSE SELECT id, guid INTO rs FROM session WHERE guid = _guid; IF FOUND THEN --Update the existing session UPDATE session SET updated = CURRENT_TIMESTAMP, values = _values, active = _active WHERE guid = rs.guid; RETURN rs.id; ELSE --Create a new session INSERT INTO session(guid, values, active) VALUES(_guid, _values, TRUE); RETURN CURRVAL('session_id_seq'); END IF; END IF; END; $session_set$ LANGUAGE plpgsql; --EOF Chula-0.7.0/apps/0000755000175000017500000000000011412546122014501 5ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/0000755000175000017500000000000011412546122015562 5ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/webserver0000755000175000017500000000120111370740256017515 0ustar jmcfarlanejmcfarlane#! /usr/bin/env python import os import sys from wsgiref.simple_server import make_server # Expose Chula and the "example" python package, as it's not "installed" cwd = os.getcwd() sys.path.insert(0, cwd + '/../..') sys.path.insert(0, cwd) from chula.www.adapters.wsgi import adapter from example.configuration import dev as config @adapter.wsgi def application(): return config # Setup a simple server using the proxy app and it's configuration port = 8080 httpd = make_server('', port, application) try: print 'Starting server on: http://localhost:%s' % port httpd.serve_forever() except KeyboardInterrupt: sys.exit() Chula-0.7.0/apps/basic/example/0000755000175000017500000000000011412546122017215 5ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/example/www/0000755000175000017500000000000011412546122020041 5ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/example/www/controllers/0000755000175000017500000000000011412546122022407 5ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/example/www/controllers/sample.py0000644000175000017500000000027411370740256024254 0ustar jmcfarlanejmcfarlanefrom chula.www import controller class Sample(controller.Controller): def index(self): return 'Sample controller' def page(self): return 'Sample controller:page' Chula-0.7.0/apps/basic/example/www/controllers/error.py0000644000175000017500000000044211370740256024121 0ustar jmcfarlanejmcfarlanefrom chula.www import controller class Error(controller.Controller): def index(self): return 'Sorry, the site is down for maintenance' def e404(self): return 'Page not found' def e500(self): return 'Trapped Error: %s' % self.model.exception.exception Chula-0.7.0/apps/basic/example/www/controllers/home.py0000644000175000017500000000034311370740256023720 0ustar jmcfarlanejmcfarlanefrom chula.www import controller class Home(controller.Controller): def index(self): return 'Hello world' def foo(self): return 'This is the method "foo" of the home controller' Chula-0.7.0/apps/basic/example/www/controllers/__init__.py0000644000175000017500000000000011370740256024515 0ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/example/www/controllers/imports/0000755000175000017500000000000011412546122024104 5ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/example/www/controllers/imports/bad_import.py0000644000175000017500000000020111370740256026576 0ustar jmcfarlanejmcfarlane""" This module cannot be imported because it should itself raise an import error. """ import intentionally_non_existent_module Chula-0.7.0/apps/basic/example/www/controllers/imports/test.py0000644000175000017500000000056711370740256025454 0ustar jmcfarlanejmcfarlanefrom chula.www import controller class Test(controller.Controller): html = """
  1. bad import
  2. global error
  3. missing controller
""" def index(self): return self.html Chula-0.7.0/apps/basic/example/www/controllers/imports/syntax_exception.py0000644000175000017500000000033111370740256030066 0ustar jmcfarlanejmcfarlanefrom chula.www import controller class Syntax_exception(controller.Controller): def index(self): for missing_colon_in_expression in xrange(5) pass return 'This will never get called' Chula-0.7.0/apps/basic/example/www/controllers/imports/__init__.py0000644000175000017500000000000011370740256026212 0ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/example/www/controllers/imports/global_exception.py0000644000175000017500000000021711370740256030003 0ustar jmcfarlanejmcfarlane""" This module cannot be imported because raises an exception a the global scope, which happens during import """ print variable_not_defined Chula-0.7.0/apps/basic/example/www/__init__.py0000644000175000017500000000000011370740256022147 0ustar jmcfarlanejmcfarlaneChula-0.7.0/apps/basic/example/configuration.py0000644000175000017500000000174011370740256022447 0ustar jmcfarlanejmcfarlaneimport os from chula import config # Development configuration dev = config.Config() dev.classpath = 'example.www.controllers' dev.construction_controller = 'error' dev.construction_trigger = '/tmp/chula_example.stop' dev.debug = True dev.error_controller = 'error' dev.session = False if 'CHULA_REGEX_MAPPER' in os.environ: dev.mapper = ( # Home controller (r'^$', 'home.index'), (r'^/home/?$', 'home.index'), (r'^/home/index/?$', 'home.index'), # Sample controller (r'^/sample/?$', 'sample.index'), (r'^/sample/page/?$', 'sample.page'), # Bad imports (r'^/imports/bad_import/index/?$', 'imports.bad_import.index'), # Controller raising exceptions (r'^/imports/global_exception/index/?$', 'imports.global_exception.index'), # Controller with syntax errors (r'^/imports/syntax_exception/index/?$', 'imports.syntax_exception.index'), ) Chula-0.7.0/apps/basic/example/__init__.py0000644000175000017500000000000011370740256021323 0ustar jmcfarlanejmcfarlaneChula-0.7.0/docs/0000755000175000017500000000000011412546122014466 5ustar jmcfarlanejmcfarlaneChula-0.7.0/docs/conf.py0000644000175000017500000001433711370740256016004 0ustar jmcfarlanejmcfarlane# -*- coding: utf-8 -*- # # Chula documentation build configuration file, created by # sphinx-quickstart on Thu Dec 10 17:52:47 2009. # # This file is execfile()d with the current directory set to its containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys, os import chula # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # 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.todo', 'sphinx.ext.coverage'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Chula' copyright = u'2009, John McFarlane' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = chula.version # The full version, including alpha/beta/rc tags. release = chula.version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'Chuladoc' # -- Options for LaTeX output -------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ ('index', 'Chula.tex', u'Chula Documentation', u'John McFarlane', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True Chula-0.7.0/docs/install.rst0000644000175000017500000000376411370740256016707 0ustar jmcfarlanejmcfarlane============ Installation ============ You can install Chula in various ways, depending on what type of operating system you have, and how your system is configured. Chula +++++ Easy Install ^^^^^^^^^^^^ Probably the easiest way to install Chula is to use ``easy_install``. If your system has Setuptools installed, you can simply do:: sudo easy_install chula If you do not have ``easy_install`` you can easily get it for Linux, Mac, and windows from their website: http://pypi.python.org/pypi/setuptools. Tarball ^^^^^^^ If you don't have Setuptools, or don't want to use ``easy_install`` for whatever reason, you can download the latest source tarball from :ref:`downloads` and do the following, where you would replace "a.b.c" with "|version|" - you get the idea: :: tar -zxvf Chula-a.b.c.tar.gz cd Chula-a.b.c sudo python setup.py install Windows ^^^^^^^ For you windows cats, you can use the two methods above, or you can download the windows specific installer found here :ref:`downloads`. Then just execute the installer, and follow the prompts. Dependencies ++++++++++++ Mandatory ^^^^^^^^^ If you installed Chula via ``easy_install`` you already have the two mandatory requirements (Pytz and Simplejson), as they were installed in automatically. If not, on Ubuntu it's really easy to install them:: sudo apt-get install python-simplejson Optional ^^^^^^^^ If you want support for session, you will use one of the following: Postgresql ~~~~~~~~~~ Install the server and client:: sudo apt-get intall postgresql python-psycopg2 Then download the source Chula tarball from :ref:`downloads` and do:: tar -zxvf path/to/downloaded/tarball sudo su - postgres cd /path/to/exploded/tarball ./sql/session/rebuild This will create a user, database, and schema. CouchDB ~~~~~~~ Install the server and client:: sudo apt-get intall couchdb python-couchdb .. External hyperlinks .. _Python: http://www.python.org .. _reST: http://www.restructuredtext.org .. _Simplejson: http://www.undefined.org/python/ Chula-0.7.0/docs/library.rst0000644000175000017500000000017111370740256016672 0ustar jmcfarlanejmcfarlaneModules ======= .. toctree:: :maxdepth: 2 library/cache library/collection library/config library/error Chula-0.7.0/docs/library/0000755000175000017500000000000011412546122016132 5ustar jmcfarlanejmcfarlaneChula-0.7.0/docs/library/collection.rst0000644000175000017500000000220611370740256021026 0ustar jmcfarlanejmcfarlane:mod:`collection` ================= .. index:: single: collection .. data:: collection.UNSET :class:`collection.base.Base` key who's value has not yet been set by the consumer. :class:`Base` +++++++++++++ .. module:: collection.base .. class:: Base() Flexible collection that supports both dictionary and attribute style access. :class:`Restricted` +++++++++++++++++++ .. module:: collection.restricted .. class:: RestrictedCollection() Collection with constrained keys. This means that every instance of this class is guaranteed to **only** have the keys defined in the class. Removal of any of it's keys raise an :class:`error.RestrictecCollectionKeyRemovalError`, and any key additions will result in :class:`error.InvalidCollectionKeyError` being raised. Though the keys in this class are guarenteed, their values can either be defaulted, or set by the consumer. The setting of the keys by the consumer is enforced by an :class:`error.RestrictecCollectionMissingDefaultAttrError` exception being raised if :const:`collection.UNSET`. This class inherits from :class:`collection.base.Base`. Chula-0.7.0/docs/library/cache.rst0000644000175000017500000000276311370740256017746 0ustar jmcfarlanejmcfarlane:mod:`cache` -- Wrapper module for upstream memcache.py ======================================================= .. index:: single: caching single: wrapper pair: upstream; wrapper pair: wrapper; upstream pair: caching; objects .. module:: cache .. data:: ENCODING Encoding to be used with memcache keys. Default value is ``ASCII`` .. data:: SANITIZE Should invalid characters in the key be removed. Default is ``False`` .. class:: Cache(servers) Takes a list of two element tuples representing a memcached cluster .. staticmethod:: clean_key(key, sanitize=SANITIZE) Return a valid key encoded via :const:`ENCODING`. Sanitization of illegal caracters from the key will be performed if *sanitize* is ``True``. If the key is too long, or *sanitize* is ``False`` and illegal characters are found in the key, an :class:`error.InvalidCacheKeyError` exception will be raised. .. method:: close() Close client connection to server. .. method:: delete(key) Delete *key* from the cluster, returning ``True`` if deleted, ``False`` if not. .. method:: get(key) Fetch the value in memcache associated with *key*. .. method:: purge(key) Alias for :meth:`delete`. .. method:: set(key, value) Set *value* in the memcache cluster using *key*. Returns ``True`` if successfully persisted, else returns ``False``. .. method:: stats() Return a ``list`` of stats per server. Chula-0.7.0/docs/library/config.rst0000644000175000017500000003244711370740256020152 0ustar jmcfarlanejmcfarlane:mod:`config` -- Chula Configuration ==================================== .. index:: single: config single: configuration pair: application; configuration .. module:: config Chula applications read all configurations from a configuration file. This file holds a :class:`Config` object. Here's an example configuration file:: from chula import config prod = config.Config() prod.classpath = 'example.www.controllers' prod.construction_controller = 'construction' prod.construction_trigger = '/tmp/chula_example.stop' prod.debug = False prod.error_controller = 'error' prod.session = False Of the configuration options above, the only two that you need to understand now are the :attr:`Config.classpath` and :attr:`Config.error_controller` options. .. class:: Config() This class provides an organized structure to hold all supported chula configuration options. This class inherits from :class:`collection.restricted.RestrictedCollection` .. note:: The following two attributes are **mandatory**, meaning your configuration must provide values for them. .. attribute:: classpath The :attr:`classpath` option specifies a package in Python's path that holds one or more Chula controllers. The convention typically used is ``project.www.controllers``. You can use any location you like, it just needs to be a valid Python package in Python's path. Most applications will either be installed or use a symlink expose the package without actually installing it. Another option that's handy for development is to alter ``sys.path`` and inject the classpath at runtime. This is really easy for standalone type apps - but might be able to do this with Mod_python_ and Mod_WSGI_ too. The important thing here is that you need to have code that bootstraps Chula (so you can have a way to alter ``sys.path`` before Chula gets to it. There are two controllers that are special in that Chula needs to know exactly where they without any mapping logic. These controllers also must have a few methods implemented. The location of these controllers are relative to the defined :attr:`classpath`. .. attribute:: error_controller The :attr:`error_controller` specifies the controller to be called when something goes wrong. Here are a few example use cases that will result in the error controller being called, and the corresponding method called: ============= =========================================================== Method Use case ============= =========================================================== :meth:`e404` The inbound request does not map to a controller. :meth:`e500` During the processing of a request, and unhandled exception is thrown within the controller. ============= =========================================================== Using an example configuration, if a request is made that cannot be mapped, Chula will call ``example.www.controllers.error.Error.e404()``. If an unhandled exception occurs ``example.www.controllers.error.Error.e500()`` will be called. This also means that if a request is made that cannot be mapped, and something goes wrong inside :meth:`e404` then both controller methods will actually get called. This makes it very important that your error controller not be capable of throwing unhandled exceptions. If you want to have informative error pages during development, you'll want to place that code inside your error controller's :meth:`e500` method that exposes this information. You can find a very simple implementation that does this inside this application's error controller and view. .. note:: The following attributes are all optional. .. attribute:: add_timer If :attr:`add_timer` is ``True`` an HTML fragment will be added to the body of the page, including the following pieces of information: * Chula adapter being used * Server hostname * Chula version * Processing time (server side) The fragment will look something like this::
FCGI/WSGI
li83-242
0.5.0
104.279995 ms
This information can be used by client side javascript to display how fast search results were obtained, for example. If your application happens to use aggressive caching (like full html caching) the timer will still be accurate. .. attribute:: construction_controller The :attr:`construction_controller` specifies the controller to be called in the event the application is marked "under construction". This is optional, but you'll be glad it's there when you need it. The basic idea of the construction controller is that all requests get routed to it when a specific file exists on disk. This means that when you need to take your site down for maintenance or something you can just *touch* the file configured via :attr:`construction_trigger`. The mandatory method that must exist in this controller is ``index()``. For example with the above configuration this would be ``example.www.controllers.construction.Construction.index()``. .. attribute:: construction_trigger Fully qualified path to a file on disk. If the file exists, the construction controller will be called for all requests. .. attribute:: debug The :attr:`debug` flag has a default value of ``True`` and is only used by the Chula queue server. It's main intention is really to be a hook that your application can use to alter it's behavior during development. .. attribute:: local The Chula configuration class is :class:`collection.restricted.RestrictedCollection`, meaning it's a dictionary with a pre defined set of keys. Any key additions or removals will result in an exception. This is done to ensure that the configuration is extremely stable. In the event you would like to store configutation local to your application, the :attr:`local` attribute is available. This can hold anything of your choosing. .. attribute:: log Fully qualified path to a file on disk. This will will hold Chula specific logging. The data sent to this file will only be ``warnings`` and above. The default value is :file:`/tmp/chula.log`. The user running the application must have write access to this file. .. attribute:: mapper Chula currently has support for classpath and regex based url mappings. The default value is to perform automatic classpath based mappings. **Classpath Mapper** The classpath mapper uses an algorithm to choose the right controller method for a given url. Here are a few examples of the mapping algorithm used (assuming the configuration example at the top of this page): * http://localhost 1. ``example.www.controllers.home.Home.index()`` With no :const:`env.REQUEST_URI` a direct call to the home controller can be made. The home controller is named ``home`` and is expected to be at the root of the specified :attr:`config.Config.classpath`, with a class named ``Home`` and a method named ``index()``. * http://localhost/products 1. ``example.www.controllers.products.Products.index()`` #. ``example.www.controllers.home.Home.products()`` #. ``example.www.controllers.error.Error.e404()`` When there is a single part this can either be a specified controller (and an assumed method) or this could be a specified method on the home controller. * http://localhost/products/dog 1. ``example.www.controllers.products.Products.Dog()`` #. ``example.www.controllers.error.Error.e404()`` When there are two parts, it must be a specified controller and method. * http://localhost/products/dog/small 1. ``example.www.controllers.products.dog.Dog.index()`` #. ``example.www.controllers.error.Error.e404()`` When there are more than two parts, it must be fully qualified, meaning a package(s), module, and controller. **Regex Mapper** In the event you would like to use regex style mappings, set this value to a tuple of dictionaries containing the regex:controller mappings. Here is an example regex mapper:: mapper = ( (r'^$', 'home.index'), (r'^/about/?$', 'home.about'), (r'^/login/?$', 'auth.login'), (r'^/logout/?$', 'auth.logout') ) In the map above, the first argument is a regular expression (this might actually become a compiled regex in time) that matches against :const:`env.REQUEST_URI`, and the second argument is a dot syntax that matches the relative path to a controller method. The syntax assumes the path is all lower case, but it will expect all actual controller classes to have an upper cased first letter, and the parens on the method are implied. So using the last map in the map above, the actual class/method used would be: ``example.www.controllers.auth.Auth.logout()`` .. attribute: mqueue_db Fully qualified path to a directory on disk. When the Chula queue is used, this directory will be used to hold queue data. The default value is :file:`/tmp/chula/mqueue`. The user running the queue must have write access to the directory. .. attribute:: mqueue_host Hostname that the Chula queue client and server should use. The default value is ``localhost``. .. attribute:: session if :attr:`session` is ``True`` session is enabled, else not. Session is enabled by default. See session_ for additional detail on setup and configuration. .. attribute:: session_db Database name used for persisting session. The default value is ``chula_session``. .. attribute:: session_encryption_key I think this is a value no longer being used. At one point the cookie value was being hashed. Currently Chula is directly using :class:`Cookie.SimpleCookie` and at some point lost support for hashing the value. This might be added back in at some point. .. attribute:: session_host Database host used for persisting session (currently only PostgreSQL) .. attribute:: session_max_stale_count The maximum number of session requests allowed to be served directly from the cache. The default value for this setting is ``10``. When the number of reqeusts exceed this value, the configured backend will be used. This is designed to increase the scalability of the session store. Chula session is always fronted by Memcached, and it's assumed that Memcached is reasonably reliable, thus with the default configuration the session backend will only see 10% of the traffic. In the event of a cache miss, the backend is always used. The only value in decreasing this value is to reduce the changes of stale data in the event of a cache failure. .. attribute:: session_memcache Memcached cluser to be used for session. This value holds a list of tuples - each containing a hostname:port syntax. The default value is ``[('localhost:11211', 1)]``. This value is directly fed to memcache.py which happens to be bundled with Chula. NOTE: There are plans to add support for libmemcached_ .. attribute:: session_name The name of the the session cookie to be sent to the browser. The default value is ``chula-session``. .. attribute:: session_nosql HTTP path to a running CouchDB_ installation. If this value is specified, CouchDB will be used for the session backend instead of PostgreSQL. The default value is ``None`` - which means PostgreSQL_ is currently the default backend session store. .. attribute:: session_password Password to the PostgreSQL session database .. attribute:: session_port Port to the PostgreSQL session database .. attribute:: session_timeout Session timeout value .. attribute:: session_username Username to the PostgreSQL session database .. attribute:: strict_method_resolution If :attr:`strict_method_resolution` is ``True`` the url mapper will send the request directly to the error controller (:meth:`e404` method) if a direct map is not possible. So basically the mappers will not attempt to use the implied ``index()`` method. This is not true for the homepage, as it's always an implied map to ``home.index()``. The default value is ``False``. .. _session: session.html .. _FastCGI: http://en.wikipedia.org/wiki/FastCGI .. _Memcached: http://www.memcached.org .. _Mod_python: http://www.modpython.org .. _Mod_WSGI: http://code.google.com/p/modwsgi/ .. _MySQL: http://www.mysql.org .. _PostgreSQL: http://www.postgresql.org .. _libmemcached: http://code.google.com/p/python-libmemcached/ .. _CouchDB: http://couchdb.apache.org Chula-0.7.0/docs/library/error.rst0000644000175000017500000001106011370740256020022 0ustar jmcfarlanejmcfarlane:mod:`error` -- Chula exceptions ================================ .. index:: single: exceptions single: errors pair: exception; handling .. module:: error .. class:: ChulaException(msg=None, append=None) Chula exception class which adds additional functionality to aid in efficiently raising custom exceptions. .. method:: _get_message() Getter for a message property, to avoid using an attribute named "message" which will raise deprecation errors in Python-2.6. Returns self._message .. method:: _set_message(msg) Getter for a message property, to avoid using an attribute named "message" which will raise deprecation errors in Python-2.6. .. method:: __str__(append=None) Return the message itself .. method:: msg() When the msg method is not overloaded, return a generic message .. class:: ControllerClassNotFoundError(_pkg, append=None) Exception indicating the requested controller class not found. Inherits from :class:`ChulaException` .. class:: ControllerImportError(_pkg, append=None) Exception while trying to import the controller. Inherits from :class:`ChulaException` .. class:: ControllerMethodNotFoundError(_pkg, append=None) Exception indicating the requested controller method not found. Inherits from :class:`ChulaException` .. class:: ControllerModuleNotFoundError(_pkg, append=None) Exception indicating the requested module method not found. Inherits from :class:`ChulaException` .. class:: ControllerMethodReturnError() Exception indicating that a controller method is returning ``None``, which is probably not on purpose. It's true that we do cast all output as a string, thus 'None' is technically valid, it's most likely that the controller method simply forgot to return. This will save time by pointing this out. If you really need to return ``None``, then return: 'None'. Inherits from :class:`ChulaException` .. class:: ControllerRedirectionError() Exception indicating that the controller was unable to perform the requested redirect. Inherits from :class:`ChulaException` .. class:: InvalidAttributeError(key, append=None) Exception indicating an invalid attribute was used. Inherits from :class:`ChulaException` .. class:: InvalidCacheKeyError(key, append=None) Exception indicating an invalid key was used against a cache source. Inherits from :class:`ChulaException` .. class:: InvalidCollectionKeyError(key, append=None) Exception indicating an invalid key was used against a restricted collection class. Inherits from :class:`ChulaException` .. class:: MalformedConnectionStringError() Exception indicating that the database connection string used is invalid. Inherits from :class:`ChulaException` .. class:: MalformedPasswordError() Exception indicating that the password used does not meet minimum requirements (aka: isn't strong enough). Inherits from :class:`ChulaException` .. class:: TypeConversionError(_value, _type, append=None) Exception indicating that the requested data type conversion was not possible. Inherits from :class:`ChulaException` .. class:: UnsupportedDatabaseEngineError(engine, append=None) Exception indicating a requst for an unsupported database engine Inherits from :class:`ChulaException` .. class:: UnsupportedMapperError(_pkg, append=None) Exception indicating an invalid mapper configuration Inherits from :class:`ChulaException` .. class:: UnsupportedUsageError() Exception indicating the chula api is being misused. Inherits from :class:`ChulaException` .. class:: MissingDependencyError(_pkg, append=None) Exception indicating a required dependency of chula is either missing or of an incompatible version. Inherits from :class:`ChulaException` .. class:: RestrictecCollectionKeyRemovalError(key, append=None) It is illegal to remove a key from a RestrictedCollection object. Inherits from :class:`ChulaException` .. class:: RestrictecCollectionMissingDefaultAttrError(key, append=None) Exception indicating that a restricted attribute was not given a default value. Inherits from :class:`ChulaException` .. class:: SessionUnableToPersistError() Chula is unable to persist either to PostgreSQL or Memached. Inherits from :class:`ChulaException` .. class:: WebserviceUnknownTransportError(key, append=None) Exception indicating that the specified webservice transport is either unknown or unsupported. Inherits from :class:`ChulaException` Chula-0.7.0/docs/changelog.rst0000644000175000017500000002246211412545770017165 0ustar jmcfarlanejmcfarlane.. _downloads: ======================= Downloads/Release Notes ======================= Chula v0.8.0 (dev) ++++++++++++++++++ *Still under development* :Source: http://github.com/jmcfarlane/chula Chula v0.7.0 ++++++++++++ *Released 2010-06-29* * Added support for native couchdb sorting * Removed support for app level sorting of couchdb documents :Documentation: `Chula-0.7.0 `_ :Download: ``_ :Download: ``_ :Download: ``_ :Download: ``_ (unsupported) :Download: ``_ (unsupported) :Download: ``_ :Source: http://github.com/jmcfarlane/chula/tree/v0.7.0 Chula v0.6.0 ++++++++++++ *Released 2010-05-07* * Updated the manifest to include apps, and test cases * Added support for Google App Engine. * Added ability to fetch data from CouchDB using views * Removed dependency on pytz. * Removed usage of :func:`socket.gethostname`, which can have a negative impact on performance (especially in heavily threaded applications). This also makes it possible to use Chula in environments that do not have access to :mod:`socket`. * When looking for :mod:`simplejson`, also try using the copy that ships with Django. * Updated the logger to not use a file handler when :attr:`config.Config.log` is ``None``. * Fixed defect in Couchdb connection cache. * Performance improvements to :mod:`nosql.couch` :Documentation: `Chula-0.6.0 `_ :Download: ``_ :Download: ``_ :Download: ``_ :Download: ``_ (unsupported) :Download: ``_ (unsupported) :Download: ``_ :Source: http://github.com/jmcfarlane/chula/tree/v0.6.0 Chula v0.5.0 ++++++++++++ *Released 2010-02-22* * Added support for Setuptools. This results in Chula being installable via ``easy_install``. * Added a bit more documentation on how to install Chula. :Documentation: `Chula-0.5.0 `_ :Download: ``_ :Download: ``_ :Download: ``_ :Download: ``_ (unsupported) :Download: ``_ (unsupported) :Download: ``_ :Source: http://github.com/jmcfarlane/chula/tree/v0.5.0 Chula v0.4.0 ++++++++++++ *Released 2010-02-10* * Added simple wrapper around couchdb-python * Added support for couchdb session store. This means you now can choose between PostgreSQL/Memcached or CouchDB/Memcached. * Added singleton decorator * Added initial logging support * Added a regex style url mapper. This means you can now choose between automatic class mapping and hand crafted mappings via regular expressions (this should be similar to Django style routing). * Added (initial) documentation using Sphinx (not yet published) * Updated memcache.py to version 1.45 * Fixed regression in chula.www.cookie where the cookie domain was getting prefixed with "." once for every cookie - oops. * Refactored session into a package. When the couchdb backend was added, not all of the failover logic was being implemented. To clean things up properly the session logic had to be abstracted away from the backends. Now there is a single session class that supports n number of backends that all use the same interface. * Moved third party libs (selenium, memcache) into chula.vendor :Download: `Chula-0.4.0.tar.gz `_ :Documentation: `Chula-0.4.0 `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.4.0 Chula v0.3.0 ++++++++++++ *Released 11/03/2009* * Improved cookie handling (better RFC compliance) * worked around Python-2.6 deprecation of Exception.message * More unit and bat tests * Enforced str key types with memcached * Disabled memcached key sanitization by default :Download: `Chula-0.3.0.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.3.0 Chula v0.2.0 ++++++++++++ *Released 09/27/2009* * Added chula.data.str2unicode * Added initial bat tests * Improved handling of exceptions during controller import * Improved chula.mail to properly handle unicode * Moved unit tests out of the source tree * Added support for Selenium tests :Download: `Chula-0.2.0.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.2.0 Chula v0.1.0 ++++++++++++ *Released 06/29/2009* * Fixed corner case in FieldStorage array structures * Fixed defect in chula.date.str2date() with UTC +n * Fixed run_tests so it works without Chula being installed * Improved chula.data.str2date to support years 1000 to 2999 (jmathai). * Improved chula.data.str2date to support a unix timetamp * Added two sample applications * Added documentation (one of the sample apps) * Added support for custom queue messages * Minor tweaks to reduce memory consumption * Made session optional, but enabled by default :Download: `Chula-0.1.0.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.1.0 Chula v0.0.6 ++++++++++++ *Released 04/11/2009* * Added support for FasgCGI * Added an ASCII transport to chula.webservice * Added a webservice decorator: chula.webservice.expose * Added testutils module * Fixed defect where error controller not found when using controller packages * Fixed defect in data.commaify with less than 2 decimals * Improved the timer to not break xhtml compliance :Download: `Chula-0.0.6.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.0.6 Chula v0.0.5 ++++++++++++ *Released 12/11/2008* * Improved chula.collection adding an add() method * Improved chula.webservice removing dependency on mod_python * Improved chula.www.cookie removing dependency on mod_python * Improved env to hold GET, POST (previously only a combo) * Improved support for copy.deepcopy on chula.collection * Improved error.e404 used when method resolution fails * Improved "under construction" flow by removing dependency on session * Improved chula.queue to keep processed/failed messages for later review * Changed behavior to always call the error controller on exception. This is slightly less convienent, but encourages better testing of error handling code paths for apps using Chula. * Changed behavior to call e404 when the controller requested isn't found * Added initial support for WSGI * Added initial suport for the Python simple_server :Download: `Chula-0.0.5.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.0.5 Chula v0.0.4 ++++++++++++ *Released 8/19/2008* * Changed dependency checking to be further down the stack * Cleaned up directory structure of source tree a little * Improved installer to use distro specific locations * Promoted chula.collection into a package * Promoted chula.db into a package (much better now) * Fixed defect in chula.collection when copy.deepcopy is used * Wired up specified error controller (previously unused) * Added chula.collection.UboundCollection * Added chula.data.isregex and chula.db.cregex * Added chula.mail * Added chula.system * Added support for an "under construction" controller * Added support for sqlite to chula.db.datastore * Added tcp based message queue (working, but very much not ready to be used) :Download: `Chula-0.0.4.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.0.4 Chula v0.0.3 ++++++++++++ *Released 6/15/2008* * Added module for working with caching services, currently only Memcache is supported. * Added support for controllers inside of packages, previously only a single namespace was supported. Note that this feature is probably going to be moved into a FileMapper so the StandardMapper can move to more of a map based model. * Added render method to pager.Pager for those that want to subclass the output. The base method simply returns the pager unmodified. * Remove "danger" logic from db.py as it's best left up to the consumer to handle that type of logic. It was poorly implemented anyway :) :Download: `Chula-0.0.3.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.0.3 Chula v0.0.2 ++++++++++++ *Released 1/21/2008* * Fixed defect where env.host is None * Fixed defect where env.protocol_type is None * Fixed defect where request_uri of: "/?" was loading e404 * Fixed defect where session not deleted on logout * More gracefully handle clients lacking cookie support * Allow the controller to have direct access to the cookie object. This provides access to it's destroy() method, useful for logout pages. * Tweaks to improve support for static content * Improved reliability/accuracy of session * Added timer to html output (turn off with config.add_timer) * Handle exception on premature client disconnection :Download: `Chula-0.0.2.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.0.2 Chula v0.0.1 ++++++++++++ *Released 12/14/2007* * Initial release :Download: `Chula-0.0.1.tar.gz `_ :Source: http://github.com/jmcfarlane/chula/tree/v0.0.1 Chula-0.7.0/docs/getting_started.rst0000644000175000017500000003152711370740256020426 0ustar jmcfarlanejmcfarlane=============== Getting Started =============== .. toctree:: :maxdepth: 2 install Terminology +++++++++++ Welcome to Chula. Let's go thru a few things before you get started building your first app. Chula is a simple toolkit that is based on the MVC_ pattern. From now on we'll use the terms "*model*", "*view*", and "*controller*" when describing things. Here's a brief summary of what these terms as they relate to Chula: =========== =================================================================== Term Description =========== =================================================================== Model The logic and data of an application. These are usually standard Python classes. They do work, implement algorithms, and hold data. View The view is responsible for presentation. Examples of this would be: * Mako_ * Cheetah_ * reST_ (restructured text) Controller The controller is the main class responsible for coordinating everything. The controller is responsible for capturing user input, calling the model for processing, invoking the view, passing the model to the view, and returning view's output to the client. =========== =================================================================== Application structure +++++++++++++++++++++ Here is an example file structure of a bare bones Chula application:: |-- example | |-- __init__.py | |-- configuration.py | `-- www | |-- __init__.py | `-- controllers | |-- __init__.py | |-- error.py | `-- home.py `-- webserver In the list of files above there is a Python package_ named ``example`` that holds the entire application. Inside it there are two controllers, a configuration, and a webserver. The webserver is not specific to Chula really, but rather a quick and dirty way to launch a Chula application without needing a webserver installed and configured. Technically speaking this is all you need to run a Chula application. Run a sample Chula application ++++++++++++++++++++++++++++++ If you would like to try the above application right now, you'd type this in your terminal:: cd wherever_you_unpacked_the_chula_tarball ./apps/basic/webserver At this point you should be able to point your browser at http://localhost:8080 and browse a hello world application that ships with Chula. The actual purpose of the application is to serve as a way to run BAT_ tests, but it's useful for this purpose as well. Hit :kbd:`Control-c` to stop the server, and let's move on. Create your own hello world application +++++++++++++++++++++++++++++++++++++++ Directory structure ~~~~~~~~~~~~~~~~~~~ The structure we want so far will support view templates, web server configuration, client side static files, and a python package:: cd Desktop mkdir -p Myapp/config # Web server configs mkdir -p Myapp/myapp # Python package mkdir -p Myapp/view # View emplates mkdir -p Myapp/www # Static files (client side) Make ``myapp`` an actual python package:: touch Myapp/myapp/__init__.py Configuration ~~~~~~~~~~~~~ Create the following file in :file:`Myapp/myapp/configuration.py`:: from chula import config # Development configuration dev = config.Config() dev.classpath = 'myapp.controllers' dev.construction_controller = 'error' dev.construction_trigger = '/tmp/myapp.stop' dev.debug = True dev.error_controller = 'error' dev.session = False Controllers ~~~~~~~~~~~ Create a package called ``controllers`` to hold all app controllers, this makes it easy to distinguish controllers from your other python modules you might have:: mkdir Myapp/myapp/controllers touch Myapp/myapp/controllers/__init__.py Create the **mandatory** ``error`` controller configured previously by creating :file:`Myapp/myapp/controllers/error.py` :: from chula.www import controller class Error(controller.Controller): def index(self): return 'Sorry, the site is down for maintenance' def e404(self): return 'Page not found' def e500(self): return 'Trapped Error: %s' % self.model.exception.exception Now create a controller that will serve as the homepage, as well as a blog or something, :file:`Myapp/myapp/controllers/home.py` :: from chula.www import controller class Home(controller.Controller): def index(self): return 'Hello world' def blog(self): return 'This is my blog' At this point we have a full Chula application, but we don't have a way to run it. For now, let's create a standalone web server script for testing purposes. Next, we'll actually wire up the application against a few different web servers. Test server ~~~~~~~~~~~ Create :file:`Myapp/webserver.py` :: import os import sys from wsgiref.simple_server import make_server from chula.www.adapters.wsgi import adapter # Expose the myapp python package, as it's not "installed" sys.path.insert(0, os.getcwd()) # Import my configuration we created above from myapp import configuration # Define a wsgi application, passing in our (dev) configuration @adapter.wsgi def application(): return configuration.dev # Setup a simple server using the proxy app and it's configuration port = 8080 httpd = make_server('', port, application) try: print 'Starting server on: http://localhost:%s' % port httpd.serve_forever() except KeyboardInterrupt: sys.exit() Test it! ~~~~~~~~ Let's try out what we have so far:: cd Myapp python webserver.py At this point you should be able to browse the following urls: #. http://localhost:8080 #. http://localhost:8080/home/blog Hit :kbd:`Control-c` to stop the server. Add env vars ~~~~~~~~~~~~ Let's add a page that's a little bit more usefull. This one will generate an HTML table of the environment variables. This page will also use Mako_ for the view. Update controller ^^^^^^^^^^^^^^^^^ Let's update our ``home`` controller to look like this, :file:`Myapp/myapp/controllers/home.py` :: from chula.www import controller # This is a new import from mako.template import Template class Home(controller.Controller): def index(self): return 'Hello world' def blog(self): return 'This is my blog' # This is the new method def envinfo(self): # Add env variables to the model self.model.env = self.env # Load our Mako template view = Template(filename='view/envinfo.tmpl') # Return the rendered template, passing in our model return view.render(model=self.model) Mako template ^^^^^^^^^^^^^ Now let's create the mako template referenced above, :file:`Myapp/view/envinfo.tmpl` :: Env Variables

Environment Variables

%for key, value in model.env.iteritems(): %endfor
Key Value
${key} ${value}
Try it! ^^^^^^^ Let's see what this looks like now:: cd Myapp python webserver.py Now browse to http://localhost:8080/envinfo and you should see a table of environment variables. It's a little hard to read because the keys are not sorted, but that's because keys in the standard dict are not sorted. I leave the sorting issue as an excercise for the reader :) Hit :kbd:`Control-c` to stop the server. Web server integration +++++++++++++++++++++++ Chula integrates with WSGI_, Mod_python_, and FastCGI_. Let's go thru how you would integrate your hello world application with each of these. Nginx via FastCGI ~~~~~~~~~~~~~~~~~ The first step in FastCGI_ integration, is to create the application server that Nginx_ will sent requests to. For this example, we'll use a unix domain socket, rather than TCP/IP for connectivity between the two. FastCGI process ^^^^^^^^^^^^^^^ Create :file:`Myapp/fastcgi.py` :: try: from flup.server.fcgi_fork import WSGIServer except ImportError: from chula.vendor.fcgi import WSGIServer print "Unable to import flup.server.fcgi import WSGIServer" print " >>> Falling back on old version available in Chula" from chula.www.adapters.fcgi import adapter from myapp import configuration @adapter.fcgi def application(): return configuration.dev # Start the server which will handle calls from the webserver WSGIServer(application, bindAddress='/tmp/myapp.socket').run() Start up the FastCGI_ process:: python Myapp/fastcgi.py Make sure Nginx has permissions to write to the socket:: chmod o+w /tmp/myapp.socket TODO: Provide an example init script to properly startup the socket, setting permissions and what not. Nginx config ^^^^^^^^^^^^ Configure Nginx_ to proxy application requests to our application. Add this to the ``server`` block of your Nginx_ configuration:: # Send all requests without a file extension to myapp: location ~ ^([a-z/_])+$ { # This is needed when using Ubuntu include /etc/nginx/fastcgi_params; # FastCGI parameter settings fastcgi_read_timeout 3m; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param SERVER_ADMIN NA; fastcgi_param SERVER_SIGNATURE nginx/$nginx_version; # The path to our running unix domain socket server unix:/tmp/myapp.socket; } Restart Nginx_ :: sudo /etc/init.d/nginx restart Try it! ^^^^^^^ Now you should be able to hit: http://your-server/home/blog WSGI via Mod_WSGI ~~~~~~~~~~~~~~~~~ Mod_WSGI_ is the best choice if you want to run your app on Apache_. This configuration also happens is a little easier to setup. Register app ^^^^^^^^^^^^ When running applications under Apache, the Python interpreter is owned by ``nobody`` or ``apache`` or something, thus you need to register your application so that it's in python's ``path``. The easiest way to do this is via a symlink. You'll use something similar to one of these (but specific to your computer's setup):: # Gentoo sudo ln -s /path/to/Myapp/myapp /usr/lib/python2.6/site-packages/myapp # Ubuntu sudo ln -s /path/to/Myapp/myapp /usr/local/lib/python2.6/dist-packages/myapp WSGI handler ^^^^^^^^^^^^ Create :file:`Myapp/wsgi.py`, which will be loaded by Mod_WSGI_ :: from chula.www.adapters.wsgi import adapter from myapp import configuration @adapter.wsgi def application(): return configuration.dev Apache config ^^^^^^^^^^^^^ Add this to your ``VirtualHost`` :: WSGIScriptAliasMatch ^([a-z/_])+$ /full/path/to/Myapp/wsgi.py Try it! ^^^^^^^ Restart Apache:: sudo /etc/init.d/apache restart Now you should be able to hit: http://your-server/home/blog Apache via Mod_python ~~~~~~~~~~~~~~~~~~~~~ Mod_PYTHON_ was the first way to run Python applications under Apache with excellent performance. It's still awesome, though Mod_WSGI_ has superceeded it. Register app ^^^^^^^^^^^^ When running applications under Apache, the Python interpreter is owned by ``nobody`` or ``apache`` or something, thus you need to register your application so that it's in python's ``path``. The easiest way to do this is via a symlink. You'll use something similar to one of these (but specific to your computer's setup):: # Gentoo sudo ln -s /path/to/Myapp/myapp /usr/lib/python2.6/site-packages/myapp # Ubuntu sudo ln -s /path/to/Myapp/myapp /usr/local/lib/python2.6/dist-packages/myapp Mod_python handler ^^^^^^^^^^^^^^^^^^ Create :file:`Myapp/myapp/mod_python.py`, which will be loaded by Mod_PYTHON_ :: from chula.www.adapters.mod_python import adapter from myapp import configuration @adapter.handler def application(): return configuration.dev Apache config ^^^^^^^^^^^^^ Update your apache ``VirtualHost`` to have:: # Send all application requests to a stub with a ".py" extension AliasMatch ^([a-z/_])+$ PLACEHOLDER.py PythonDebug On # Send requests to *.py to myapp's handler AddHandler mod_python .py PythonHandler myapp.mod_python Try it! ^^^^^^^ Restart Apache:: sudo /etc/init.d/apache restart Now you should be able to hit: http://your-server/home/blog What's next +++++++++++ When creating your own application is's going to be important that you understand the configuration options available. You'll also want to learn more about featues available, and how to use them. You can find detail on configuration `here `_. .. _Apache: http://www.apache.org .. _BAT: http://en.wikipedia.org/wiki/Acceptance_testing .. _Cheetah: http://www.cheetahtemplate.org .. _FastCGI: http://en.wikipedia.org/wiki/FastCGI .. _Mako: http://www.makotemplates.org .. _Mod_python: http://www.modpython.org .. _Mod_WSGI: http://code.google.com/p/modwsgi/ .. _MVC: http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller .. _Nginx: http://nginx.org .. _package: http://docs.python.org/tutorial/modules.html#packages .. _reST: http://www.restructuredtext.org .. _WsGI: http://www.wsgi.org Chula-0.7.0/docs/_static/0000755000175000017500000000000011412546122016114 5ustar jmcfarlanejmcfarlaneChula-0.7.0/docs/_static/flow.dot0000644000175000017500000000222611370740256017604 0ustar jmcfarlanejmcfarlanedigraph chula { ADAPTER [label="Chula Adapter" style="filled" fillcolor="yellow"] apache [label="Apache"] browser [label="Web Browser" style=filled fillcolor=orange] cheetah [label="Cheetah"] config [label="Chula Config" style=filled fillcolor="#91cdfb"] controller [label="Chula Controller" style=filled fillcolor=green] data [label="Your data"] fastcgi [label="FastCGI"] kid [label="Kid"] lighttpd [label="Lighttpd"] mako [label="Mako"] memcache [label="Memcached"] model [label="Chula Model" style="filled" fillcolor="#ffcd85"] mod_python [label="Mod_python"] nginx [label="Nginx"] postgres [label="PostgreSQL"] session [label="Session Cluster"] wsgi [label="WSGI"] /* Controller */ controller->model controller->session controller->cheetah controller->mako controller->kid /* Web Servers */ HTTP->apache->ADAPTER HTTP->lighttpd->ADAPTER HTTP->nginx->ADAPTER /* Adapter */ ADAPTER->mod_python->controller ADAPTER->wsgi->controller ADAPTER->fastcgi->controller /* Session */ session->postgres session->memcache /* Glue */ model->data browser->HTTP model->controller config->ADAPTER } Chula-0.7.0/docs/index.rst0000644000175000017500000000112211370740256016332 0ustar jmcfarlanejmcfarlane.. Chula documentation master file, created by sphinx-quickstart on Thu Dec 10 17:52:47 2009. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. Welcome to Chula's documentation! ================================= .. toctree:: :maxdepth: 2 about getting_started session Download Chula ============== .. toctree:: :maxdepth: 3 changelog Module Library ============== .. toctree:: :maxdepth: 1 library Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` Chula-0.7.0/docs/about.rst0000644000175000017500000001076711370740256016354 0ustar jmcfarlanejmcfarlane=========== About Chula =========== History +++++++ Chula is written by John McFarlane (aka me). When I first wanted to learn Python I started a project named *Apple*. At the time there were lots of frameworks for building web applications with Python, but I was interested in learning Python so it seemed more fun to make my own. Additionally I was surprised by the complexity of other frameworks. I was looking for something a bit simpler. I hacked on Apple for a few years, and then decided to start over and try to improve things based on what I had learned - 3 months later I released the first version of Chula. Since that time I've tried to improve things and add features as needed, but I never lost sight of it's main purpose: to be a vehicle for me to have fun and learn stuff. If you're really wanting to build the next killer application I'd prolly recommend using `Django `__. If you're looing for something smaller or even just something *different* then give Chula a try :) Features ++++++++ * Web servers: Most of the common setups are supported (Mod_Python_, Mod_WSGI_, and FastCGI_). * Session: Uses both Memcached_ and PostgreSQL_ for cluster safe storage that scales pretty well. * Message queue: Support for asynchronous processing of messages * Typical stuff: Environment, GET and POST variables * Speed: Chula seems to perform pretty well Dependencies ++++++++++++ Chula depends on the following packages, some of which are optional depending on configuration: Mandatory ~~~~~~~~~ #. Python_ 2.6 (2.5 can work with minor changes, see Roadmap_) #. Simplejson_ Optional ~~~~~~~~ * CouchDB_ and the couchdb-python_ driver * Flup_ (If using FastCGI this is recommended, but still optional) * Mako_ (Optional but recommended) * Memcached_ * PostgreSQL_ and the Psycopg2_ driver * Web server: Nginx_, Apache_, etc. If you intend on enabling support for session, you will need Memcached, and either PostgreSQL or CouchDB and their respective drivers. You can learn more about session `here `_. Source code +++++++++++ Chula uses the Git_ version control system. The official Chula repository can be found on Github_. Issue tracker +++++++++++++ Chula uses the issue tracker that's integrated with Github_. If you find defects or have ideas for improvement please feel free to file issues. In the event Chula becomes more popular, a more sophisticated tracker will be used. The link to the Github tracker is here: http://github.com/jmcfarlane/chula/issues Release cycle +++++++++++++ Generally there are about 4 releases a year Roadmap +++++++ The roadmap right now is pretty small. Here are the features I'm currently thinking about: 1. Profiling - profilng of both Chula and apps running on it #. Support for MySQL_ based session. Currently only PostgreSQL_ and CouchDB are supported. With either backend Memcached_ will continue to be used. #. Consider adding back support for Python-2.5. This wouldn't be too difficult - just need to change how the logging singleton works. The other complication would be managing early versions of 2.5 that did not include httplib2, as this is needed by couchdb (if that session backend is used). To further complicate things, the version of httplib2 available to most distros [that ship such an old version of Python] is not compatible with couchdb-python_. Who's using it ++++++++++++++ I don't actually know of anyone that's using it. If you're using it, let me know. .. Internal hyperlinks .. _About: about.html .. _`Getting Started`: getting_started.html .. External hyperlinks .. _Apache: http://www.apache.org .. _Cheetah: http://www.cheetahtemplate.org .. _CouchDB: http://couchdb.apache.org .. _couchdb-python: http://code.google.com/p/couchdb-python/ .. _FastCGI: http://en.wikipedia.org/wiki/FastCGI .. _Flup: http://trac.saddi.com/flup .. _Git: http://www.git.cz .. _Github: http://www.github.com/jmcfarlane/chula .. _Mako: http://www.makotemplates.org .. _Memcached: http://www.memcached.org .. _Mod_python: http://www.modpython.org .. _Mod_WSGI: http://code.google.com/p/modwsgi/ .. _MVC: http://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller .. _MySQL: http://www.mysql.org .. _Nginx: http://nginx.org .. _package: http://docs.python.org/tutorial/modules.html#packages .. _PostgreSQL: http://www.postgresql.org .. _Psycopg2: https://dndg.it/cgi-bin/gitweb.cgi?p=public/psycopg2.git .. _Python: http://www.python.org .. _reST: http://www.restructuredtext.org .. _Simplejson: http://www.undefined.org/python/ Chula-0.7.0/docs/session.rst0000644000175000017500000000677311370740256016727 0ustar jmcfarlanejmcfarlane======= Session ======= Chula includes session support via cookies and backend servers. Currently there are three supported backends: 1. Memcached 2. PostgreSQL 3. CouchDB Via configuration you can choose between PostgreSQL or CouchDB as the main session store - Memcached is always used as a cache. Cluster Safe ++++++++++++ Because local storage is not used, Chula is has cluster safe session. Basically this means that you can fire up multiple instances of your application and they all share session. This also means you don't have to use `sticky sessions` in your load balancer configuration. Scalable ++++++++ Chula maintains session in Memcached_ backed by a persistent data store. Because Memcache is reasonably reliable, only a percentage of session requests are actually sent to the backend. The default configuration is to update the backend every 10 requests. This means the session backend (which is slower than cache) is only serving 10% of the actual traffic. The frequency of backend updates is configurable via :attr:`config.Config.session_max_stale_count`, and the backend is always consulted in the event of a cache miss. A current limitation of the session store is that it does not use connection pooling. This can optionally be added by fronting your PostgreSQL server with pgpool_. When CouchDB is used as the backend, connection pooling isn't relevent as it uses HTTP. Native Storage ++++++++++++++ Chula stores your session values as pickle_'d strings (via cPickle) thus you can store any values that are serializeable by cPickle. Maintenance +++++++++++ Chula does not clean up all PostgreSQL based sessions. You will likely want to configure a cron job to periodically delete stale sessions from the database. When CouchDB is used, the sessions are sharded by year/month. This allows you to easily purge off old sessions by year/month. Setup +++++ PostgreSQL ---------- In order to use Chula session you will need to create the database used by the backend. Use the following command to create them:: user# sudo su - postgres ## Or whatever user PostgreSQL is running as postgres# cd wherever_you_unpacked_the_chula_tarball postgres# ./sql/session/reload The above command will create a user named ``chula`` and a database named ``chula_session``. Next you'll need to make sure your server is configured to support requests over TCP/IP. Usually this is done by setting ``listen_addresses = 'localhost'`` or another hostname in ``postgresql.conf``. Assuming you're connecting to the server running locally, you're all done! If you're connecting to a remote server, you need to add the hostname in your configuration via :attr:`config.Config.session_host`. If you want to use a different user/password/port/server you are free to do so. CouchDB ------- You wil need to configure :attr:`config.Config.session_nosql` with the full HTTP path to your CouchDB server. If this is a local install, you'd set the value to http://localhost:5984. Don't worry about the database, as it wil be created automatically on demand. Memcached --------- You need to configure your cluster information via :attr:`config.Config.session_memcache`. If you're using a local install of Memcached you can just take the defaults, else configure it with something like this:: [('host1:11211', 1), ('host2:11211', 1), ('host3:11211', 1)] Reference +++++++++ For more detail on Chula configuration in general, see :mod:`config`. .. _pgpool: http://pgpool.projects.postgresql.org/ .. _pickle: http://docs.python.org/library/pickle.html Chula-0.7.0/docs/template.txt0000644000175000017500000000065211370740256017054 0ustar jmcfarlanejmcfarlane======================= This is the page header ======================= Main text under the header This is the first main header +++++++++++++++++++++++++++++ Text under the first section The second header ----------------- Text under the second section The third header ~~~~~~~~~~~~~~~~ Text under the third header Here's a list, that *includes* a link: 1. `Getting Started`_ is a linked page 2. A another element Chula-0.7.0/LICENSE0000644000175000017500000004313111370740256014554 0ustar jmcfarlanejmcfarlane GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. Chula-0.7.0/chula/0000755000175000017500000000000011412546122014632 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/db/0000755000175000017500000000000011412546122015217 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/db/functions.py0000644000175000017500000001341211370740256017611 0ustar jmcfarlanejmcfarlane"""Functions to make working with databases easier""" import re from chula import data, error def cbool(input_): """ Returns a formatted string safe for use in SQL. If None is passed, it will return 'NULL' so as to insert a NULL value into the database. @param input_: String to be cleaned @type input_: String @return: String I{TRUE/FALSE}, or 'NULL' >>> print 'SET active = %s;' % cbool(True) SET active = TRUE; """ if input_ in [None, '']: return 'NULL' input_ = str(input_).lower() if input_ in data.TRUE: return 'TRUE' elif input_ in data.FALSE: return 'FALSE' else: raise error.TypeConversionError(input_, 'sql boolean') def cdate(input_, doquote=True, isfunction=False): """ Returns a formatted string safe for use in SQL. If None or an empty string is passed, it will return 'NULL' so as to insert a NULL value into the database. B{Todo:} I{This function needs to be able to receive datetime.datetime types too.} @param input_: Date to be cleaned @type input_: String @return: String, or 'NULL' >>> print 'SET updated = %s;' % cdate('1/1/2005') SET updated = '1/1/2005'; >>> print 'SET updated = %s;' % cdate('now()', isfunction=True) SET updated = now(); """ if input_ in [None, '', 'NULL']: return 'NULL' elif isfunction: return input_ else: input_ = str(input_) if data.isdate(input_): if doquote: input_ = data.wrap(input_, "'") else: raise error.TypeConversionError(input_, 'sql date') return input_ def cfloat(input_): """ Returns a formatted string safe for use in SQL. If None is passed, it will return 'NULL' so as to insert a NULL value into the database. @param input_: Float to be cleaned @type input_: Anything @return: Float, or 'NULL' >>> print 'WHERE field = %s;' % cfloat("45") WHERE field = 45.0; """ # Check if the data passed is a NULL value if input_ is None or str(input_).lower() == 'null' or input_ == '': return 'NULL' elif isinstance(input_, float): return input_ try: return float(input_) except: raise error.TypeConversionError(input_, 'sql float') def cint(input_): """ Returns a formatted string safe for use in SQL. If None is passed, it will return 'NULL' so as to insert a NULL value into the database. @param input_: Integer to be cleaned @type input_: Anything @return: Integer, or 'NULL' >>> print 'WHERE field = %s;' % cint("45") WHERE field = 45; """ # Check if the data passed is a NULL value if input_ is None or str(input_).lower() == 'null' or input_ == '': return 'NULL' elif isinstance(input_, int): return input_ try: return int(input_) except: raise error.TypeConversionError(input_, 'sql float') def cregex(input_, doquote=True): """ Returns a regular expression safe for use in SQL. If None is passed if will raise an exception as None is not a valid regular expression. The intented use is with regex based SQL expressions. @param input_: Value to evaluate @type input_: str @param doquote: I{OPTIONAL}: Wrapped in single quotes, defaults to B{True} @type doquote: bool @return: str """ if data.isregex(input_): if doquote: return data.wrap(input_, "'") else: return input_ else: raise error.TypeConversionError(input_, 'sql regex') def cstr(input_, doquote=True, doescape=True): """ Returns a formatted string safe for use in SQL. If None is passed, it will return 'NULL' so as to insert a NULL value into the database. Single quotes will be escaped. @param input_: String to be cleaned @type input_: String @param doquote: I{OPTIONAL}: Wrapped in single quotes, defaults to B{True} @type doquote: bool @param doescape: I{OPTIONAL}: Escape single quotes, defaults to B{True} @type doescape: bool @return: String, or 'NULL' >>> print 'SET description = %s;' % cstr("I don't") SET description = 'I don''t'; >>> print 'SET now = %s;' % cstr("CURRENT_TIME", doquote=False) SET now = CURRENT_TIME; """ if input_ is None: return 'NULL' input_ = str(input_) if doescape: escape = {"'":"''", "\\":"\\\\"} input_ = data.replace_all(escape, input_) if doquote: return data.wrap(input_, "'") else: return input_ def ctags(input_): """ Returns a string safe for use in a sql statement @param: input_ @type input_: Anything @return: 'NULL', or input_ string >>> print ctags('') NULL >>> print ctags('linux git foo') 'foo git linux' """ if input_ in [None, '']: return 'NULL' if isinstance(input_, list): input_ = ' '.join(input_) tags = data.tags2str(data.str2tags(input_)) return "'%s'" % tags.lower() def empty2null(input_): """ Returns NULL if an empty string or None is passed, else returns the input_ string. @param: input_ @type input_: Anything @return: 'NULL', or input_ string >>> print empty2null('') NULL """ if input_ in [None, '']: return 'NULL' else: return input_ def unquote(input_): """ Return string not padded with single quotes. This is useful to clean something changed by cstr() @param: input_ @type input_: str @return: str, or input unchanged """ if isinstance(input_, str): if input_.startswith("'") and input_.endswith("'"): input_ = input_[1:-1] return input_ Chula-0.7.0/chula/db/engines/0000755000175000017500000000000011412546122016647 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/db/engines/sqlite.py0000644000175000017500000000350411370740256020533 0ustar jmcfarlanejmcfarlane"""Chula Sqlite datastore object""" try: import sqlite3 except: raise error.MissingDependencyError('sqlite3') from chula import error from chula.db.engines import engine ISOLATION_LEVELS = ['DEFERRED', 'EXCLUSIVE', 'IMMEDIATE', None] class DataStore(engine.Engine): """ Sqlite engine class """ def __init__(self, uri, *args, **kwargs): super(DataStore, self).__init__() # Handle in memory databases if uri == 'memory': uri = ':memory:' # Handle the initial isolation level if 'isolation' in kwargs: isolation = kwargs['isolation'] else: isolation = None # Handle the initial timeout level if 'timeout' in kwargs: timeout = kwargs['timeout'] else: timeout = 5 # Create a database connection self.conn = sqlite3.connect(uri, isolation_level=isolation, timeout=timeout) def cursor(self, type='dict'): if type == 'dict': self.conn.row_factory = sqlite3.Row return super(DataStore, self).cursor() def interrupt(self): self.conn.interupt() def set_isolation(self, level=None): """ Toggle the isolation level. Here are the available isolation levels: - DEFFERED = ? - EXCLUSIVE = Prevents anyone else from reading/writing - IMMEDIATE = ? - None = Autocommit mode (the default) @param level: Isolation level @type level: str """ if level in ISOLATION_LEVELS: self.conn.isolation_level = level else: raise error.InvalidAttributeError(level) Chula-0.7.0/chula/db/engines/couch.py0000755000175000017500000000176611370740256020346 0ustar jmcfarlanejmcfarlanefrom couchdb import Server, ResourceNotFound, PreconditionFailed from chula.db.engines import engine class DataStore(engine.Engine): """ CouchDB engine class """ def __init__(self, uri, *args, **kwargs): super(DataStore, self).__init__() self.conn = Server(uri) def delete(self, db): try: del self.conn[db] except ResourceNotFound, ex: pass def db(self, db): try: return self.conn[db] except ResourceNotFound, ex: try: return self.conn.create(db) except PreconditionFailed, ex: # Work around the situation where a client/server # mismatch results in a PreconditionFailed error being # raised, even when it was actually created # successfully. Remove this maybe in time. if db in self.conn: return self.conn[db] else: raise Chula-0.7.0/chula/db/engines/engine.py0000644000175000017500000000150411370740256020475 0ustar jmcfarlanejmcfarlane""" Chula database engine module """ from chula import collection class Engine(object): def __init__(self): self.conn = None self.error = collection.Collection() def set_isolation(self, level=1): """ Set the database connection isolation level """ pass def close(self): """ Destroy a database connection object """ self.conn.close() def commit(self): """ Perform database commit """ self.conn.commit() def rollback(self): """ Perform query rollback """ self.conn.rollback() def cursor(self): """ Create database cursor @return: Instance """ return self.conn.cursor() Chula-0.7.0/chula/db/engines/__init__.py0000644000175000017500000000003411370740256020764 0ustar jmcfarlanejmcfarlane"""Chula database stores""" Chula-0.7.0/chula/db/engines/postgresql.py0000644000175000017500000000456011370740256021440 0ustar jmcfarlanejmcfarlane"""Chula Postgresql datastore object""" import re from chula import error try: import psycopg2 from psycopg2 import extensions, extras except: raise error.MissingDependencyError('Psycopg2') from chula.db.engines import engine class DataStore(engine.Engine): """ Postgresql engine class using the Psycopg2 driver """ def __init__(self, uri, passwd='', *args, **kwargs): super(DataStore, self).__init__() m = re.match(r'(?P[-a-zA-Z0-9]+)@' r'(?P[-a-zA-Z0-9]+)/' r'(?P[-a-zA-Z0-9_]+)$', uri) if m is None: raise error.MalformedConnectionStringError(engine) parts = m.groupdict() parts['pass'] = passwd conn = 'host=%(host)s dbname=%(db)s user=%(user)s password=%(pass)s' self.conn = psycopg2.connect(conn % parts) # Expose the psycopg2 exceptions self.error.DataError = psycopg2.DataError self.error.DatabasError = psycopg2.DatabaseError self.error.Error = psycopg2.Error self.error.IntegrityError = psycopg2.IntegrityError self.error.InterfaceError = psycopg2.InterfaceError self.error.InternalError = psycopg2.InternalError self.error.NotSupportedError = psycopg2.NotSupportedError self.error.OperationalError = psycopg2.OperationalError self.error.ProgrammingError = psycopg2.ProgrammingError def set_isolation(self, level=1): """ Toggle the isolation level. Here are the available isolation levels: - 0 = No isolation - 1 = READ COMMITTED (the default) - 3 = SERIALIZABLE @param level: Isolation level @type level: Integer """ self.conn.set_isolation_level(level) def cursor(self, type='dict'): """ Create database cursor @param type: Type of cursor to return @type type: string, I{dict} or I{tuple} @return: Instance """ if type == 'tuple': return self.conn.cursor(cursor_factory=psycopg2.extensions.cursor) elif type == 'dict': return self.conn.cursor(cursor_factory=psycopg2.extras.DictCursor) else: msg = 'Invalid cursor type, use either tuple or dict' raise error.UnsupportedUsageError(append=msg) Chula-0.7.0/chula/db/datastore.py0000644000175000017500000000453011370740256017570 0ustar jmcfarlanejmcfarlane""" Chula datastore object, provides consistent access to all supported database engines """ import re # Loading ourselves before third party is bad, but we need to be able # to raise our own exception if things go wrong from chula import error, data class DataStoreFactory(object): """ The Database class creates an instance of a supported database engine (aka datastore) """ def __new__(cls, conn, *args, **kwargs): """ Creates an instance of a DataStore class. The connection string is a tuple with the following values in order: 1. Database type B{(Currently the only supported option is: pg)} 2. Username 3. Host 4. Database name eg: pg:username@server/databasename @param conn: Connection string @type conn: String @param passwd: Database password @type passwd: String @return: Instance >>> conn = DataStoreFactory('pg:chula@localhost/chula_test', 'passwd') >>> cursor = conn.cursor() >>> cursor.execute('SELECT * FROM cars LIMIT 1;') >>> data = cursor.fetchone() >>> print data [1, 'Honda', 'Civic'] >>> print data['make'] Honda >>> print dict(data) {'model': 'Civic', 'make': 'Honda', 'uid': 1} >>> >>> cursor = conn.cursor(type='tuple') >>> cursor.execute('SELECT * FROM cars LIMIT 1;') >>> print cursor.fetchone() (1, 'Honda', 'Civic') >>> >>> conn.close() """ try: parts = conn.split(':') if len(parts) > 1: engine = parts[0] uri = ':'.join(parts[1:]) else: raise error.MalformedConnectionStringError except AttributeError: raise error.MalformedConnectionStringError(conn) if engine == 'pg': from chula.db.engines import postgresql as engine elif engine == 'couchdb': from chula.db.engines import couch as engine elif engine == 'sqlite': from chula.db.engines import sqlite as engine else: raise error.UnsupportedDatabaseEngineError(engine) # Return a database engine instance return engine.DataStore(uri, *args, **kwargs) Chula-0.7.0/chula/db/__init__.py0000644000175000017500000000027111370740256017337 0ustar jmcfarlanejmcfarlane"""Chula database package, a simple db abstraction layer""" from functions import * # For now support legacy style acess (for now) from datastore import DataStoreFactory as DataStore Chula-0.7.0/chula/www/0000755000175000017500000000000011412546122015456 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/cookie.py0000644000175000017500000000456411370740256017321 0ustar jmcfarlanejmcfarlane""" Cookie reads and writes cookies """ from Cookie import SimpleCookie from datetime import datetime from chula import data class CookieCollection(SimpleCookie): def __init__(self, config=None, timeout=20, path='/', input=None): """ Create a collection of cookies @param timeout: How long the cookie should live @type timeout: int (Unit of measure: minutes) """ super(CookieCollection, self).__init__(input) self.timeout = timeout self.path = path self.domain = None def headers(self): timeout = self.timeout * 60 now = datetime.utcnow() expires = data.date_add('s', timeout, now) expires = expires.strftime('%a, %d-%b-%Y %H:%M:%S %Z') cookies = [] for key, cookie in self.iteritems(): # Don't write out cookies that are prefixed with underbars # as they are considered private. This also has the side # effect of not writing out Urchin cookies like crazy :) if key.startswith('_'): # TODO: find a better way to avoid writing Urchin cookies continue if not self.domain is None: # Domain must be prefixed with "." and exclude the # port. If the domain already has a "." prefix we've # already seen this domain name and can skip. # REFERENCE: RFC 2109, RFC 2965 if not self.domain.startswith('.'): self.domain = '.' + self.domain.split(':')[0] # If the domain doesn't look to be a real FQDN, remove: if not self.domain.count('.') > 1: self.domain = None # Always include the name of the cookie first header = [] header.append('%s=%s' % (key, cookie.value)) # Supported cookie attributes header.append('Expires=%s' % expires) header.append('Path=%s' % self.path) # Don't include the domain for sites that only use hostnames # eg: http://wiki/foo/bar/file.html if not self.domain is None: header.append('Domain=%s' % self.domain) # Create the full header tuple cookies.append(('Set-Cookie', '; '.join(header))) return cookies def destroy(self): self.timeout = -10000 Chula-0.7.0/chula/www/adapters/0000755000175000017500000000000011412546122017261 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/adapters/fcgi/0000755000175000017500000000000011412546122020171 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/adapters/fcgi/env.py0000644000175000017500000000104211370740256021337 0ustar jmcfarlanejmcfarlane""" Manage the environment when python is using fcgi """ import os import socket from chula.www.adapters.wsgi import env FCGI = 'FCGI/WSGI' PATH = os.environ.get('PATH') class Environment(env.Environment): def __init__(self, environ): super(Environment, self).__init__(environ) # Indicate what type of adapter this is self.chula_adapter = FCGI # Set the remote_host from the remote_addr self.REMOTE_HOST = socket.getfqdn(self.REMOTE_ADDR) # Set the path self.PATH = PATH Chula-0.7.0/chula/www/adapters/fcgi/adapter.py0000644000175000017500000000151611370740256022175 0ustar jmcfarlanejmcfarlane""" Chula fastcgi adapter """ from chula.www.adapters import base from chula.www.adapters.fcgi import env def configured_app(environ, start_response, config): adapter = base.BaseAdapter(config) adapter.set_environment(env.Environment(environ)) # Execute the controller and store it's output chunks = [c for c in adapter.execute()] # Add the content type to the headers adapter.add_header(('Content-Type', adapter.env.content_type)) # Execute the wsgi callback start_response('%s OK' % adapter.env.status, adapter.env.headers) # Yield the data to the client for chunk in chunks: yield chunk # Clean house adapter._gc() def fcgi(fcn): def wrapper(environ, start_response): config = fcn() return configured_app(environ, start_response, config) return wrapper Chula-0.7.0/chula/www/adapters/fcgi/__init__.py0000644000175000017500000000000011370740256022277 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/adapters/wsgi/0000755000175000017500000000000011412546122020232 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/adapters/wsgi/env.py0000644000175000017500000000505711370740256021412 0ustar jmcfarlanejmcfarlane""" Manage the environment when python is using wsgi """ from cgi import FieldStorage import os from chula.www.adapters import env WSGI = 'WSGI' class Environment(env.BaseEnv): def __init__(self, environ): super(Environment, self).__init__() # Indicate what type of adapter this is self.chula_adapter = WSGI # Set the required variables from the wsgi environ object self.fill(environ) # Check for redirects and recover the querystring if 'REDIRECT_QUERY_STRING' in environ: self.QUERY_STRING = environ.get('REDIRECT_QUERY_STRING') # Make sure REQUEST_URI is set if not 'REQUEST_URI' in environ: parts = [] parts.append(environ.get('PATH_INFO', '')) self.REQUEST_URI = ''.join(parts) # Include the querystring if not '?' in self.REQUEST_URI: qs = environ.get('QUERY_STRING', None) if not qs is None and qs != '': self.REQUEST_URI += '?' + qs # Make sure PATH is set self.PATH = os.environ.get('PATH', None) # Be nice to the Python wsiref simple_server if self.SERVER_SOFTWARE.startswith('WSGIServer/0.1 Python'): self.DOCUMENT_ROOT = None self.REMOTE_PORT = None self.SCRIPT_FILENAME = None self.SERVER_ADDR = None self.SERVER_ADMIN = None self.SERVER_SIGNATURE = None # Make sure SCRIPT_NAME is set if not self.SCRIPT_NAME: self.SCRIPT_NAME = self.PATH_INFO # Set http get or post variables self.form = FieldStorage(fp=self.wsgi_input, environ=environ, keep_blank_values=1) # Add additional variables provided by the base class super(Environment, self).extras() def __deepcopy__(self, memo={}): """ Currently not all wsgi.foo objects are easy to deepcopy. This method overloads BaseEnv to return a fresh object with wsgi input/error objects being "by reference" copies. """ # Make a copy of the existing objects wsgi_errors = self.wsgi_errors wsgi_input = self.wsgi_input # Remove them from the collection self.remove('wsgi_errors') self.remove('wsgi_input') # Put them back (by reference) fresh = super(Environment, self).__deepcopy__(memo) fresh.wsgi_input = wsgi_input fresh.wsgi_errors = wsgi_errors return fresh Chula-0.7.0/chula/www/adapters/wsgi/adapter.py0000644000175000017500000000151311370740256022233 0ustar jmcfarlanejmcfarlane""" Chula wsgi adapter """ from chula.www.adapters import base from chula.www.adapters.wsgi import env def configured_app(environ, start_response, config): adapter = base.BaseAdapter(config) adapter.set_environment(env.Environment(environ)) # Execute the controller and store it's output chunks = [c for c in adapter.execute()] # Add the content type to the headers adapter.add_header(('Content-Type', adapter.env.content_type)) # Execute the wsgi callback start_response('%s OK' % adapter.env.status, adapter.env.headers) # Yield the data to the client for chunk in chunks: yield chunk # Clean house adapter._gc() def wsgi(fcn): def wrapper(environ, start_response): config = fcn() return configured_app(environ, start_response, config) return wrapper Chula-0.7.0/chula/www/adapters/wsgi/__init__.py0000644000175000017500000000000011370740256022340 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/adapters/mod_python/0000755000175000017500000000000011412546122021441 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/adapters/mod_python/fakerequest.py0000644000175000017500000000565611370740256024355 0ustar jmcfarlanejmcfarlane""" Classes to aid in unit testing or anything that needs to simulate a valid Apache/Mod_python request object. """ import os import sys class FakeFieldStorage(dict): """ Fake FieldStorage object. """ def __init__(self, *args, **kwargs): """ Simulates the mod_python FieldStorage object, really just to provide the list object. @return: Pseudo mod_python FieldStorage object """ super(FakeFieldStorage, self).__init__() self.list = self class FakeRequest(dict): """ Fake request object """ def __init__(self): """ Simulates the req object common to mod_python development @return: Pseudo mod_python req object """ environ = os.environ self.subprocess_env = environ self.method = 'GET' self.proto_num = 9999 self.protocol = 'HTTP/1.1' self.content_type = "text/plain" self.status = 200 self.args = environ.get('QUERY_STRING', '') self.filename = 'index.py' self.hostname = 'localhost' self.path_info = environ.get('PATH_INFO', '') self.REQUEST_URI = '/' self.uri = '/' self.the_request = 'GET / HTTP/1.1' self.connection = FakeRequestConnection() self.content_length = -1 self._headed = 0 self.headers_out = self self.headers = "" self.read = sys.stdin.read self.server = FakeServer() self.headers_in = {'Referer':environ.get('HTTP_REFERER', ''), 'Cookie':environ.get('HTTP_COOKIE', ''), 'User-Agent':environ.get('USER_AGENT', '') } self.get = FakeFieldStorage() self.form = FakeFieldStorage() def _write_headers(self): self._headed = 1 sys.stdout.write('status: %s\n' % self.status) sys.stdout.write('Content-Type: %s\n' % self.content_type) if self.content_length >= 0: sys.stdout.write('Content-Length: %s\n' % self.content_length) sys.stdout.write(self.headers) sys.stdout.write('\n') self.write = sys.stdout.write def add(self, key, value): self.headers += '%s: %s\n' % (key, value) def document_root(self): return '' def get_remote_host(self): return None def set_content_length(self, len): self.content_length = len def write(self, s): if not self._headed: self._write_headers() sys.stdout.write(s) class FakeRequestConnection(object): def __init__(self): self.local_addr = ['', ''] self.local_host = ('127.0.0.1', 80) self.remote_addr = ('127.0.0.1', 9999) self.remote_host = 'localhost' self.remote_ip = '127.0.0.1' class FakeServer(object): pass # Expose drop in replacements for the real thing FieldStorage = FakeFieldStorage Chula-0.7.0/chula/www/adapters/mod_python/env.py0000644000175000017500000000247011370740256022615 0ustar jmcfarlanejmcfarlane""" Manage the environment when python is using mod_python """ from mod_python import util from chula.www.adapters import env MOD_PYTHON = 'MOD_PYTHON' class Environment(env.BaseEnv): def __init__(self, req): super(Environment, self).__init__() # Indicate what type of adapter this is self.chula_adapter = MOD_PYTHON # Fetch additional mod_python environment variables req.add_common_vars() subprocess = req.subprocess_env.copy() # Set the required variables from mod_python's req object(s) self.fill(req.subprocess_env.copy()) # Add environment variables not available in subprocess_env self.PATH_INFO = req.path_info # Check for redirects and recover the querystring if 'REDIRECT_QUERY_STRING' in subprocess: self.QUERY_STRING = subprocess.get('REDIRECT_QUERY_STRING') # If req.form exists and is of type util.FieldStorage use it, # else use what mod_python publisher would use. try: if isinstance(self.req.form, util.FieldStorage): self.form = self.req.form except: self.form = util.FieldStorage(req, keep_blank_values=1) # Add additional variables provided by the base class super(Environment, self).extras() Chula-0.7.0/chula/www/adapters/mod_python/adapter.py0000644000175000017500000000177711370740256023456 0ustar jmcfarlanejmcfarlane""" Chula mod_python adapter """ from mod_python import apache as APACHE from chula.www.adapters import base from chula.www.adapters.mod_python import env def configured_handler(req, config): adapter = base.BaseAdapter(config) adapter.set_environment(env.Environment(req)) # Execute the controller and store it's output chunks = [c for c in adapter.execute()] # Add the headers to the mod_python req object for header in adapter.env.headers: req.headers_out.add(header[0], header[1]) # Set the content_type and status req.content_type = adapter.env.content_type req.status = adapter.env.status # Write the data to the client try: for chunk in chunks: req.write(chunk) except IOError, ex: if config.debug: raise # All is well try: return APACHE.OK except: pass def handler(fcn): def wrapper(req): config = fcn() return configured_handler(req, config) return wrapper Chula-0.7.0/chula/www/adapters/mod_python/__init__.py0000644000175000017500000000000011370740256023547 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/adapters/env.py0000644000175000017500000001713011370740256020434 0ustar jmcfarlanejmcfarlane""" Chula adapter environment class """ from copy import deepcopy import cgi import re from chula import error, collection from chula.www import http class BaseEnv(collection.RestrictedCollection): """ Provide a consistent interface all adapters must conform to """ @staticmethod def __validkeys__(): """ The minimum environment must at least adhere to the wsgi spec """ return ('DOCUMENT_ROOT', 'GATEWAY_INTERFACE', 'HTTP_ACCEPT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_ENCODING', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_CONNECTION', 'HTTP_COOKIE', 'HTTP_HOST', 'HTTP_KEEP_ALIVE', 'HTTP_USER_AGENT', 'PATH', 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REMOTE_HOST', 'REMOTE_PORT', 'REQUEST_METHOD', 'REQUEST_URI', 'SCRIPT_FILENAME', 'SCRIPT_NAME', 'SERVER_ADDR', 'SERVER_ADMIN', 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', 'SERVER_SIGNATURE', 'SERVER_SOFTWARE', 'chula_adapter', 'chula_class', 'chula_method', 'chula_module', 'chula_package', 'chula_version', 'wsgi_errors', 'wsgi_file_wrapper', 'wsgi_input', 'wsgi_multiprocess', 'wsgi_multithread', 'wsgi_run_once', 'wsgi_url_scheme', 'wsgi_version', 'ajax_uri', 'content_type', 'cookies', 'debug', 'form', 'form_get', 'form_post', 'headers', 'route', 'status', 'under_construction' ) def __defaults__(self): self.DOCUMENT_ROOT = collection.UNSET self.GATEWAY_INTERFACE = collection.UNSET self.HTTP_ACCEPT = None self.HTTP_ACCEPT_CHARSET = None self.HTTP_ACCEPT_ENCODING = None self.HTTP_ACCEPT_LANGUAGE = None self.HTTP_CONNECTION = None self.HTTP_COOKIE = None self.HTTP_HOST = None self.HTTP_KEEP_ALIVE = None self.HTTP_USER_AGENT = None self.PATH = collection.UNSET self.PATH_INFO = '' self.QUERY_STRING = '' self.REMOTE_ADDR = collection.UNSET self.REMOTE_HOST = None self.REMOTE_PORT = collection.UNSET self.REQUEST_METHOD = collection.UNSET self.REQUEST_URI = collection.UNSET self.SCRIPT_FILENAME = collection.UNSET self.SCRIPT_NAME = '' self.SERVER_ADDR = collection.UNSET self.SERVER_ADMIN = collection.UNSET self.SERVER_NAME = collection.UNSET self.SERVER_PORT = collection.UNSET self.SERVER_PROTOCOL = collection.UNSET self.SERVER_SIGNATURE = collection.UNSET self.SERVER_SOFTWARE = collection.UNSET self.chula_adapter = collection.UNSET self.chula_class = collection.UNSET self.chula_method = collection.UNSET self.chula_module = collection.UNSET self.chula_package = collection.UNSET self.chula_version = collection.UNSET self.route = collection.UNSET self.wsgi_errors = None self.wsgi_file_wrapper = None self.wsgi_input = None self.wsgi_multiprocess = None self.wsgi_multithread = None self.wsgi_run_once = None self.wsgi_url_scheme = None self.wsgi_version = None self.ajax_uri = collection.UNSET self.content_type = 'text/plain' self.cookies = None self.debug = True self.headers = [] self.form = collection.UNSET self.form_get = collection.UNSET self.form_post = collection.UNSET self.status = http.HTTP_OK self.under_construction = False def __deepcopy__(self, memo={}): """ Return a copy of a BaseEnv object """ return self.copy_into(BaseEnv()) def _ajax_uri(self): protocol_type = re.match(r'(HTTPS?)', self.SERVER_PROTOCOL) if not protocol_type is None: protocol_type = protocol_type.group() else: msg = 'Unsupported protocol: %s' % self.SERVER_PROTOCOL raise ValueError(msg) # Prefer HTTP_HOST over SERVER_NAME per PEP333 domain = self.HTTP_HOST if domain is None: domain = self.SERVER_NAME return protocol_type.lower() + '://' + domain def _cookie(self): """ Make sure HTTP_COOKIE exists even if empty """ return self.get('HTTP_COOKIE', {}) def _downcast_cgi_vars(self): """ When mod_python.util.FieldStorage or cgi.FieldStorage encounter array types (think html checkboxes) it winds up being a Field() or MiniFieldStorage() object for mod_python or cgi respectively. Both are intended to be accessed via a "value" attribute. This method casts these objects so the actual value is held and thus can be referenced directly. In the event the object doesn't have a "value" attribute it's left alone (not sure how this can happen, but it does). """ for key in self.form.keys(): if isinstance(self.form[key], list): for i in xrange(len(self.form[key])): if not getattr(self.form[key][i], 'value', None) is None: self.form[key][i] = self.form[key][i].value else: # Let me know if you can make this get called :) pass def _clean_http_vars(self): passed = deepcopy(dict(self.form)) # Create object to hold only HTTP GET variables self.form_get = cgi.parse_qs(self.QUERY_STRING, keep_blank_values=1) for key in self.form_get.keys(): if len(self.form_get[key]) == 1: self.form_get[key] = self.form_get[key][0] else: self.form_get[key] = self.form_get[key] # Create an object to hold only HTTP POST variables self.form_post = {} for key in passed.keys(): if not key in self.form_get: if isinstance(passed[key], list): self.form_post[key] = passed[key] else: self.form_post[key] = passed[key].value # Make sure the form object contains both while taking # precedence over POST when overlap exists self.form = deepcopy(self.form_get) self.form.update(self.form_post) def fill(self, env): """ Populate the collection with values. Some keys contain "." characters which would break attribute access on this collection. For this reason dots will be replaced with underbars. """ for key, value in env.iteritems(): key = key.replace('.', '_') if key in self: self[key] = value def extras(self): """ Set extra environment variables, all being Chula specific """ self.HTTP_COOKIE = self._cookie() self.ajax_uri = self._ajax_uri() # Make sure get/post variables are handled correctly self._clean_http_vars() self._downcast_cgi_vars() Chula-0.7.0/chula/www/adapters/base.py0000644000175000017500000001315311370740256020557 0ustar jmcfarlanejmcfarlane""" Chula base adapter for all supported web adapters """ from copy import deepcopy import re import time import chula from chula import collection, error, guid, logger from chula.www import cookie from chula.www.mapper import ClassPathMapper from chula.www.mapper import RegexMapper RE_HTML = re.compile(r"\s*\s*$", re.IGNORECASE) class BaseAdapter(object): def __init__(self, config): self.config = config self.timer_start() self.controller = None self.log = logger.Logger(config).logger('chula.www.adapters.base') self.mapper = None def _gc(self): del self.config del self.controller del self.env del self.mapper del self.timer def exception(self, controller, ex): # Prepare a collection to hold exception context context = collection.Collection() context.exception = ex context.env = deepcopy(controller.env) context.form = deepcopy(controller.form) return context def execute(self): self.controller = self.fetch_controller() # Call the controller method and if an exception is raised, # use the configured e500 controller. If that breaks, it's up # to your web server custom 500 handler to handle things. try: html = self.controller.execute() except Exception, ex: # Save off the exception before re-mapping the controller exception = self.exception(self.controller, ex) # Try the error controller (and set an exception # attribute in the model) self.controller = self.mapper.map(500) self.controller.model.exception = exception # Don't yield yet, need to replace to add chula stuff html = self.controller.execute() # Write the returned html to the request object. # We're manually casting the view as a string because Cheetah # templates seem to require it. If you're not using Cheetah, # sorry... str() is cheap :) if not html is None: html = str(html) written = False # Add info about server info and processing time if self.config.add_timer: timer = """
%s
%s
%f ms
""" % (self.controller.env.chula_adapter, self.controller.env.chula_version, (time.time() - self.timer) * 1000) yield RE_HTML.sub(timer, html) written = True if not written: yield html else: raise error.ControllerMethodReturnError() # Add the content type to the environment self.env.content_type = self.controller.content_type # Add the cookies to the headers self.env.headers.extend(self.env.cookies.headers()) for c in self.env.cookies.headers(): self.log.debug('Added cookie: %s, %s' % c) # If this is an under construction page do not try to persist # session, avoid as many dependencies as possible if self.env.under_construction: self.env.status = 503 elif self.config.session: # Persist session and perform garbage collection try: self.controller._pre_session_persist() self.controller.session.persist() except error.SessionUnableToPersistError: # Need to assign an error controller method to call here raise except Exception: # This is a downstream exception, let them handle it raise finally: self.controller._gc() def timer_start(self): if self.config.add_timer: self.timer = time.time() else: self.timer = None def set_environment(self, env): self.env = env self.env.headers = [] # Initialize cookies self.env.cookies = cookie.CookieCollection(config=self.config) self.env.cookies.domain = self.env.HTTP_HOST self.env.cookies.key = self.config.session_encryption_key self.env.cookies.timeout = self.config.session_timeout # Make sure the domain gets set if self.env.cookies.domain is None: self.env.cookies.domain = self.env.SERVER_NAME # Load exising cookies sent from the client (browser) if not self.env.HTTP_COOKIE is None: self.env.cookies.load(self.env.HTTP_COOKIE) def add_header(self, header): self.env.headers.append(header) def fetch_controller(self): mapper = self.config.mapper if mapper == 'ClassPathMapper': self.mapper = ClassPathMapper(self.config, self.env) elif isinstance(mapper, (tuple, frozenset, list, set)): self.mapper = RegexMapper(self.config, self.env, mapper) else: raise error.UnsupportedMapperError(mapper) # Load the controller, using e404 if not found try: controller = self.mapper.map() except error.ControllerImportError, ex: controller = self.mapper.map(500) controller.model.exception = self.exception(controller, ex) except error.ControllerClassNotFoundError: controller = self.mapper.map(404) controller.env.route = self.mapper.route return controller Chula-0.7.0/chula/www/adapters/__init__.py0000644000175000017500000000000011370740256021367 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/mapper/0000755000175000017500000000000011412546122016742 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/www/mapper/classpath.py0000644000175000017500000000247711370740256021317 0ustar jmcfarlanejmcfarlane""" Python classpath based Chula URL mapper """ from chula.www.mapper import base class ClassPathMapper(base.BaseMapper): def parse(self): # Parse the uri (excluding the querystring) parts = self.uri.split('?')[0].split('/') # Remove any empty segments while '' in parts: parts.remove('') count = len(parts) # Update package, module, method if count == 0: # Homepage pass elif count == 1: # Root controller using default method self.route.method = base.DEFAULT_METHOD self.route.module = parts.pop() elif count == 2: # Root controller using specified method self.route.method = parts.pop() self.route.module = parts.pop() self.route.package += '.'.join(parts) elif count > 2: # Package controller using specified method self.route.method = parts.pop() self.route.module = parts.pop() self.route.package += '.' + '.'.join(parts) else: # This can't happen? raise ValueError('Bad route: %s' % self.route) # The class name is always the capitalized module name self.route.class_name = self.route.module.capitalize() return str(self) Chula-0.7.0/chula/www/mapper/base.py0000644000175000017500000001513111370740256020236 0ustar jmcfarlanejmcfarlane""" Base class to convert HTTP url path into a Python object path. This class can be subclassed to customize the url mapping behavior. """ import os from chula import logger from chula.www.mapper import * class BaseMapper(object): def __init__(self, config, env): # Check to make sure the config is available if config.classpath is None: msg = ('[cfg.classpath] must be specified in your configuration.' ' See documentation for help on how to set this.') raise error.UnsupportedConfigError(msg) self.config = config self.env = env self.log = logger.Logger(config).logger('chula.www.mapper.base') self.uri = env.REQUEST_URI # Set the under construction controller construction_route = {'module':self.config.construction_controller, 'method':'index'} self.construction = collection.Collection() self.construction.trigger = self.config.construction_trigger self.construction.route = construction_route # Set the default route values self.route = collection.Collection() self.route.package = self.config.classpath self.route.module = DEFAULT_MODULE self.route.method = DEFAULT_METHOD # Set the default [404] route values self.route_404 = copy.copy(self.route) self.route_404.module = self.config.error_controller self.route_404.method = 'e404' def __str__(self): namespace = '%(package)s.%(module)s.%(class_name)s.%(method)s' try: return namespace % self.route except: return str(self.route) def default_route(self): """ Create the default route which will [later] map to a Python object. The default route is either the homepage, or a 404 page. """ if self.uri != '/' and not self.uri.startswith('/?'): self.route = copy.copy(self.route_404) def parse(self): """ Determine the right Python class and method to use. The idea here is to let subclasses determine this logic. """ raise NotImplementedError('The "parse" method must be overloaded') def import_module(self): path = '%s.%s' % (self.route.package, self.route.module) class_name = self.route.module.capitalize() self.route.class_name = class_name msg = "%s - %s [Route being used: %s]" try: return __import__(path, globals(), locals(), [class_name]) except ImportError, ex: msg = msg % (path, ex, self.route) # Distinguish between the following: # 1. controller module not found (ImportError) [HTTP 404] # 2. controller found, but raises an ImportError [HTTP 500] classpath_in_excecption = ex.args[0].split()[-1] if path.endswith(classpath_in_excecption): raise error.ControllerClassNotFoundError(msg) else: raise error.ControllerImportError(msg) except Exception, ex: msg = msg % (path, ex, self.route) raise error.ControllerImportError(msg) def map(self, status=200): """ Return a reference to the controller module? """ if status is 200: self.default_route() # Determine if the site is under construction if not self.construction.trigger is None and \ os.path.exists(self.construction.trigger): self.route.update(self.construction.route) self.env.under_construction = True # TODO: Fix defect that occurs when an exception is # raised when trying to import the under construction # controller, as what happens is the e404 gets used # and it should use e500 else: self.parse() elif status == 404: self.route = self.route_404 elif status == 500: self.route.package = self.config.classpath self.route.module = self.config.error_controller self.route.method = 'e500' # If after the mapper's parse() call the route is still 404 if self.route.method == self.route_404.method \ and self.route.module == self.route_404.module \ and self.route.package == self.route_404.package: status = 404 # Import the controller module module = self.import_module() # Instantiate the controller class from the module controller = getattr(module, self.route.class_name, None) # If no controller found, raise exception if controller is None: msg = '%(package)s.%(module)s.%(class_name)s' % self.route msg += ' [Using Route: %s]' % self.route raise error.ControllerClassNotFoundError(msg) self.controller = controller(self.env, self.config) self.bind() # Set the http status, which can be influenced by mapper.parse() self.controller.env.status = status return self.controller def bind(self): # Make sure we don't try to load a private method if self.route.method.startswith('_'): self.route.method = DEFAULT_METHOD # Lookup the requested method to make sure it exists method = getattr(self.controller, self.route.method, None) # Fallback on the default method if the requested does not exist if not self.config.strict_method_resolution and method is None: method = getattr(self.controller, DEFAULT_METHOD, None) # If we still don't have a method something is very wrong if method is None: # TODO: Log this: #msg = '%(class_name)s.%(method)s()' % self.route #msg += ' => Route: %s' % self.route #msg += ' => Controller: %s' % self.controller #raise error.ControllerMethodNotFoundError(msg) self.route = self.route_404 module = self.import_module() controller = getattr(module, self.route.class_name, None) self.controller = controller(self.env, self.config) method = getattr(self.controller, self.route.method, None) self.controller.execute = method self.update_env() def update_env(self): env = self.controller.env env['chula_class'] = self.route.class_name env['chula_method'] = self.route.method env['chula_module'] = self.route.module env['chula_package'] = self.route.package env['chula_version'] = chula.version return Chula-0.7.0/chula/www/mapper/__init__.py0000644000175000017500000000042611370740256021064 0ustar jmcfarlanejmcfarlane""" Chula URL mapping package """ import copy import chula from chula import collection, error DEFAULT_MODULE = 'home' DEFAULT_METHOD = 'index' # Include supported mappers from chula.www.mapper.classpath import ClassPathMapper from chula.www.mapper.regex import RegexMapper Chula-0.7.0/chula/www/mapper/regex.py0000644000175000017500000000235111370740256020436 0ustar jmcfarlanejmcfarlane""" Python route/route based Chula URL mapper """ import re from chula import logger from chula.www.mapper import base class RegexMapper(base.BaseMapper): def __init__(self, config, env, route_map): super(RegexMapper, self).__init__(config, env) self.route_map = route_map self.log = logger.Logger(config).logger('chula.www.mapper.regex') def _process_route(self, route, force=False): regex, target = route if re.match(regex, self.uri) is None and force is False: return # Pull off the method() and module parts = target.split('.') self.route.method = parts.pop() self.route.module = parts.pop() # Pull off the package (if specified) if parts: self.route.package += '.' + '.'.join(parts) self.log.debug('Set route: %s via %s' % (self.route, regex)) return self.route def parse(self): # Process each route looking for a match for route in self.route_map: if not self._process_route(route) is None: break # The class name is always the capitalized module name self.route.class_name = self.route.module.capitalize() return str(self) Chula-0.7.0/chula/www/controller.py0000644000175000017500000000652211370740256020227 0ustar jmcfarlanejmcfarlane""" Generic base controller used by all web requests """ from chula import collection, guid, error, session from chula.www import http class Controller(object): """ The Controller class helps manage all web requests. This is done by all requests being either of this class or a subclass. """ def __init__(self, env, config): """ Initialize the web request, performing taks common to all web requests. @param env: Normalized environment (wsgi at a minimum) @type env: env WSGI environ object with a few extra objects """ self.content_type = 'text/html' # Add some convenience attributes self.config = config self.env = env self.form = env.form # Create a default model. This object is optionally populated by the # controller, or it can do it's own thing. self.model = collection.Collection() self.model.env = self.env # Start up session using the cookie's guid (or a fake one) if self.config.session: if self.config.session_name in self.env.cookies: guid_ = self.env.cookies[self.config.session_name].value else: # Create a new guid create a cookie guid_ = guid.guid() self.env.cookies[self.config.session_name] = guid_ # Instantiate session and expose to the model self.session = session.Session(self.config, guid_) self.model.session = self.session def _gc(self): """ Complete garbage collection. The intended purpose is to allow consolidated garbage collection specific to each project. This method gets called in the apache handler just before sending data to the browser. """ self.session._gc() del self.config del self.env del self.form del self.model del self.session def _pre_session_persist(self): """ Provide mechanism for removing items from session just prior to being persisted. This is useful when you want to have unserializeable objects that need to be casted to a different type before being persisted to the database. This usually means casting to a JSON encodable type. """ pass def execute(self): """ Provide a consistent method name for execution by the handler. This method is to be rebound by the UrlMapper. """ return "ERROR: execute() has not been properly bound" def redirect(self, destination, type='TEMPORARY'): """ Redirect the browser to another page. @param destination: URL of target destination @type destination: String """ try: self.env.headers.append(('location', str(destination))) except TypeError, ex: msg = 'Invalid redirection content_type: %s, %s' % (destination, ex) raise error.ControllerRedirectionError(msg) if type == 'TEMPORARY': self.env.status = http.HTTP_MOVED_TEMPORARILY elif type == 'PERMANENT': self.env.status = http.HTTP_MOVED_PERMANENTLY else: msg = 'Unkonwn redirection type: %s' % type raise error.UnsupportedUsageError(msg) return 'REDIRECTING...' Chula-0.7.0/chula/www/http.py0000644000175000017500000000240611370740256017020 0ustar jmcfarlanejmcfarlane""" """ HTTP_CONTINUE = 100 HTTP_SWITCHING_PROTOCOLS = 101 HTTP_PROCESSING = 102 HTTP_OK = 200 HTTP_CREATED = 201 HTTP_ACCEPTED = 202 HTTP_NON_AUTHORITATIVE = 203 HTTP_NO_CONTENT = 204 HTTP_RESET_CONTENT = 205 HTTP_PARTIAL_CONTENT = 206 HTTP_MULTI_STATUS = 207 HTTP_MULTIPLE_CHOICES = 300 HTTP_MOVED_PERMANENTLY = 301 HTTP_MOVED_TEMPORARILY = 302 HTTP_SEE_OTHER = 303 HTTP_NOT_MODIFIED = 304 HTTP_USE_PROXY = 305 HTTP_TEMPORARY_REDIRECT = 307 HTTP_BAD_REQUEST = 400 HTTP_UNAUTHORIZED = 401 HTTP_PAYMENT_REQUIRED = 402 HTTP_FORBIDDEN = 403 HTTP_NOT_FOUND = 404 HTTP_METHOD_NOT_ALLOWED = 405 HTTP_NOT_ACCEPTABLE = 406 HTTP_PROXY_AUTHENTICATION_REQUIRED= 407 HTTP_REQUEST_TIME_OUT = 408 HTTP_CONFLICT = 409 HTTP_GONE = 410 HTTP_LENGTH_REQUIRED = 411 HTTP_PRECONDITION_FAILED = 412 HTTP_REQUEST_ENTITY_TOO_LARGE = 413 HTTP_REQUEST_URI_TOO_LARGE = 414 HTTP_UNSUPPORTED_MEDIA_TYPE = 415 HTTP_RANGE_NOT_SATISFIABLE = 416 HTTP_EXPECTATION_FAILED = 417 HTTP_UNPROCESSABLE_ENTITY = 422 HTTP_LOCKED = 423 HTTP_FAILED_DEPENDENCY = 424 HTTP_INTERNAL_SERVER_ERROR = 500 HTTP_NOT_IMPLEMENTED = 501 HTTP_BAD_GATEWAY = 502 HTTP_SERVICE_UNAVAILABLE = 503 HTTP_GATEWAY_TIME_OUT = 504 HTTP_VERSION_NOT_SUPPORTED = 505 HTTP_VARIANT_ALSO_VARIES = 506 HTTP_INSUFFICIENT_STORAGE = 507 HTTP_NOT_EXTENDED = 510 Chula-0.7.0/chula/www/__init__.py0000644000175000017500000000025211370740256017575 0ustar jmcfarlanejmcfarlane""" Chula objects used to help wire up web based applications """ __all__ = ['apache', 'controller', 'cookie', 'fakerequest' ] Chula-0.7.0/chula/data.py0000644000175000017500000003540511370740256016133 0ustar jmcfarlanejmcfarlane""" Functions to make working with data easier. """ import datetime import re import string import time from chula import error, regex TRUE = ['1', 't', 'true', 'yes', 'y', 'on'] FALSE = ['0', 'f', 'false', 'no', 'n', 'off'] RE_UNIX_TIMESTAMP = re.compile(r'[0-9]{10}(\.[0-9]+)?') def commaify(input_): """ Generate a number with commas for pretty printing @param input_: Data to be commaified @type input_: Number or string @return: String >>> print commaify('45000000000') 45,000,000,000 """ parts = str(input_).split('.') formatted = re.sub(r'(\d{3})', r'\1,', parts[0][::-1]) # Strip off extra comma on the front (currently end) if it exists if formatted.endswith(','): formatted = formatted[:-1] # Add the decimal back on if it exists try: output = formatted[::-1] + '.' + parts[1].ljust(2, '0') except IndexError: output = formatted[::-1] except: print 'Unable to format number:', input_ raise return output.replace('-,', '-') def date_add(unit, delta, date): """ Add or subtract from the date passed @param unit: Unit of measure (B{S}ec/B{M}in/B{H}r/B{d}ays/B{w}eeks) @type unit: String @param delta: Offset, amount to adjust the date by @type delta: Integer @param date: Date to be added/subtracted to/from @type date: datetime.datetime @return: datetime.datetime >>> start = str2date('1/1/2005 11:35') >>> print date_add('days', -5, start) 2004-12-27 11:35:00 """ initial = date if unit == 'seconds' or unit == 's': delta = datetime.timedelta(seconds=delta) elif unit == 'minutes' or unit == 'm': delta = datetime.timedelta(minutes=delta) elif unit == 'hours' or unit == 'h': delta = datetime.timedelta(hours=delta) elif unit == 'days' or unit == 'd': delta = datetime.timedelta(days=delta) elif unit == 'weeks' or unit == 'w': delta = datetime.timedelta(months=delta) else: msg = 'Invalid unit, please use: s, m, h, d, or w' raise error.UnsupportedUsageError(msg) return initial + delta def date_diff(start, stop, unit='seconds'): """ Calculates the difference between two dates. @param start: Start time @type start: datetime.datetime @param stop: Stop time @type stop: datetime.datetime @param unit: Unit of measure (B{S}ec/B{M}in/B{H}r/B{d}ays/B{w}eeks) @type unit: String @return: Integer (defaults to seconds, if unit not passed) >>> start = str2date('1/1/2005') >>> stop = str2date('1/5/2005') >>> print date_diff(start, stop) 345600.0 >>> print date_diff(start, stop, 'd') 4.0 """ if start > stop: start, stop = stop, start issign = -1 else: issign = 1 diff = stop - start days = diff.days minutes, seconds = divmod(diff.seconds, 60) hours, minutes = divmod(minutes, 60) seconds += round((days * 86400) + (hours * 3600) + (minutes * 60)) minutes += round((days * 1440) + (hours * 60)) hours += round(days * 24) days = round(days) weeks = round(days * 7) if unit == 'minutes' or unit == 'm': return minutes * issign elif unit == 'hours' or unit == 'h': return hours * issign elif unit == 'days' or unit == 'd': return days * issign elif unit == 'weeks' or unit == 'w': return weeks * issign else: return seconds * issign def date_within_range(time, offset, now=None): """ The idea is to provide shorthand for "is foobar time within 02:00 + 30 min". This can be useful for things that look for time periods when different logic applies, like from 2am and the next 30 minutes expect the network to be slow, as backups are taking place. Anything in the past is considered out of range. B{Uncertain if this method should stay or be removed} @param time: Representation of hours:minutes @type time: String representation of time @param offset: The size of the range or window @type offset: Integer @param now: I{Optional} argument to specify time range/window start. @type now: datetime.datetime @return: bool >>> print date_within_range('11:00', 30, str2date('1/1/2005 11:25')) True >>> print date_within_range('11:00', 30, str2date('1/1/2005 11:35')) False """ range = time.split(':') if now is None: now = datetime.datetime.now() # Calculate the begin time based on today begin = datetime.datetime(now.year, now.month, now.day, int(range[0]), int(range[1])) diff = now - begin # Determine if now() is beyond the begin time (positive number of days) if diff.days >= 0: minutes = diff.seconds / 60 if minutes <= offset: return True else: return False else: return False def escape_for_js(input): """ Clean the input for use within javascript @param input: String to have illegal js chars escaped @type input: str @return: str (safe for use in javascript) """ replace = string.replace input = replace(input, "'", r"\'") input = replace(input, '"', r'\"') input = replace(input, "\r\n", ' ') return input def format_phone(input_): """ Format a string into a properly formatted telephone number. Accepts a few common patterns. @param input_: Telephone number to format @type input_: String @return: String formatted as: (area) exchange-number >>> print format_phone('555-123-1234') (555) 123-1234 """ m = re.match(r'(?P\d{3})?' r'(\D)*' r'(?P\d{3})' r'(\D)*' r'(?P\d{4})', input_) if not m is None: area = m.group('area') if not area is None: area = '(%s) ' % area else: area = '' phone = '%s-%s' % (m.group('exchange'), m.group('number')) return area + phone else: return input_ def format_money(amount): """ Format a numeric value into commaified dollars and cents (two digits) @param amount: Money to be converted @type currency: Float @return: String >>> print format_money(15000) 15,000.00 >>> print format_money(15000.100030) 15,000.10 """ try: amount = float(amount) except Exception: msg = 'The money passed must be castable as float' raise error.TypeConversionError(amount, 'float', append=msg) return commaify('%.2f' % amount) def isdate(input_): """ Determines if the value passed is a date. @param input_: Value to evaluate @type input_: Anything @return: bool >>> print isdate('1/1/2005') True >>> print isdate('1/41/2005') False """ try: date = str2date(input_) if date is None: return False else: return True except: return False def isregex(input_): """ Determines if the value passed is a valid regular rexpression @param input_: Value to evaluate @type input_: str @return: bool >>> print isregex(r'.*') True >>> print isregex(r'[') False """ try: re.compile(input_) return True except: return False def istag(input_, regexp=None): """ Determines if the value passed is a tag @param input_: Value to evaluate @type input_: Anything @param regexp: Alternate regex to chula.regex.TAG @type regexp: Valid regular expression (string) @return: bool >>> print istag('foo') True >>> print istag('foo!!') False """ if not isinstance(input_, basestring): return False if regexp is None: regexp = regex.TAG if re.search(regexp, input_) is None: return False else: return True def istags(input_, regexp=None): """ Determines if the value passed is a collection of tag. @param input_: Value to evaluate @type input_: Anything @param regexp: Alternate regex to chula.regex.TAGS @type regexp: Valid regular expression (string) @return: bool >>> print istags('foo bar') True >>> print istags('foo, bar') True >>> print istags('foo!! bar') False """ if not isinstance(input_, basestring): return False if regexp is None: regexp = regex.TAGS if re.search(regexp, input_) is None: return False else: return True def none2empty(input_): """ Convert none to an empty string. @param input_: Value to evaluate @type input_: Anything @return: Empty string ('') or value passed >>> print none2empty([1]) [1] >>> if none2empty(None) == '': ... print True True """ if input_ is None: return '' else: return input_ def replace_all(subs, input_): """ Simple text replacement, using an input_ dictionary against a string. @param subs: Dictionary of find/replace values @type subs: Dictionary @param input_: String to update @type input_: String @return: String >>> print replace_all({'l':'1', 'o':'0'}, "Hello world") He110 w0r1d """ if not input_ is None and input_ != '': # Generate a regular expression using the dictionary keys regex = re.compile("(%s)" % "|".join(map(re.escape, subs.keys()))) # Recursively for each match, look-up corresponding value in dictionary return regex.sub(lambda mo: subs[mo.string[mo.start():mo.end()]], input_) else: return input_ def str2bool(input_): """ Determine if the data passed is either True or False @param input_: Value to evaluate @type input_: String @return: bool >>> str2bool(True) True >>> str2bool('on') True """ if input_ in [True, False]: return input_ input_ = str(input_).lower() if input_ in TRUE: return True elif input_ in FALSE: return False else: raise error.TypeConversionError(input_, 'boolean') def str2date(input_): """ Conversion from string to datetime object. Most of the common patterns are currently supported. If None is passed None will be returned @param input_: Date time (of supported pattern) @type input_: String, or None @return: datetime.datetime >>> print str2date("10/4/2005 21:45") 2005-10-04 21:45:00 """ if input_ is None: return None elif not isinstance(input_, basestring): msg = 'Value passed must be of type string.' raise error.TypeConversionError(input_, 'datetime.datetime', append=msg) # Consume a unix timestamp if possible if not RE_UNIX_TIMESTAMP.match(input_) is None: return datetime.datetime.fromtimestamp(float(input_)) from time import strptime ptime = {'I':'00', 'M':'00', 'S':'00'} parts = {'Y':r'(?P(1|2)\d{3})', 'm':r'(?P(1[0-2]|0?[1-9]))', 'd':r'(?P([0-2]?[1-9]|[123][01]))', 'I':r'(?P([0-5]?[0-9]|60))', 'M':r'(?P([0-5]?[0-9]|60))', 'S':r'(?P([0-5]?[0-9]|60))'} regs = [] regs.append('^%(m)s\D%(d)s\D%(Y)s$') regs.append('^%(m)s%(d)s%(Y)s$') regs.append('^%(m)s\D%(d)s\D%(Y)s\D%(I)s\D%(M)s$') regs.append('^%(m)s\D%(d)s\D%(Y)s\D%(I)s\D%(M)s\D%(S)s$') regs.append('^%(Y)s\D%(m)s\D%(d)s$') regs.append('^%(Y)s%(m)s%(d)s$') regs.append('^%(Y)s\D%(m)s\D%(d)s\D%(I)s\D%(M)s$') regs.append('^%(Y)s\D%(m)s\D%(d)s\D%(I)s\D%(M)s\D%(S)s$') # 2007-09-25 00:00:00-04:00 regs.append('^%(Y)s\D%(m)s\D%(d)s\D%(I)s\D%(M)s\D%(S)s[+-]\d{2}:\d{2}$') # 2005-10-4 21:01:00.970532-04:00 # 2009-04-16 23:16:34.953368+00:00 exp = r'^%(Y)s\D%(m)s\D%(d)s\D%(I)s\D%(M)s\D%(S)s\.[0-9]+[+-]\d{2}:\d{2}$' regs.append(exp) for regexp in regs: match = re.match(regexp % parts, input_) if not match is None: ptime.update(match.groupdict()) break if len(ptime.keys()) == 3: raise error.TypeConversionError(input_, 'datetime') fixed = '%(Y)s-%(m)s-%(d)sT%(I)s:%(M)s:%(S)s' % ptime return datetime.datetime(*strptime(fixed, "%Y-%m-%dT%H:%M:%S")[0:6]) def str2tags(input_): """ Convert a string of tags into a sorted list of tags @param input_: String @type input_: String @return: List >>> print str2tags('linux good ms annoying linux good') ['annoying', 'good', 'linux', 'ms'] """ if istags(input_): input_ = input_.lower() input_ = input_.replace(',', ' ') tags = list(frozenset(input_.split())) if '' in tags: tags.remove('') tags.sort() return tags elif input_ == '': return [] raise error.TypeConversionError(input_, 'list of tags') def str2unicode(retval, encoding='utf8', errors='ignore'): """ Convert a string into a unicode encoded string. If the character set is not specified, utf-8 will be used. If errors are encountered during conversion, by default they will be ignored. This means the invalid characters will be removed. @param retval: String @type retval: str, unicode @param encoding: Desired encoding, utf8 by default @type encoding: str @param errors: How to handle unicode conversion errors @type: str (valid values: 'strict', 'replace', 'ignore') @return: str >>> print str2unicode('abc') abc >>> print str2unicode('\x80abc') abc """ if not isinstance(retval, unicode): retval = unicode(retval, errors=errors) return retval.encode(encoding) def wrap(input_, wrap): """ Wrap a string with something @param input_: String to wrap @type input_: String @param wrap: String to wrap with @type wrap: String @return: String >>> print wrap('sql', "'") 'sql' """ return '%s%s%s' % (wrap, input_, wrap) def tags2str(tags): """ Convert a list of tags into a tsearch2 compliant string. Note that this string is not single quote padded for use with an sql insert statement. For that, pass the returened value to: db.ctags() @param tags: List of valid tags @type tags: List @return: List >>> print tags2str(['annoying', 'good', 'linux', 'ms']) annoying good linux ms """ if not isinstance(tags, list): msg = "The list of tags passed is not a list" raise ValueError, msg for tag in tags: if not istag(tag): raise error.TypeConversionError(tag, 'tag') tags.sort() return ' '.join(tags) Chula-0.7.0/chula/test/0000755000175000017500000000000011412546122015611 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/test/bat.py0000644000175000017500000000322011370740256016735 0ustar jmcfarlanejmcfarlane"""Class for use with Basic Acceptance Testing""" import os import signal import subprocess import time import unittest import urllib2 from chula import collection #class RedirectHandler(urllib2.HTTPRedirectHandler): # def http_error_301(self, req, fp, code, msg, headers): # upstream = urllib2.HTTPRedirectHandler.http_error_301 # result = upstream(self, req, fp, code, msg, headers) # result.code = code # return result # # def http_error_302(self, req, fp, code, msg, headers): # upstream = urllib2.HTTPRedirectHandler.http_error_302 # result = upstream(self, req, fp, code, msg, headers) # result.code = code # return result class Bat(unittest.TestCase): def setUp(self): self.server = subprocess.Popen(['./apps/basic/webserver'], shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) time.sleep(0.5) def tearDown(self): os.kill(self.server.pid, signal.SIGTERM) def request(self, url): if not url.startswith('http://'): url = 'http://localhost:8080' + url #opener = urllib2.build_opener(RedirectHandler()) #response = opener.open(url) try: response = urllib2.urlopen(url) except urllib2.HTTPError, ex: response = ex except urllib2.URLError, ex: response = ex retval = collection.Collection() retval.data = response.read() retval.status = response.code retval.headers = response.info().headers return retval Chula-0.7.0/chula/test/selenium.py0000644000175000017500000000262611370740256020021 0ustar jmcfarlanejmcfarlaneimport os import socket import sys import time import unittest from chula.vendor import selenium as upstream KEY_ENTER = '10' UNSET = 'UNSET' class TestCase(unittest.TestCase): def __init__(self, methodName): super(TestCase, self).__init__(methodName) # Member variables self.browser = '*firefox' self.max_wait = 15 * 1000 self.remote_control_port = 4444 self.remote_control = 'localhost' self.speed = 0 self.target = 'http://localhost' # Fill any attributes from the environment for key in os.environ: if getattr(self, key, None) is UNSET: setattr(self, key, os.environ[key]) # Cast a few types self.speed = int(self.speed) self.max_wait = int(self.max_wait) def setUp(self): self.selenium = upstream.selenium(self.remote_control, self.remote_control_port, self.browser, self.target) # Create a shorthand alias self.s = self.selenium # Start a session on the remote control, slow things down a little self.selenium.start() self.selenium.set_speed(int(self.speed)) def tearDown(self): self.selenium.stop() def wait(self): self.selenium.wait_for_page_to_load(self.max_wait) Chula-0.7.0/chula/test/__init__.py0000644000175000017500000000000011370740256017717 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/collection/0000755000175000017500000000000011412546122016765 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/collection/ubound.py0000644000175000017500000000136111370740256020643 0ustar jmcfarlanejmcfarlane""" Collection that supports a configured maximum size. The size is enforced by purging records in a FIFO manner. """ from collections import deque from chula import error from chula.collection import base class UboundCollection(base.Collection): """ Collection class with a maximum size """ def __init__(self, max): super(UboundCollection, self).__init__() self.__dict__['max'] = max self.__dict__['fifo'] = deque() def __setitem__(self, key, value): if len(self.__dict__['fifo']) >= self.__dict__['max']: evict = self.__dict__['fifo'].popleft() self.remove(evict) self.__dict__['fifo'].append(key) super(UboundCollection, self).__setitem__(key, value) Chula-0.7.0/chula/collection/base.py0000644000175000017500000000446111370740256020265 0ustar jmcfarlanejmcfarlane""" Flexible collection that supports both dictionary and attribute style access. """ from copy import deepcopy from chula import error class Collection(dict): """ Example usage: >>> from chula import collection >>> person = collection.Collection() >>> person.name = 'Mr. Smith' >>> person.age = 20 >>> person.timezone = 'PST' >>> >>> print person.age 20 >>> print person['timezone'] PST >>> print person {'timezone': 'PST', 'age': 20, 'name': 'Mr. Smith'} """ def __delattr__(self, key): """ Allow attribute style deletion @param key: Key to be deleted @type key: String """ self.__delitem__(key) def __deepcopy__(self, memo={}): """ Return a fresh copy of a Collection object """ return self.copy_into(self.__class__()) def copy_into(self, collection): """ Copy the current object into the object passed @param collection: Object to be copied into @type collection: chula.collection.Collection @return: chula.collection.Collection (filled copy) """ for key, value in self.iteritems(): collection[key] = deepcopy(value) return collection def __getattr__(self, key): """ Allow attribute style get @param key: Key to be accessed @type key: String @return: Attribute """ try: return self.__getitem__(key) except KeyError: raise AttributeError('Attribute not found: %s' % key) def __setattr__(self, key, value): """ Allow attribute style set @param key: Key to be set @type key: String @param value: Value of key @type value: Any """ self.__setitem__(key, value) def add(self, key, value): """ Allow set via method @param key: Key to be set @type key: String @param value: Value of key @type value: Any """ self.__setitem__(key, value) def remove(self, key): """ Allow list.remove() style attribute deletion @param key: Key/value pair to be removed @type key: String """ self.__delitem__(key) Chula-0.7.0/chula/collection/__init__.py0000644000175000017500000000022611370740256021105 0ustar jmcfarlanejmcfarlane""" Chula collection package """ from chula.collection.base import * from chula.collection.restricted import * from chula.collection.ubound import * Chula-0.7.0/chula/collection/restricted.py0000644000175000017500000000663711370740256021532 0ustar jmcfarlanejmcfarlane""" Collection with a configurable, but enforced number of attributes. This class is useful for situations where you you need to enforce every instance of the collection has the exact same structure. """ from copy import deepcopy from chula import error from chula.collection import base UNSET = 'THIS ATTRIBUTE NEEDS TO BE SET BY YOU' class RestrictedCollection(base.Collection): """ Collection class with a pre-determined set of validkeys attributes """ def __init__(self): super(RestrictedCollection, self).__init__() self.__dict__['privatekeys'] = self.__privatekeys__() self.__dict__['validkeys'] = self.__validkeys__() self.__defaults__() # Ensure defaults have all been set properly for key in self.__dict__['validkeys']: if not key in self: raise error.RestrictecCollectionMissingDefaultAttrError(key) def __privatekeys__(self): return (()) def __validkeys__(self): return (()) def __defaults__(self): pass def __delitem__(self, key): """ Prevent deletion of keys @param key: Key to be deleted @type key: String @return: None """ if key in self.__dict__['validkeys']: return super(RestrictedCollection, self).__delitem__(key) else: raise error.RestrictecCollectionKeyRemovalError(key) def __getitem__(self, key): """ Allow restricted attribute access @param key: Key to be accessed @type key: String @return: Attribute """ if key in self.__dict__['validkeys']: value = super(RestrictedCollection, self).__getitem__(key) if value != UNSET: return value else: raise error.RestrictecCollectionMissingDefaultAttrError(key) elif key in self.__dict__['privatekeys']: return super(RestrictedCollection, self).__getitem__(key) else: raise error.InvalidCollectionKeyError(key) def __setitem__(self, key, value): """ Allow restricted attribute write access @param key: Key to be set @type key: String @param value: Value of key @type value: Anything """ if key in self.__dict__['validkeys']: super(RestrictedCollection, self).__setitem__(key, value) elif key in self.__dict__['privatekeys']: super(RestrictedCollection, self).__setitem__(key, value) else: raise error.InvalidCollectionKeyError(key) def __delattr__(self, key): raise error.RestrictecCollectionKeyRemovalError(key) def __getattr__(self, key): return self.__getitem__(key) def __getstate__(self): return self def __setattr__(self, key, value): self.__setitem__(key, value) def __setstate__(self, dict_): self.__dict__['validkeys'] = dict_.keys() self.update(dict_) def strip(self): """ Purge I{privatekeys} from the collection. This is useful when passing the collection along, without it's private keys. This does actually delete the private keys, and thus acts on itself. If this isn't what you want use copy.deepcopy(). """ for key in self.__dict__['privatekeys']: super(RestrictedCollection, self).__delitem__(key) return self Chula-0.7.0/chula/webservice.py0000644000175000017500000001267211370740256017361 0ustar jmcfarlanejmcfarlane""" Chula helper module for working with web services """ import cPickle from chula import error, json, collection class Transport(collection.RestrictedCollection): """ Web service transport class designed to be subclassed to provide various means of encoding. """ def __init__(self, controller): super(Transport, self).__init__() self.controller = controller def __privatekeys__(self): """ Populate the private keys """ return ('controller',) def __validkeys__(self): """ Populate the supported keys """ return ('data', 'exception', 'msg', 'success') def __defaults__(self): """ Set reasonable defaults """ self.data = None self.exception = None self.msg = None self.success = False @staticmethod def __default__(controller, kwargs, arg, default): """ Return a default value of the specified arg for use in the creation or calling of a webservice. This method allows the webservice usage to be controlled either at creation or runtime (kwargs, http args). The algorithm used is: 1. Fetch from HTTP GET variables 2. Fetch from HTTP POST variables 3. Fetch from **kwargs passed in the decorated method 4. Use the default value set in the transport Currently supported keyword arguments: - x_header: Should the HTTP X-JSON header be used for payload @param kwargs: The collection of key=value arguments @type keyargs: Dict @param arg: The specific argument to be used @type arg: String @param default: The default value @type: default: Boolean """ # The default then update with the desired algorithm retval = default # Use kwargs passed to the controller's decorator if available retval = kwargs.get(arg, retval) # Use HTTP POST if available retval = controller.env.form.get(arg, retval) # Use HTTP GET if available retval = controller.env.form_get.get(arg, retval) return retval class JSON(Transport): """ Webservice that uses json as the transport layer """ def encode(self, **kwargs): """ Encode the transport into a json string and return X-JSON """ if self.__default__(self.controller, kwargs, 'x_header', False): self.controller.content_type = 'application/x-json' self.controller.env.headers.append(('X-JSON', json.encode(self.strip()))) return '' else: self.controller.content_type = 'text/plain' return json.encode(self.strip()) class PICKLE(Transport): """ Webservice that uses Python pickling as the transport layer """ def encode(self, **kwargs): """ Encode the transport into a Python pickled string """ self.controller.content_type = 'text/plain' return cPickle.dumps(dict(self.strip())) class ASCII(Transport): """ Webservice that uses ascii as the transport layer """ def encode(self, **kwargs): """ Encode the transport into an acii string """ payload = str(self.data) self.controller.content_type = 'text/plain' # With ascii we don't have any structure, so either return the # data or an exception if caught if not self.exception is None: return str(self.exception) else: return payload class Transports(object): """ Bundle the transports in an object for use with getattr """ JSON = JSON ASCII = ASCII PICKLE = PICKLE def expose(**kwargs): """ Decorator for exposing a method as a web service. It takes a list of keyword arguments which are passed to the webservice encoding() method for use with making decisions. For example: @webservice.expose(x_header=True) def foo(self): pass will cause services using JSON as a transport to include the payload in a X-JSON HTTP header rather than the actual body. You can also set these via HTTP GET arguments. See chula.webservice.Transport.__default__() for more information. """ def decorator(fcn): def wrapper(self): transport = Transport.__default__(self, kwargs, 'transport', 'JSON').upper() # Reference the actual transport object, JSON is default webservice_callable = getattr(Transports, transport, None) if webservice_callable is None: raise error.WebserviceUnknownTransportError(transport) # Instantiate the web servie ws = webservice_callable(self) # Execute the controller and fill the webservice try: ws.data = fcn(self) ws.success = True except Exception, ex: ws.exception = str(ex) # Let the controller specify the msg if provided if hasattr(self, 'msg'): ws.msg = self.msg return ws.encode(**kwargs) return wrapper return decorator Chula-0.7.0/chula/json.py0000644000175000017500000000046711370740256016173 0ustar jmcfarlanejmcfarlane""" Wrapper to make it easy to switch from one json library to another """ from chula import error try: import simplejson except: try: from django.utils import simplejson except: raise error.MissingDependencyError('Simplejson') decode = simplejson.loads encode = simplejson.dumps Chula-0.7.0/chula/nosql/0000755000175000017500000000000011412546122015766 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/nosql/couch.py0000755000175000017500000001123711412545537017461 0ustar jmcfarlanejmcfarlanefrom operator import itemgetter import copy import os import re import time from couchdb import client, ResourceNotFound from chula.db import datastore from chula import logger CONNECTION_CACHE = {} ENCODING = 'ascii' ENV = 'CHULA_COUCHDB_SERVER' VALID_ID = r'^[-a-zA-Z0-9_.]+$' VALID_ID_RE = re.compile(VALID_ID) def connect(db, server=None, shard=None): futon = None if server is None: server = os.environ.get(ENV, None) if server is None: msg = 'Server uri not specified' raise Exception(msg) # Determine the cache key and serve from cache if possible key = (server, db, shard) if key in CONNECTION_CACHE: return CONNECTION_CACHE[key] # Create fresh connection to the db, and cache it futon = datastore.DataStoreFactory('couchdb:%s' % server) if shard is None: conn = futon.db(db) else: conn = futon.db(os.path.join(db, shard)) CONNECTION_CACHE[key] = conn return conn class Document(dict): """ CouchDB document abstraction class """ def __init__(self, id, db_conn=None, document=None, server=None, shard=None, track_dirty=True): self.db_conn = db_conn self.server = server self.shard = shard self.track_dirty = track_dirty super(Document, self).__init__() if not db_conn is None: self.db = db_conn else: self.db = connect(self.DB, server=server, shard=shard) # If this is a couchdb document, just fill - don't fetch try: self.fill(id, document) except TypeError: try: self.fill(self.db[id]['_id'], self.db[id]) except ResourceNotFound, ex: self.fill(id, {}) except: raise # Allow keeping track of is_dirty if track_dirty: self._copy = copy.deepcopy(self) else: track_dirty = None # Loggers use thread locks and thus can't be copied self.log = logger.Logger().logger('chula.nosql.couch') @staticmethod def sanitize_id(id): if VALID_ID_RE.match(id) is None: msg = 'Invalid couchdb document name: "%s", must match: %s' raise InvalidCouchdbDocumentId(msg % (id, VALID_ID)) else: return id.encode(ENCODING) @classmethod def delete(self, id, server=None, shard=None): id = self.sanitize_id(id) db = connect(self.DB, server=server, shard=shard) try: del db[id] except ResourceNotFound: pass def fill(self, id, data): self.id = self.sanitize_id(id) self.update(data) def is_dirty(self): if self._copy is None: return True for key, value in self.iteritems(): if self._copy.get(key, '__MISSING__') != value: return True if self.id != self._copy.id: return True return False def persist(self): # Make sure the id has not been made invalid self.id = self.sanitize_id(self.id) # Support new documents if self.get('_rev', None) is None: self['created'] = time.time() self.db[self.id] = self # Support existing documents elif self.is_dirty(): # Support document renames if not self._copy is None and self.id != self._copy.id: try: del self.db[self._copy.id] except ResourceNotFound, ex: msg = 'Rename attempt failed (must be a unique id)' raise DocumentAlreadyExistsError(msg) # Since couchdb sees this as a del/add - remove the _rev del self['_rev'] self['modified'] = time.time() self.db[self.id] = self return self.db[self.id]['_rev'] return self['_rev'] class Documents(list): def __init__(self, server=None, shard=None): super(Documents, self).__init__() self.db = connect(self.DB, server=server, shard=shard) self.server = server self.shard = shard def _fill(self, view, cls): if not cls is None: return [cls(doc.id, doc.value) for doc in view] else: return view def query(self, func, cls=None, **kwargs): return self._fill(self.db.query(func, **kwargs), cls) def view(self, name, cls=None, **kwargs): return self._fill(self.db.view(name, **kwargs), cls) class DocumentAlreadyExistsError(ResourceNotFound): pass class InvalidCouchdbDocumentId(Exception): pass Chula-0.7.0/chula/nosql/__init__.py0000644000175000017500000000000011370740256020074 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/queue/0000755000175000017500000000000011412546122015756 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/queue/messages/0000755000175000017500000000000011412546122017565 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/queue/messages/message.py0000644000175000017500000000750011370740256021574 0ustar jmcfarlanejmcfarlane"""Base message queue object""" import datetime import thread from chula import collection, data, error, json from chula.queue import messages class Message(collection.Collection): def __init__(self, msg=None): now = datetime.datetime.now() self.created = now self.id = thread.get_ident() self.inprocess = False self.message = None self.name = '%s.%s.msg' % (now.strftime('%Y%m%d%H%M%S'), self.id) self.processed = False self.type = None self.updated = None self.fill(msg) @staticmethod def decode(msg): try: return json.decode(msg) except ValueError, er: raise InvalidMessageEncodingError(str(msg)) def encode(self): try: mask = '%Y/%m/%d %H:%M:%S' self.created = self.created.strftime(mask) if not self.updated is None: self.updated = self.created.updated(mask) return json.encode(self) except TypeError, er: msg = 'Message is not [json] not encodable: ' + str(self) raise TypeError(msg) def fill(self, msg): self.type = '%s.%s' % (self.__class__.__module__, self.__class__.__name__) if not msg is None: # Fill from the decoded message values for key in self.keys(): self[key] = msg[key] # Enforce attribute types self.id = int(self.id) self.created = data.str2date(self.created) self.updated = data.str2date(self.updated) self.inprocess = data.str2bool(self.inprocess) self.processed = data.str2bool(self.processed) def process(self): msg = 'Please overload the process() method' raise NotImplementedError(msg) def validate(self): pass class MessageFactory(object): """ Transform the passed object into it's native type """ def __new__(self, msg): """ Construct a new message of any subclass of Message() @param msg: Message to be created @type msg: file or dict @return: chula.queue.messages.message.Message or subclass of """ if isinstance(msg, file): msg = Message.decode(''.join(msg.readlines())) # Currently not persisting "inprocess" to the file so go # by the name of the actual file, not it's contents if str(file.name).endswith('.inprocess'): msg['inprocess'] = True elif isinstance(msg, dict): pass else: msg = 'Invalid message: %s' % msg raise Exception(msg) try: # Pull out the exact type of message and import the module module_path, class_name = msg['type'].rsplit('.', 1) module = __import__(module_path, globals(), locals(), [class_name]) # Instantiate an instance of the actual type (subclass of Message) msg = module.Message(msg) except Exception, ex: msg = 'Error detail: %s' % ex raise InvalidMessageEncodingError(msg) # Some sanity checking if not isinstance(msg, Message): msg = '[%s] must subclass chula.queue.messages.Message' % msg raise Exception(msg) else: return msg class CannotPurgeUnprocessedError(error.ChulaException): """ Exception indicating that the message was not marked as having been processed, thus cannot be purged from the queue """ def msg(self): return "Unable to purge an unprocessed message" class InvalidMessageEncodingError(error.ChulaException): """ Exception indicating that the message is not propery encoded """ def msg(self): return "Incorrect encoding, or incomplete message" Chula-0.7.0/chula/queue/messages/echo.py0000644000175000017500000000060611370740256021066 0ustar jmcfarlanejmcfarlane""" Chula echo message object """ from chula.queue.messages import message class Message(message.Message): def process(self): return self.message if __name__ == '__main__': from chula.queue.messages.echo import Message from chula.queue.tester import Tester msg = Message() msg.message = 'This is a test message' tester = Tester() tester.test(msg) Chula-0.7.0/chula/queue/messages/mail.py0000644000175000017500000000404111370740256021067 0ustar jmcfarlanejmcfarlane""" Chula email message object """ from chula import collection from chula.mail import Mail from chula.queue.messages import message class Message(message.Message): def fill(self, msg): super(Message, self).fill(msg) self.message = Contract() # Update the contract if not msg is None: if not msg['message'] is None: for key, value in msg['message'].iteritems(): self.message[key] = value def process(self): email = Mail(self.message.smtp) email.from_addy = self.message.from_addy email.to_addy = self.message.to_addy email.body = self.message.body email.subject = self.message.subject try: email.send() return 'Mail Sent' except: raise def validate(self): for key, value in self.message.iteritems(): if value == collection.UNSET: msg = 'Required message attribute not specified: %s' % key raise KeyError(msg) class Contract(collection.RestrictedCollection): def __validkeys__(self): """ Email message body to force the required attributes """ return ('body', 'from_addy', 'reply_to_addy', 'smtp', 'subject', 'to_addy') def __defaults__(self): self.body = collection.UNSET self.from_addy = collection.UNSET self.reply_to_addy = None self.smtp = collection.UNSET self.subject = collection.UNSET self.to_addy = collection.UNSET if __name__ == '__main__': from chula.queue.messages.mail import Message from chula.queue.tester import Tester msg = Message() msg.message.body = 'Hello world' msg.message.subject = 'Testing message queue with email message' msg.message.from_addy = 'john.mcfarlane+chula@gmail.com' msg.message.to_addy = msg.message.from_addy msg.message.smtp = 'smtp.comcast.net' tester = Tester() tester.test(msg) Chula-0.7.0/chula/queue/messages/__init__.py0000644000175000017500000000000011370740256021673 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/queue/mqueue.py0000644000175000017500000001373011370740256017644 0ustar jmcfarlanejmcfarlane""" Chula message queue """ import Queue import cPickle import os import shutil from chula import collection from chula.queue.messages import message class MessageQueue(Queue.Queue, object): """ Standard Python Queue.Queue with filesystem backing. It also supports key based lookups to fetch the output of message processing. """ def __init__(self, config, db=None): """ @param config: Chula configuration @type config: chula.config.Config @param db: Data store for the messages @type db: str (file system path) """ super(MessageQueue, self).__init__() self._msg_store_iter = None if db is None: self.db = config.mqueue_db else: self.db = db # Where do we keep the actual messages on disk self.msg_store = os.path.join(self.db, 'msgs') # Create a location to store the message processing output self.msg_result_store = collection.UboundCollection(1024) # Make sure the db dir exists, creating it if necessary for directory in ['', 'processed', 'failures']: try: os.makedirs(os.path.join(self.msg_store, directory)) except OSError, er: if str(er).startswith('[Errno 17] File exists'): pass else: raise def add(self, msg): """ Add a new message onto the queue @param msg: Message to be added to the queue @type msg: chula.queue.messages.message.Message or subclass @return: None """ self.persist(msg) self.persist_result(msg, None) self.put(msg) def unprocessed_messages(self, subdir='', suffix='.msg'): """ Allow iteration over messages in the store that have not been processed. This method is only meant to be called when the server is not running. Because a mutex is not used, calling this method while the server is running could result in a race condition where a given message is processed twice (though not likely). This method is a generator that yields instances of chula.queue.messages.message.Message or subclasses of it. @param subdir: Directory inside the store to find messages @type subdir: str @param suffix: File suffix to process, defaults to: ".msg" @type suffix: str @return: generator """ directory = os.path.join(self.msg_store, subdir) for f in os.listdir(directory): if f.endswith(suffix): msg = message.MessageFactory(open(os.path.join(directory, f))) yield msg def fetch(self, name): """ Return the result of a message after it was processed @param name: Message name @type name: str @return: Return value of msg.process() or None if not found """ return self.msg_result_store.get(name, None) def msg_path(self, name): """ Fetch the filesystem path in the store for a particular message. @param name: Name of the message (file name) @type name: str @return: str (fully qualified path, similar to readlink -f) """ return os.path.join(self.msg_store, name) def persist(self, msg): """ Persist a message to disk @param msg: Message to be persisted to the store @type msg: chula.queue.messages.message.Message or subclass @return: None """ fmsg = open(self.msg_path(msg.name), 'w') fmsg.write(msg.encode()) fmsg.close() def persist_result(self, msg, result): """ Populate a result value into the private message result store. This allows a request from the client to request the result of message processing. If enough time has elapsed the result might have been purged from the message result store. @param msg: Message to be processed @type msg: chula.queue.messages.message.Message or subclass @param result: Result of message processing @type result: The return value of msg.process() @return: None """ self.msg_result_store[msg.name] = result def mark_in_process(self, msg): """ Mark a given message as being in process @param msg: Message to be processed @type msg: chula.queue.messages.message.Message or subclass @return: bool """ before = os.path.join(self.msg_store, msg.name) after = before + '.inprocess' os.rename(before, after) return msg def get(self): """ Fetch a message from the queue. Calling this method also marks the message as being in process. @return: chula.queue.messages.message.Message or subclass """ msg = super(MessageQueue, self).get() self.mark_in_process(msg) return msg def purge(self, msg, ex=None): """ Move the passed message into the I{processed} location in the store. If there is an exception, persist that into the I{failures} location. @param msg: Message to be purged @type msg: chula.queue.messages.message.Message or subclass @param ex: Exception if any @type ex: Exception or None @return: None """ if ex is None: folder = 'processed' else: folder = 'failures' elog = os.path.join(self.msg_store, folder, msg.name + '.cpickle') elog = open(elog, 'w') cPickle.dump(ex, elog) elog.close() try: fpath = self.msg_path(msg.name + '.inprocess') dest = os.path.join(self.msg_store, folder, msg.name) shutil.move(fpath, dest) except IOError, er: msg = 'The message was not marked as being processed' raise message.CannotPurgeUnprocessedError(msg) Chula-0.7.0/chula/queue/server.py0000755000175000017500000001225411370740256017654 0ustar jmcfarlanejmcfarlane""" Chula message queue daemon """ from __future__ import with_statement import datetime import os import socket import sys import thread import time from chula import json, logger, system from chula.queue import mqueue from chula.queue.messages import message class MessageQueueServer(object): """ Multithreaded server for processing messages in an asynchronous fashion. Communication with the server happens via TCP sockets. """ def __init__(self, config): """ @param config: Chula configuration @type config: chula.config.Config """ self.config = config self.debug = True self.log = logger.Logger(config).logger('chula.queue.server') self.pid_file = os.path.join(self.config.mqueue_db, 'server.pid') self.queue = mqueue.MessageQueue(self.config) self.system = system.System() self.thread_count = 0 self.thread_max = self.system.procs + 1 def worker(self): """ Method to process messages in the queue. The number of worker threads that are spawned at startup are configurable. """ thread_id = thread.get_ident() self.log.debug('Worker thread started: %s' % thread_id) while True: msg = self.queue.get() try: result = msg.process() self.queue.persist_result(msg, result) self.queue.purge(msg) except Exception, ex: self.queue.purge(msg, ex) self.log.debug('%s was processed' % msg.name) if self.debug: print '%s processed by: %s' % (msg.name, thread_id) def receive_message(self, client): """ Receive a message from a chula.queue.client or anything else really. @param client: TCP Socket @type client: socket._socketobject """ chars_left = 1 msg = [''] msg_length = None # Consume the message while chars_left > 0: chunk = client.recv(chars_left) # Check if the client closed prematurely if chunk == '': print 'ERROR: Client socket closed prematurely' break # Look for the message size if msg_length is None: if chunk == ':': try: msg_length = int(msg.pop()) chars_left = msg_length continue except ValueError: client.sendall('BAD') break else: msg[0] += chunk else: # Once size is known, the rest is the actual message if chars_left > 1: chars_left -= len(chunk) msg.append(chunk) # Combine the chunks msg = ''.join(msg) # Check to see if this is a message result fetch if msg.endswith('.msg'): result = self.queue.fetch(msg) result = json.encode(result) client.sendall(result) client.shutdown(0) client.close() return # Decode and add to the queue try: msg = message.Message.decode(msg) msg = message.MessageFactory(msg) self.queue.add(msg) print '%s added' % msg.name # Send a response to the client client.sendall(msg.name) except message.InvalidMessageEncodingError, er: print 'Bad message body' client.sendall('BAD') finally: try: client.shutdown(0) client.close() except: pass def start(self): """ Start the mqueue server """ # Create a pid file pid = open(self.pid_file, 'w') pid.write(str(os.getpid())) pid.close() # Listen on the specified port s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind((self.config.mqueue_host, self.config.mqueue_port)) s.listen(5) # Startup worker threads for t in xrange(self.thread_max): thread.start_new_thread(self.worker, ()) # Before starting the server add any unprocessed msgs to the queue for msg in self.queue.unprocessed_messages(): info = 'Found message out of band, adding to queue: %s' self.log.debug(info % msg) self.queue.add(msg) # Serve forever while True: try: (clientsocket, address) = s.accept() thread.start_new_thread(self.receive_message, (clientsocket,)) except KeyboardInterrupt: os.remove(self.pid_file) print print 'Received signal to shutdown...' break s.shutdown(0) s.close() # Give the socket a second to close time.sleep(1) # Testing if __name__ == '__main__': print 'Running with pid: %s' % os.getpid() from chula import config config = config.Config() server = MessageQueueServer(config) server.start() Chula-0.7.0/chula/queue/client.py0000644000175000017500000000311511370740256017615 0ustar jmcfarlanejmcfarlane""" TCP client for the Chula message queue daemon """ import socket from chula.queue.messages import message class MessageQueueClient(object): def __init__(self, config): self.host = config.mqueue_host self.port = config.mqueue_port def add(self, msg): msg.validate() msg = message.Message.encode(msg) msg = self.encode(msg) # Connect to the server and sent the message self.connect() sent = self.socket.sendall(msg) # Read back the response response = [] while True: chunk = self.socket.recv(512) response.append(chunk) if chunk == '': break self.close() return ''.join(response) def encode(self, msg): msg = '%s:%s' % (len(msg), msg) return msg def connect(self): self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.connect((self.host, self.port)) def close(self): self.socket.shutdown(0) self.socket.close() def fetch(self, name): if name is None: return None # Connect to the server and sent the message self.connect() sent = self.socket.sendall(self.encode(name)) # Read back the response response = [] while True: chunk = self.socket.recv(512) response.append(chunk) if chunk == '': break self.close() response = ''.join(response) return message.Message.decode(response) Chula-0.7.0/chula/queue/__init__.py0000644000175000017500000000000011370740256020064 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/queue/tester.py0000644000175000017500000000154711370740256017654 0ustar jmcfarlanejmcfarlane""" Module for sending test messages thru the queue """ import time from chula.config import Config from chula.queue import client class Tester(object): def __init__(self, config=None, debug=True, wait=500): self.client = client.MessageQueueClient(Config()) self.debug = debug self.wait = wait def test(self, msg): id = self.client.add(msg) print 'Message added to queue:', id if self.debug: self.get_response(id) def get_response(self, id): print '>>> Waiting for response...' for x in xrange(self.wait): time.sleep(0.001) response = self.client.fetch(id) if not response is None: print '>>> Response:', response break if response is None: print ">>> No response after %sms" % self.wait Chula-0.7.0/chula/error.py0000644000175000017500000001700011370740256016342 0ustar jmcfarlanejmcfarlane""" Custom chula exceptions """ class ChulaException(Exception): """ Chula exception class which adds additional functionality to aid in efficiently raising custom exceptions. """ def __init__(self, msg=None, append=None): """ Create custom exception @param msg: Default exception message @type msg: String @param append: Message to be appended to the exception message @type append: String """ self._message = None self.append = append def _get_message(self): """ Getter for a message property, to avoid using an attribute named "message" which will raise deprecation errors in Python-2.6. """ return self._message def _set_message(self, msg): """ Getter for a message property, to avoid using an attribute named "message" which will raise deprecation errors in Python-2.6. @param msg: Error message to be used @type msg: str """ self._message = msg message = property(_get_message, _set_message) def __str__(self, append=None): """ Return the message itself @param append: Additional info to be added to the message @type append: String @return: String """ if self.message is None: self.message = self.msg() if self.append is None: return repr(self.message) else: return repr(self.message + ': ' + self.append) def msg(self): """ When the msg method is not overloaded, return a generic message """ return 'Generic chula exception' class ControllerClassNotFoundError(ChulaException): """ Exception indicating the requested controller class not found. """ def __init__(self, _pkg, append=None): self.message = 'Unable to find the following class: %s' % _pkg self.append = append class ControllerImportError(ChulaException): """ Exception while trying to import the controller. """ def __init__(self, _pkg, append=None): self.message = 'Error while trying to import: %s' % _pkg self.append = append class ControllerMethodNotFoundError(ChulaException): """ Exception indicating the requested controller method not found. """ def __init__(self, _pkg, append=None): self.message = 'Unable to find the following method: %s' % _pkg self.append = append class ControllerModuleNotFoundError(ChulaException): """ Exception indicating the requested module method not found. """ def __init__(self, _pkg, append=None): self.message = 'Unable to find the following module: %s' % _pkg self.append = append class ControllerMethodReturnError(ChulaException): """ Exception indicating that a controller method is returning None, which is probably not on purpose. It's true that we do cast all output as a string, thus None is technically valid, it's most likely that the controller method simply forgot to return. This will save time by pointing this out. If you really need to return None, then return: 'None' """ def msg(self): return "Method either didn't return, or returned None" class ControllerRedirectionError(ChulaException): """ Exception indicating that the controller was unable to perform the requested redirect. """ def msg(self): return "Unable to redirect as requested" #class ExtremeDangerError(ChulaException): # """ # Exception indicating a refusal to do something dangerous. Usually # if this exception is raised you'll be glad it saved you from doing # something stupid. # """ # # def msg(self): # return 'Chula is not willing to perform the requested task' class InvalidAttributeError(ChulaException): """ Exception indicating an invalid attribute was used. """ def __init__(self, key, append=None): self.message = 'Invalid attribute: %s' % key self.append = append class InvalidCacheKeyError(ChulaException): """ Exception indicating an invalid key was used against a cache source. """ def __init__(self, key, append=None): self.message = 'Invalid key: %s' % key self.append = append class InvalidCollectionKeyError(ChulaException): """ Exception indicating an invalid key was used against a restricted collection class. """ def __init__(self, key, append=None): self.message = 'Invalid key: %s' % key self.append = append class MalformedConnectionStringError(ChulaException): """ Exception indicating that the database connection string used is invalid. """ def msg(self): return 'Invalid database connection string' class MalformedPasswordError(ChulaException): """ Exception indicating that the password used does not meet minimum requirements (aka: isn't strong enough). """ def msg(self): return 'Password does not adhere to: chula.regex.PASSWD' class TypeConversionError(ChulaException): """ Exception indicating that the requested data type conversion was not possible. """ def __init__(self, _value, _type, append=None): self.message = 'Unable to convert value [%s] to type [%s]' \ % (str(_value), str(_type)) self.append = append class UnsupportedDatabaseEngineError(ChulaException): """ Exception indicating a requst for an unsupported database engine """ def __init__(self, engine, append=None): self.message = 'Unsupported db engine: %s' % engine self.append = append class UnsupportedMapperError(ChulaException): """ Exception indicating an invalid mapper configuration """ def __init__(self, _pkg, append=None): self.message = 'Invalid chula.config.mapper class: %s' % _pkg self.append = append class UnsupportedUsageError(ChulaException): """ Exception indicating the chula api is being misused. """ def msg(self): return 'Invalid use of this object' class MissingDependencyError(ChulaException): """ Exception indicating a required dependency of chula is either missing or of an incompatible version. """ def __init__(self, _pkg, append=None): self.message = 'Please install: %s' % _pkg self.append = append class RestrictecCollectionKeyRemovalError(ChulaException): """ It is illegal to remove a key from a RestrictedCollection object. """ def __init__(self, key, append=None): self.message = ('Keys cannot be deleted from a' 'ResctrictedCollection object: %s' % key) self.append = append class RestrictecCollectionMissingDefaultAttrError(ChulaException): """ Exception indicating that a restricted attribute was not given a default value. """ def __init__(self, key, append=None): self.message = 'Please set the "%s" attr to fix this' % key self.append = append class SessionUnableToPersistError(ChulaException): """ Chula is unable to persist either to PostgreSQL or Memached. """ def msg(self): return 'Unable to persist session, all backends failed' class WebserviceUnknownTransportError(ChulaException): """ Exception indicating that the specified webservice transport is either unknown or unsupported. """ def __init__(self, key, append=None): self.message = 'Unknown transport: "%s"' % key self.append = append Chula-0.7.0/chula/singleton.py0000644000175000017500000000104311370740256017213 0ustar jmcfarlanejmcfarlane"""Singleton class decorator""" def singleton(cls): """ Singleton class decorator >>> from chula.singleton import singleton >>> >>> @singleton ... class MyClass(object): ... pass ... >>> a = MyClass() >>> b = MyClass() >>> c = MyClass() >>> >>> a is b True >>> a is c True """ instances = {} def get_instance(*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance Chula-0.7.0/chula/pager.py0000644000175000017500000000477311370740256016324 0ustar jmcfarlanejmcfarlane""" Generic paging class designed to aid in drawing next/previous widgets """ class Pager(list): def __init__(self, offset, recordcount, limit=8, visiblepages=19): """ Create a new pager @param offset: 0 based representation of what to offset/skip by @type offset: Integer @param recordcount: 1 based representation of records available @type recordcount: Integer @param limit: 1 based representation of records to show per page @type limit: Integer @param visiblepages: 1 based representation of pages to show at a time @type visiblepages: Integer @return: List of dictionaries >>> from chula import pager >>> p = pager.Pager(0, 50) >>> p[0]['isselected'] True >>> p[0]['offset'] 0 >>> p = pager.Pager(30, 100, 5, 11) >>> p[0]['isselected'] False >>> p[0]['offset'] 5 """ super(Pager, self).__init__() if (visiblepages % 2) == 0: raise ValueError, 'visiblepages cannot be an even number' if recordcount < 1: return if offset < 0: raise ValueError, 'offset must be >= 0' elif offset >= recordcount: raise ValueError, 'offset must be < recordcount' currentpage = offset // limit # 0 based totalpages = ((recordcount - 1) // limit) + 1 # 1 based firstvisible = 0 # 0 based # If current page is more than half of total visiblepages, adjust # first page to avoid sliding too far to the right if currentpage > visiblepages // 2: firstvisible = currentpage - visiblepages // 2 # Initially set last page to first + visible lastvisible = firstvisible + visiblepages - 1 # If previous calculation made last page too high, calculate # first page by subtracting from the last if lastvisible > totalpages - 1: lastvisible = totalpages - 1 firstvisible = totalpages - visiblepages if totalpages <= visiblepages: firstvisible = 0 lastvisible = totalpages - 1 self.currentpage = currentpage for page in xrange(firstvisible, lastvisible + 1): self.append({'offset':page * limit, 'isselected':(page == currentpage), 'number':page * limit / limit + 1}) def render(self): return self Chula-0.7.0/chula/config.py0000644000175000017500000000475511370740256016473 0ustar jmcfarlanejmcfarlane""" Chula configuration class (restricted collection class) """ from chula import error, collection class Config(collection.RestrictedCollection): """ Chula configuration class. This class provides an organized structure to hold all supported chula configuration options. Most are optional, but a few are mandatory. Any attribute with a default value of: I{UNSET} must be set by your configuration. Failure to do so will result in an exception. """ @staticmethod def __validkeys__(): """ Initialize the supported configuration options with either a reasonable default, or I{UNSET}. """ return ('add_timer', 'classpath', 'construction_controller', 'construction_trigger', 'debug', 'error_controller', 'local', 'log', 'mapper', 'mqueue_db', 'mqueue_host', 'mqueue_port', 'session', 'session_db', 'session_encryption_key', 'session_host', 'session_max_stale_count', 'session_memcache', 'session_name', 'session_nosql', 'session_password', 'session_port', 'session_timeout', 'session_username', 'strict_method_resolution', ) def __defaults__(self): self.add_timer = True self.classpath = collection.UNSET self.construction_controller = None self.construction_trigger = None self.debug = True self.error_controller = collection.UNSET self.local = collection.Collection() self.log = '/tmp/chula.log' self.mapper = 'ClassPathMapper' self.mqueue_db = '/tmp/chula/mqueue' self.mqueue_host = 'localhost' self.mqueue_port = 8001 self.session_db = 'chula_session' self.session_encryption_key = 'chula-session-key' self.session_host = 'localhost' self.session_max_stale_count = 10 self.session_memcache = [('localhost:11211', 1)] self.session_name = 'chula-session' self.session_nosql = None self.session_password = 'chula' self.session_port = 5432 self.session_timeout = 30 self.session = True self.session_username = 'chula' self.strict_method_resolution = False Chula-0.7.0/chula/ecalendar.py0000644000175000017500000000160011370740256017126 0ustar jmcfarlanejmcfarlane""" Simple module to aid in working with calendars """ import calendar import datetime class Calendar(list): def __init__(self, year=None, month=None): """ Creates calendar @param year: Calendar year @type year: int @param month: Calendar month @type month: int @return: list """ w = 0 cal = [] if year is None or month is None: today = datetime.datetime.now() year = today.year month = today.month weeks = calendar.monthcalendar(year, month) for week in weeks: self.append([]) for day in week: if day > 0: day = datetime.datetime(year, month, day).date() self[w].append(day) else: self[w].append(None) w += 1 Chula-0.7.0/chula/session/0000755000175000017500000000000011412546122016315 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/session/backends/0000755000175000017500000000000011412546122020067 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/session/backends/couchdb.py0000644000175000017500000000461511370740256022065 0ustar jmcfarlanejmcfarlane"""Chula couchdb based session store""" import os from chula import data, logger from chula.nosql import couch from chula.session.backends import base class Backend(base.Backend): KEY = 'PICKLE' def __init__(self, config, guid): super(Backend, self).__init__(config, guid) self.server = self.config.session_nosql self.doc = None self.log = logger.Logger(config).logger('chula.session.couchdb') self.shard = None self.calculate_shard() self.connect() def connect(self): if not self.doc is None: return self.doc self.log.debug('Connecting with shard: %s' % self.shard) try: self.doc = SessionDoc(self.guid, server=self.server, shard=self.shard) return self.doc except Exception, ex: self.log.error('Unable to connect to the db: %s' % ex) return None def destroy(self): if self.doc is None: self.log.error('Unable to destroy(), no db connection') return False SessionDoc.delete(self.guid, server=self.server, shard=self.shard) return True def fetch_session(self): if self.doc == {}: self.log.debug('Document not found: %s' % self.guid) return None try: values = self.doc[self.KEY] self.log.debug('Session found: OK') return values except KeyError, ex: self.log.debug('Did not find session data in the document') except Exception, ex: self.log.error('Error fetching session: ex:%s' % ex) return None def gc(self): self.conn = None def persist(self, encoded): self.log.debug('persist() called') if self.doc is None: self.log.error('Unable to persist(), no db connection') return False self.doc[self.KEY] = encoded persisted = self.doc.persist() self.log.debug('saved guid:%s, revision: %s' % (self.guid, persisted)) return True def calculate_shard(self): if not self.shard is None: return self.shard date = data.str2date(self.guid.split('.')[0]) self.shard = os.path.join(str(date.year), str(date.month)) return self.shard class SessionDoc(couch.Document): DB = 'chula/session' Chula-0.7.0/chula/session/backends/memcached.py0000644000175000017500000000336611370740256022366 0ustar jmcfarlanejmcfarlane"""Chula memcached based session store""" import hashlib from chula import logger from chula.session.backends import base from chula.vendor import memcache class Backend(base.Backend): def __init__(self, config, guid): super(Backend, self).__init__(config, guid) self.connect() self.calculate_key() self.log = logger.Logger(config).logger('chula.session.memcached') def gc(self): try: self.conn.disconnect_all() except: pass finally: self.conn = None def connect(self): if not isinstance(self.conn, memcache.Client): self.conn = memcache.Client(self.config.session_memcache, debug=0) def destroy(self): self.connect() if not self.conn is None: return self.conn.delete(self.key) return False def fetch_session(self): self.connect() values = self.conn.get(self.key) if values is None: self.log.debug('Did not find session, guid: %s' % self.guid) return None else: self.log.debug('Session found: OK') return values def calculate_key(self): """ Hash the key to avoid character escaping and the >255 character limitation of cache keys. @return: SHA1 hash """ self.key = hashlib.sha1('session:%s' % self.guid).hexdigest() return self.key def persist(self, encoded): self.connect() timeout = self.config.session_timeout * 60 if isinstance(self.conn, memcache.Client): # Non zero status is success if self.conn.set(self.key, encoded, timeout) != 0: return True return False Chula-0.7.0/chula/session/backends/base.py0000644000175000017500000000217311370740256021365 0ustar jmcfarlanejmcfarlane"""Session backend abstract class""" class Backend(dict): def __init__(self, config, guid): self.config = config self.guid = guid self.conn = None def connect(self): """ Obtain a connection to the backend """ raise NotImplementedError def destroy(self): """ Destroy user session @param guid: Session guid @type guid: chula.guid.guid() @return: bool """ raise NotImplementedError def fetch_session(self): """ Fetch a user's session from the backend @param guid: Session guid @type guid: chula.guid.guid() @return: dict, or None """ raise NotImplementedError def gc(self): """ Clean up datastore connection """ raise NotImplementedError def persist(self, encoded): """ Persist session @param guid: Session guid @type guid: chula.guid.guid() @param encoded: Data to persist @type encoded: str @return: bool """ raise NotImplementedError Chula-0.7.0/chula/session/backends/__init__.py0000644000175000017500000000000011370740256022175 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/session/backends/postgresql.py0000644000175000017500000000534411370740256022661 0ustar jmcfarlanejmcfarlane"""Chula postgresql based session store""" from chula import db, logger from chula.db.datastore import DataStoreFactory from chula.session.backends import base class Backend(base.Backend): def __init__(self, config, guid): super(Backend, self).__init__(config, guid) self.cursor = None self.log = logger.Logger(config).logger('chula.session.postgresql') self.connect() def connect(self): if self.conn is None: uri = 'pg:%(session_username)s@%(session_host)s/%(session_db)s' uri = uri % self.config try: self.conn = DataStoreFactory(uri, self.config.session_password) except Exception, ex: self.log.error('Unable to connect to postgresql: %s' % ex) try: self.cursor = self.conn.cursor() except Exception, ex: self.log.error('Unable to create postgresql cursor: %s' % ex) if not self.conn is None and not self.cursor is None: self.log.debug('Successfull connection to postgresql') def destroy(self): sql = "DELETE FROM SESSION WHERE guid = %s;" % db.cstr(self.guid) try: self.cursor.execute(sql) self.conn.commit() return True except: self.conn.rollback() raise return False def fetch_session(self): self.log.debug('fetching data from postgresql') sql = "SELECT values FROM session WHERE guid = %s AND active = TRUE;" sql = sql % db.cstr(self.guid) row = None try: self.cursor.execute(sql) row = self.cursor.fetchone() except Exception, ex: #self.conn.error.OperationalError, ex: self.log.warning('SQL error guid: %s, ex:%s' % (self.guid, ex)) return None if row is None: self.log.debug('No active session, guid: %s' % self.guid) return None else: self.log.debug('Session found: OK') return row['values'] def gc(self): try: self.conn.close() except: pass finally: self.conn = None def persist(self, encoded): self.log.debug('persist() called') sql = "SELECT session_set(%s, %s, TRUE);" sql = sql % (db.cstr(self.guid), db.cstr(encoded)) # Attempt the persist try: self.cursor.execute(sql) self.conn.commit() self.log.debug('Persisted: OK') return True except Exception, ex: try: self.conn.rollback() except: pass # If we get here - persist failed return False Chula-0.7.0/chula/session/session.py0000644000175000017500000001672411370740256020373 0ustar jmcfarlanejmcfarlane"""Chula session factory""" import cPickle from chula import json, logger from chula.session.backends import memcached CPICKLE = 'cPickle' JSON = 'json' STALE_COUNT = 'REQUESTS-BETWEEN-DB-PERSIST' SESSION_UNAVAILABLE = 'SESSION_IS_CURRENTLY_UNAVAILABLE' class Session(dict): """ The Session class keeps track of user session. """ def __init__(self, config, existing_guid=None, transport=CPICKLE): """ Create a user session object @param config: Application configuration @type config: Instance of chula.config object @param existing_guid: Used to attach to an existing user's session @type existing_guid: chula.guid.guid() @param transport: Storage format for session data @type transport: str ('cPickle' or 'json') """ # Create member variables self._config = config self._expired = False self._log = logger.Logger(config).logger('chula.session') self._max_stale_count = config.session_max_stale_count self._persist_immediately = False self._timeout = config.session_timeout self._transport = transport # Establish a guid if existing_guid is None: self._guid = guid.guid() else: self._guid = existing_guid # Determine the backend to use if not self._config.session_nosql is None: from chula.session.backends import couchdb self._backend = couchdb.Backend(self._config, self._guid) else: from chula.session.backends import postgresql self._backend = postgresql.Backend(self._config, self._guid) # Always use a memcached backend self._cache = memcached.Backend(self._config, self._guid) # Set global session defaults self.isauthenticated = False # Retrieve session self.load() def __getattr__(self, key): """ Support attribute style accessor """ return self.get(key, None) def __setattr__(self, key, value): """ Support attribute style mutator """ if key.startswith('_'): self.__dict__[key] = value else: self[key] = value def _gc(self): """ Clean up anything related to a user's session, which includes backend connections. """ self._log.debug('session._gc() called') self._backend.gc() self._cache.gc() def decode(self, data): """ Decode the session using the specified transport (cPickle by default). @param data: Session data to be decoded @type data: str @return: Dict """ # Detect already decoded data if data is None: return None # TODO: Is this the right thing to do? elif isinstance(data, dict): return data # Decode using the desired transport if self._transport == CPICKLE: return cPickle.loads(data) else: return json.decode(data) def destroy(self): """ Expire a user's session now. This does persist to the database and cache immediately. """ self._backend.destroy() self._cache.destroy() # Ensure the data still in memory (self) is not persisted back self._expired = True self.isauthenticated = False def encode(self, data): """ Encode the session using the specified transport (cPickle by default). @param data: Session data to be encoded @type data: Instance of chula.session.Session object @return: str """ if self._transport == CPICKLE: # Since cPickle actually pickles the entire object we need # to exclude all of the private variables prior to encoding: return cPickle.dumps(dict(self)) else: return json.encode(data) def load(self): """ Fetch session data from cache first, then fall back to the backend if needed. """ self._log.debug('Fetching session, guid:%s' % self._guid) data = None # Fetch session from cache first if not self._cache is None: data = self.decode(self._cache.fetch_session()) # If the cache is unavailable fetch from the db and be sure to # persist to the database as we can't trust the cache currently if data is None: self._log.debug('`--> stale_count: %s' % self.get(STALE_COUNT)) data = self.decode(self._backend.fetch_session()) if data is None: msg = 'Active session not found in cache or backend, guid:%s' self._log.debug(msg % self._guid) # Either the backends are unavailable, or this is a brand # new session, either way persist asap self.flush_next_persist() else: self.update(data) self._log.debug('User session now loaded with the following k/v pairs:') for key, value in self.iteritems(): self._log.debug('`--> %s: %s' % (key, value)) def flush_next_persist(self): """ Persisting to the database does not occur on every request. Calling this method forces the very next persist() to force a write to the database. Use this when important session data changes and you don't want to risk it being lost. """ self._log.debug('flush_next_persist called') self._persist_immediately = True def persist(self): """ Stores session data for later retrieval Makes decisions on whether to store long-term or short-term Currently long-term is a postgres db, short-term is cache. """ # Don't do anthing if this session is expired if self._expired: return # Set and increment the stale count. If unset, it's inital # value is set to -2, then incremented so it winds up being # set to -1 if initially unset. A value of -1 results in the # first commit of session hitting the cache, the second hits # the db - after that it's based on the max_stale_count. self[STALE_COUNT] = self.get(STALE_COUNT, -2) + 1 self._log.debug('stale_count in persist(): %s' % self[STALE_COUNT]) # Persist the session state to the database if this is a new # session (the STALE_COUNT won't be set) or the age (requests # between database persists) is greater than a constant value, # 10 for now. if self[STALE_COUNT] == 0 or self[STALE_COUNT] > self._max_stale_count: self._log.debug('decision made to call flush_next_persist') self.flush_next_persist() # Save the stale count in case the backend persist fails, then # mark the session as no longer stale (restore if persist # fails) current_stale_count = self[STALE_COUNT] self[STALE_COUNT] = 0 # Create encoded session using the desired transport encoded = self.encode(self) # Persist to the backend if needed if self._persist_immediately: if self._backend.persist(encoded): self._persist_immediately = False else: self[STALE_COUNT] = current_stale_count self.flush_next_persist() # Always persist to cache persisted = self._cache.persist(encoded) Chula-0.7.0/chula/session/__init__.py0000644000175000017500000000005211370740256020432 0ustar jmcfarlanejmcfarlanefrom chula.session.session import Session Chula-0.7.0/chula/passwd.py0000644000175000017500000000315511370740256016520 0ustar jmcfarlanejmcfarlane""" Generate and validate passwords """ import hashlib from random import randrange from chula import error, regex lock = 'chula-salt' SALT_LENGTH = 6 def hash(password, salt=None, pattern=regex.PASSWD): """ Generate a password hash @param password: User's password @type password: String @param salt: Salt for use with generating an existing hash @type salt: String @param pattern: Regex used to validate the password @type pattern: String (valid regex) @return: String >>> from chula import passwd >>> hashed = passwd.hash('mypassword') >>> len(hashed) == 46 True """ if not regex.match(pattern, password): raise error.MalformedPasswordError if salt is None: def generate_salt(): for i in xrange(SALT_LENGTH): yield chr(randrange(65, 122)) salt = ''.join(generate_salt()) return salt + hashlib.sha1(salt + password + lock).hexdigest() def matches(password, known_hash): """ Checks password to see if it matches the actual password @param password: The user provided password I{(to be validated)} @type password: String @param known_hash: Known good hash for the requested password @type known_hash: String @return: bool >>> from chula import passwd >>> user_input = 'mypassword' >>> pass_from_db = '^Bo\\Jcc16b5dae81478c1ab4655dd69df121e87ea43a2f' >>> passwd.matches(user_input, pass_from_db) True >>> passwd.matches('guessing', pass_from_db) False """ salt = known_hash[:SALT_LENGTH] return hash(password, salt) == known_hash Chula-0.7.0/chula/cache.py0000644000175000017500000000377711370740256016274 0ustar jmcfarlanejmcfarlane""" Wrapper class for the Memcache client """ from chula import error from chula.vendor import memcache ENCODING = 'ascii' SANITIZE = False class Cache(object): def __init__(self, servers): self.servers = servers self.cache = memcache.Client(self.servers, debug=0) @staticmethod def clean_key(key, sanitize=SANITIZE): if not isinstance(key, basestring): msg = 'Cache keys must be of type: str' raise error.InvalidCacheKeyError(msg) key = list(key) for char in key: if ord(char) < 33 or ord(char) == 127: if sanitize: key.remove(char) else: msg = "Memcache doesn't support ORD < 33 or == 127" raise error.InvalidCacheKeyError(msg) key = ''.join(key) if len(key) > memcache.SERVER_MAX_KEY_LENGTH: msg = 'Key must be <= %s chars' % memcache.SERVER_MAX_KEY_LENGTH raise error.InvalidCacheKeyError(msg) else: return key.encode(ENCODING) def close(self): self.cache.disconnect_all() def delete(self, key): deleted = self.cache.delete(self.clean_key(key)) # Non zero status is success if deleted != 0: return True else: return False def get(self, key): value = self.cache.get(self.clean_key(key)) return value def purge(self, key): return self.delete(self.clean_key(key)) def set(self, key, value, minutes=1): key = self.clean_key(key) saved = self.cache.set(key, value, round(minutes * 60)) # Non zero status is success if saved != 0: return True else: return False def stats(self): servers = [] stats = self.cache.get_stats() for server in stats: conn = server[0] attrs = server[1] attrs['conn'] = conn servers.append(attrs) return servers Chula-0.7.0/chula/mail.py0000644000175000017500000000311311370740256016133 0ustar jmcfarlanejmcfarlane""" Simple utility class to send emails """ import smtplib import socket from chula import data, regex class Mail(object): def __init__(self, server): """ Set the required attributes """ self.body = None self.from_addy = None self.reply_to_addy = None self.server = server self.subject = None self.to_addy = None def _validate_encoding(self): self.body = data.str2unicode(self.body) self.from_addy = data.str2unicode(self.from_addy) self.reply_to_addy = data.str2unicode(self.reply_to_addy) self.server = data.str2unicode(self.server) self.subject = data.str2unicode(self.subject) self.to_addy = data.str2unicode(self.to_addy) def send(self): """ Send the requested mail using smtplib """ if not regex.match(regex.EMAIL, str(self.reply_to_addy)): self.reply_to_addy = self.from_addy self._validate_encoding() headers = [] headers.append('From: %s' % self.from_addy) headers.append('Reply-To: %s' % self.reply_to_addy) headers.append('To: %s' % ','.join(self.to_addy.split())) headers.append('Subject: %s' % self.subject) headers.append('\n') try: server = smtplib.SMTP(self.server) server.set_debuglevel(0) server.sendmail(self.from_addy, self.to_addy.split(), '\n'.join(headers) + self.body) server.quit() except socket.gaierror: raise Chula-0.7.0/chula/logger.py0000644000175000017500000000437211370740256016500 0ustar jmcfarlanejmcfarlane"""Chula logger class""" import logging from logging.handlers import RotatingFileHandler from chula.config import Config from chula.singleton import singleton ROOT = 'chula' @singleton class Logger(object): def __init__(self, config=None): if config is None: config = Config() # Create a logger instance. NOTE: the level set in the logger # determines which severity of messages it will pass to it's # handlers. We want to send everything to the handlers and # let them decide what to do. logger = logging.getLogger('') logger.setLevel(logging.DEBUG) # Create file handler for WARNING and above if not config.log is None: fmt = ('%(asctime)s,' '%(levelname)s,' 'pid:%(process)d,' '%(name)s,' '%(filename)s:%(lineno)d,' '%(message)s' ) fh = RotatingFileHandler(config.log, maxBytes=104857600, backupCount=5) fh.setLevel(logging.WARNING) fh.setFormatter(logging.Formatter(fmt)) logger.addHandler(fh) # Create console handler for DEBUG and above (stderr) if config.debug: fmt = ('%(levelname)-9s' '%(name)-35s' '%(filename)-15s' '%(lineno)-5d' '%(message)s' ) ch = logging.StreamHandler() ch.setLevel(logging.DEBUG) ch.setFormatter(logging.Formatter(fmt)) logger.addHandler(ch) def logger(self, name=ROOT): if not name.startswith(ROOT): name = '%s.%s' % (ROOT, name) return logging.getLogger(name) if __name__ == '__main__': from chula import config config = config.Config() logger = Logger(config).logger() foo = Logger(config).logger('foo') bar = Logger(config).logger('chula.bar') logger.critical('critical msg') logger.debug('debug msg') logger.error('error msg') logger.info('info msg') logger.warning('warning msg') foo.warning('warning msg') bar.warning('warning msg') foo.info('info msg') Chula-0.7.0/chula/system.py0000644000175000017500000000210611370740256016536 0ustar jmcfarlanejmcfarlane""" Module to return operating system information TODO: This module has not been tested on win32 at all """ from __future__ import with_statement import os LINUX = 'LINUX' WIN32 = 'WIN32' SUPPORTED = (LINUX, WIN32) class System(object): def __init__(self): self.type = self.fetch_os() self.procs = getattr(self, 'fetch_procs_' + self.type.lower())() self.arch = self.fetch_arch() def fetch_arch(self): return os.uname()[4] def fetch_os(self): uname = os.uname()[0].upper() for name in SUPPORTED: if name == uname: return name return 'UNKONWN' def fetch_procs_linux(self): procs = 0 with open('/proc/cpuinfo', 'r') as cpuinfo: for line in cpuinfo: if line.startswith('processor'): procs += 1 return procs def fetch_procs_win32(self): key = 'NUMBER_OF_PROCESSORS' if key in os.environ: return int(os.environ[key]) def fetch_procs_unkonwn(self): return None Chula-0.7.0/chula/example.py0000644000175000017500000000217611370740256016654 0ustar jmcfarlanejmcfarlane""" Example python module for use with illustrating the example unit test(s) """ class Example(object): def __init__(self): """ Example constructor """ self.name = 'Example attribute' def sum(self, a, b): """ Sum two values @param a: Value to be summed with b @type a: integer @param b: Vale to be summed with a @type b: integer @return: integer """ try: return a + b except ValueError: raise def awesome(self): """ Describe the nature of all things related to Gentoo Linux @return: boolean >>> from chula import example >>> gentoo = example.Example() >>> if gentoo.awesome(): ... print 'Gentoo is awesome!' Gentoo is awesome! >>> git = example.Example() >>> if git.awesome(): ... print 'Git is also awesome!' Git is also awesome! >>> distro = example.Example() >>> 'Fedora' is distro.awesome() False """ return True def something(): return [] Chula-0.7.0/chula/testutils.py0000644000175000017500000001227311370740256017260 0ustar jmcfarlanejmcfarlane"""Module for working with unit tests""" import doctest import imp import os import re import sys import unittest RE_TEST_MODULE = re.compile(r'.*test_[a-zA-Z]+[-a-zA-Z0-9_]+\.py$') RE_TEST_CLASS = re.compile(r'Test_[a-zA-Z_]+') DOCTEST = 'doctest' class TestFinder(set): """ Class to find and run unit tests. When looking for tests it uses the following regular expressions to match modules with the pattern I{Test_class} and it will load doctests if I{Test_class.doctest} exists and is set to a module. Example usage: >>> from chula import testutils >>> tests = testutils.TestFinder('/path/to/project') >>> tests.run() """ def __init__(self, location): """ @param location: A filesystem path, or collection of paths @type location: str, list, or tuple @return: set """ super(TestFinder, self).__init__() self.search(location) self.build_suite() def search(self, location): """ Search the passed path(s) and fill the set with the path to each test module. @param location: A filesystem path, or collection of paths @type location: str, list, or tuple @return: None """ if isinstance(location, (list, tuple)): for node in location: self.search_node(node) else: self.search_node(location) def search_node(self, node): """ Process a file or directory and import tests @param node: A filesystem directory or file @type node: str @return: None """ if os.path.exists(node): if os.path.isdir(node): self.search_directory(node) else: self.add_file(node) def search_directory(self, directory): """ Import all of the tests in a directory @param directory: A directory on the filesystem @type directory: str @return: None """ for root, dirs, files in os.walk(directory): for file in files: path = os.path.join(root, file) self.add_file(path) def add_file(self, file): """ Add a file to the unique list (set) of files that contain tests. @param file: A file on the filesystem @type file: str @return: None """ if not RE_TEST_MODULE.match(file) is None: self.add(file) def build_suite(self): """ Combine all the tests into a single unittest suite """ cwd = os.getcwd() self.suite = unittest.TestSuite() sys.path.insert(0, None) for test in self: dir_name = os.path.dirname(test) module_name = os.path.basename(test)[:-3] # Add the module to the front of the python path sys.path[0] = dir_name # Generate a fully unique [module] name, but try to avoid # including duplicate long strings on the front. For # example, we don't want: # _home_username_path_to_repo_tests if dir_name.startswith(cwd): dir_name = dir_name.rsplit(cwd)[1] # Don't start with a slash either if dir_name.startswith(os.sep): dir_name = dir_name[1:] # Finally generate the uniq [module] name u_name = os.path.join(dir_name, module_name).replace(os.sep, '_') try: # Import the module using imp rather than __import__ # as imp allows us to specify the name of the module. # This allows us to have tests with the same name in # different directories - and avoid namespace clashes. f, filename, descr = imp.find_module(module_name) module = imp.load_module(u_name, f, filename, descr) except ImportError, ex: print 'Unable to import', test raise for tests in self.extract_tests(module): self.suite.addTests(tests) sys.path.pop(0) def extract_tests(self, module): """ Import all of the tests in a file by looking for classes that match a Test_abc pattern. If the test class has a I{doctest} attribute that holds a module, the corresponding doctests wil be added as well. @param module: Module containing unit tests @type module: module @return: iterator """ for obj in dir(module): if not RE_TEST_CLASS.match(obj) is None: # Load the class as a test suite unbound_class = getattr(module, obj) tests = unittest.makeSuite(unbound_class) # Add doctest testing if the test suite supports it doctests = getattr(unbound_class, DOCTEST, None) if not doctests is None: tests.addTest(doctest.DocTestSuite(doctests)) yield tests def run(self, verbosity=2): """ Run the unit tests found, with a default verbosity of 2. """ return unittest.TextTestRunner(verbosity=verbosity).run(self.suite) Chula-0.7.0/chula/guid.py0000644000175000017500000000066211370740256016147 0ustar jmcfarlanejmcfarlanefrom random import randrange import time # Save cost of the function lookup _now = time.time def guid(): """ Generate a random guid 64 characters in length @return: String """ def builder(): now = '%16f-' % _now() max = 64 - len(now) yield now for i in xrange(max): # 65=A 91=Z 97=a 122=z yield chr(randrange(65, 91)) return ''.join(builder()) Chula-0.7.0/chula/vendor/0000755000175000017500000000000011412546122016127 5ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/vendor/fcgi.py0000644000175000017500000012612111370740256017423 0ustar jmcfarlanejmcfarlane# Copyright (c) 2002, 2003, 2005, 2006 Allan Saddi # 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. # # 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. # # $Id$ """ fcgi - a FastCGI/WSGI gateway. For more information about FastCGI, see . For more information about the Web Server Gateway Interface, see . Example usage: #!/usr/bin/env python from myapplication import app # Assume app is your WSGI application object from fcgi import WSGIServer WSGIServer(app).run() See the documentation for WSGIServer/Server for more information. On most platforms, fcgi will fallback to regular CGI behavior if run in a non-FastCGI context. If you want to force CGI behavior, set the environment variable FCGI_FORCE_CGI to "Y" or "y". """ __author__ = 'Allan Saddi ' __version__ = '$Revision$' import sys import os import signal import struct import cStringIO as StringIO import select import socket import errno import traceback try: import thread import threading thread_available = True except ImportError: import dummy_thread as thread import dummy_threading as threading thread_available = False # Apparently 2.3 doesn't define SHUT_WR? Assume it is 1 in this case. if not hasattr(socket, 'SHUT_WR'): socket.SHUT_WR = 1 __all__ = ['WSGIServer'] # Constants from the spec. FCGI_LISTENSOCK_FILENO = 0 FCGI_HEADER_LEN = 8 FCGI_VERSION_1 = 1 FCGI_BEGIN_REQUEST = 1 FCGI_ABORT_REQUEST = 2 FCGI_END_REQUEST = 3 FCGI_PARAMS = 4 FCGI_STDIN = 5 FCGI_STDOUT = 6 FCGI_STDERR = 7 FCGI_DATA = 8 FCGI_GET_VALUES = 9 FCGI_GET_VALUES_RESULT = 10 FCGI_UNKNOWN_TYPE = 11 FCGI_MAXTYPE = FCGI_UNKNOWN_TYPE FCGI_NULL_REQUEST_ID = 0 FCGI_KEEP_CONN = 1 FCGI_RESPONDER = 1 FCGI_AUTHORIZER = 2 FCGI_FILTER = 3 FCGI_REQUEST_COMPLETE = 0 FCGI_CANT_MPX_CONN = 1 FCGI_OVERLOADED = 2 FCGI_UNKNOWN_ROLE = 3 FCGI_MAX_CONNS = 'FCGI_MAX_CONNS' FCGI_MAX_REQS = 'FCGI_MAX_REQS' FCGI_MPXS_CONNS = 'FCGI_MPXS_CONNS' FCGI_Header = '!BBHHBx' FCGI_BeginRequestBody = '!HB5x' FCGI_EndRequestBody = '!LB3x' FCGI_UnknownTypeBody = '!B7x' FCGI_EndRequestBody_LEN = struct.calcsize(FCGI_EndRequestBody) FCGI_UnknownTypeBody_LEN = struct.calcsize(FCGI_UnknownTypeBody) if __debug__: import time # Set non-zero to write debug output to a file. DEBUG = 0 DEBUGLOG = '/tmp/fcgi.log' def _debug(level, msg): if DEBUG < level: return try: f = open(DEBUGLOG, 'a') f.write('%sfcgi: %s\n' % (time.ctime()[4:-4], msg)) f.close() except: pass class InputStream(object): """ File-like object representing FastCGI input streams (FCGI_STDIN and FCGI_DATA). Supports the minimum methods required by WSGI spec. """ def __init__(self, conn): self._conn = conn # See Server. self._shrinkThreshold = conn.server.inputStreamShrinkThreshold self._buf = '' self._bufList = [] self._pos = 0 # Current read position. self._avail = 0 # Number of bytes currently available. self._eof = False # True when server has sent EOF notification. def _shrinkBuffer(self): """Gets rid of already read data (since we can't rewind).""" if self._pos >= self._shrinkThreshold: self._buf = self._buf[self._pos:] self._avail -= self._pos self._pos = 0 assert self._avail >= 0 def _waitForData(self): """Waits for more data to become available.""" self._conn.process_input() def read(self, n=-1): if self._pos == self._avail and self._eof: return '' while True: if n < 0 or (self._avail - self._pos) < n: # Not enough data available. if self._eof: # And there's no more coming. newPos = self._avail break else: # Wait for more data. self._waitForData() continue else: newPos = self._pos + n break # Merge buffer list, if necessary. if self._bufList: self._buf += ''.join(self._bufList) self._bufList = [] r = self._buf[self._pos:newPos] self._pos = newPos self._shrinkBuffer() return r def readline(self, length=None): if self._pos == self._avail and self._eof: return '' while True: # Unfortunately, we need to merge the buffer list early. if self._bufList: self._buf += ''.join(self._bufList) self._bufList = [] # Find newline. i = self._buf.find('\n', self._pos) if i < 0: # Not found? if self._eof: # No more data coming. newPos = self._avail break else: # Wait for more to come. self._waitForData() continue else: newPos = i + 1 break if length is not None: if self._pos + length < newPos: newPos = self._pos + length r = self._buf[self._pos:newPos] self._pos = newPos self._shrinkBuffer() return r def readlines(self, sizehint=0): total = 0 lines = [] line = self.readline() while line: lines.append(line) total += len(line) if 0 < sizehint <= total: break line = self.readline() return lines def __iter__(self): return self def next(self): r = self.readline() if not r: raise StopIteration return r def add_data(self, data): if not data: self._eof = True else: self._bufList.append(data) self._avail += len(data) class MultiplexedInputStream(InputStream): """ A version of InputStream meant to be used with MultiplexedConnections. Assumes the MultiplexedConnection (the producer) and the Request (the consumer) are running in different threads. """ def __init__(self, conn): super(MultiplexedInputStream, self).__init__(conn) # Arbitrates access to this InputStream (it's used simultaneously # by a Request and its owning Connection object). lock = threading.RLock() # Notifies Request thread that there is new data available. self._lock = threading.Condition(lock) def _waitForData(self): # Wait for notification from add_data(). self._lock.wait() def read(self, n=-1): self._lock.acquire() try: return super(MultiplexedInputStream, self).read(n) finally: self._lock.release() def readline(self, length=None): self._lock.acquire() try: return super(MultiplexedInputStream, self).readline(length) finally: self._lock.release() def add_data(self, data): self._lock.acquire() try: super(MultiplexedInputStream, self).add_data(data) self._lock.notify() finally: self._lock.release() class OutputStream(object): """ FastCGI output stream (FCGI_STDOUT/FCGI_STDERR). By default, calls to write() or writelines() immediately result in Records being sent back to the server. Buffering should be done in a higher level! """ def __init__(self, conn, req, type, buffered=False): self._conn = conn self._req = req self._type = type self._buffered = buffered self._bufList = [] # Used if buffered is True self.dataWritten = False self.closed = False def _write(self, data): length = len(data) while length: toWrite = min(length, self._req.server.maxwrite - FCGI_HEADER_LEN) rec = Record(self._type, self._req.requestId) rec.contentLength = toWrite rec.contentData = data[:toWrite] self._conn.writeRecord(rec) data = data[toWrite:] length -= toWrite def write(self, data): assert not self.closed if not data: return self.dataWritten = True if self._buffered: self._bufList.append(data) else: self._write(data) def writelines(self, lines): assert not self.closed for line in lines: self.write(line) def flush(self): # Only need to flush if this OutputStream is actually buffered. if self._buffered: data = ''.join(self._bufList) self._bufList = [] self._write(data) # Though available, the following should NOT be called by WSGI apps. def close(self): """Sends end-of-stream notification, if necessary.""" if not self.closed and self.dataWritten: self.flush() rec = Record(self._type, self._req.requestId) self._conn.writeRecord(rec) self.closed = True class TeeOutputStream(object): """ Simple wrapper around two or more output file-like objects that copies written data to all streams. """ def __init__(self, streamList): self._streamList = streamList def write(self, data): for f in self._streamList: f.write(data) def writelines(self, lines): for line in lines: self.write(line) def flush(self): for f in self._streamList: f.flush() class StdoutWrapper(object): """ Wrapper for sys.stdout so we know if data has actually been written. """ def __init__(self, stdout): self._file = stdout self.dataWritten = False def write(self, data): if data: self.dataWritten = True self._file.write(data) def writelines(self, lines): for line in lines: self.write(line) def __getattr__(self, name): return getattr(self._file, name) def decode_pair(s, pos=0): """ Decodes a name/value pair. The number of bytes decoded as well as the name/value pair are returned. """ nameLength = ord(s[pos]) if nameLength & 128: nameLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff pos += 4 else: pos += 1 valueLength = ord(s[pos]) if valueLength & 128: valueLength = struct.unpack('!L', s[pos:pos+4])[0] & 0x7fffffff pos += 4 else: pos += 1 name = s[pos:pos+nameLength] pos += nameLength value = s[pos:pos+valueLength] pos += valueLength return (pos, (name, value)) def encode_pair(name, value): """ Encodes a name/value pair. The encoded string is returned. """ nameLength = len(name) if nameLength < 128: s = chr(nameLength) else: s = struct.pack('!L', nameLength | 0x80000000L) valueLength = len(value) if valueLength < 128: s += chr(valueLength) else: s += struct.pack('!L', valueLength | 0x80000000L) return s + name + value class Record(object): """ A FastCGI Record. Used for encoding/decoding records. """ def __init__(self, type=FCGI_UNKNOWN_TYPE, requestId=FCGI_NULL_REQUEST_ID): self.version = FCGI_VERSION_1 self.type = type self.requestId = requestId self.contentLength = 0 self.paddingLength = 0 self.contentData = '' def _recvall(sock, length): """ Attempts to receive length bytes from a socket, blocking if necessary. (Socket may be blocking or non-blocking.) """ dataList = [] recvLen = 0 while length: try: data = sock.recv(length) except socket.error, e: if e[0] == errno.EAGAIN: select.select([sock], [], []) continue else: raise if not data: # EOF break dataList.append(data) dataLen = len(data) recvLen += dataLen length -= dataLen return ''.join(dataList), recvLen _recvall = staticmethod(_recvall) def read(self, sock): """Read and decode a Record from a socket.""" try: header, length = self._recvall(sock, FCGI_HEADER_LEN) except: raise EOFError if length < FCGI_HEADER_LEN: raise EOFError self.version, self.type, self.requestId, self.contentLength, \ self.paddingLength = struct.unpack(FCGI_Header, header) if __debug__: _debug(9, 'read: fd = %d, type = %d, requestId = %d, ' 'contentLength = %d' % (sock.fileno(), self.type, self.requestId, self.contentLength)) if self.contentLength: try: self.contentData, length = self._recvall(sock, self.contentLength) except: raise EOFError if length < self.contentLength: raise EOFError if self.paddingLength: try: self._recvall(sock, self.paddingLength) except: raise EOFError def _sendall(sock, data): """ Writes data to a socket and does not return until all the data is sent. """ length = len(data) while length: try: sent = sock.send(data) except socket.error, e: if e[0] == errno.EAGAIN: select.select([], [sock], []) continue else: raise data = data[sent:] length -= sent _sendall = staticmethod(_sendall) def write(self, sock): """Encode and write a Record to a socket.""" self.paddingLength = -self.contentLength & 7 if __debug__: _debug(9, 'write: fd = %d, type = %d, requestId = %d, ' 'contentLength = %d' % (sock.fileno(), self.type, self.requestId, self.contentLength)) header = struct.pack(FCGI_Header, self.version, self.type, self.requestId, self.contentLength, self.paddingLength) self._sendall(sock, header) if self.contentLength: self._sendall(sock, self.contentData) if self.paddingLength: self._sendall(sock, '\x00'*self.paddingLength) class Request(object): """ Represents a single FastCGI request. These objects are passed to your handler and is the main interface between your handler and the fcgi module. The methods should not be called by your handler. However, server, params, stdin, stdout, stderr, and data are free for your handler's use. """ def __init__(self, conn, inputStreamClass): self._conn = conn self.server = conn.server self.params = {} self.stdin = inputStreamClass(conn) self.stdout = OutputStream(conn, self, FCGI_STDOUT) self.stderr = OutputStream(conn, self, FCGI_STDERR, buffered=True) self.data = inputStreamClass(conn) def run(self): """Runs the handler, flushes the streams, and ends the request.""" try: protocolStatus, appStatus = self.server.handler(self) except: traceback.print_exc(file=self.stderr) self.stderr.flush() if not self.stdout.dataWritten: self.server.error(self) protocolStatus, appStatus = FCGI_REQUEST_COMPLETE, 0 if __debug__: _debug(1, 'protocolStatus = %d, appStatus = %d' % (protocolStatus, appStatus)) self._flush() self._end(appStatus, protocolStatus) def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE): self._conn.end_request(self, appStatus, protocolStatus) def _flush(self): self.stdout.close() self.stderr.close() class CGIRequest(Request): """A normal CGI request disguised as a FastCGI request.""" def __init__(self, server): # These are normally filled in by Connection. self.requestId = 1 self.role = FCGI_RESPONDER self.flags = 0 self.aborted = False self.server = server self.params = dict(os.environ) self.stdin = sys.stdin self.stdout = StdoutWrapper(sys.stdout) # Oh, the humanity! self.stderr = sys.stderr self.data = StringIO.StringIO() def _end(self, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE): sys.exit(appStatus) def _flush(self): # Not buffered, do nothing. pass class Connection(object): """ A Connection with the web server. Each Connection is associated with a single socket (which is connected to the web server) and is responsible for handling all the FastCGI message processing for that socket. """ _multiplexed = False _inputStreamClass = InputStream def __init__(self, sock, addr, server): self._sock = sock self._addr = addr self.server = server # Active Requests for this Connection, mapped by request ID. self._requests = {} def _cleanupSocket(self): """Close the Connection's socket.""" try: self._sock.shutdown(socket.SHUT_WR) except: return try: while True: r, w, e = select.select([self._sock], [], []) if not r or not self._sock.recv(1024): break except: pass self._sock.close() def run(self): """Begin processing data from the socket.""" self._keepGoing = True while self._keepGoing: try: self.process_input() except EOFError: break except (select.error, socket.error), e: if e[0] == errno.EBADF: # Socket was closed by Request. break raise self._cleanupSocket() def process_input(self): """Attempt to read a single Record from the socket and process it.""" # Currently, any children Request threads notify this Connection # that it is no longer needed by closing the Connection's socket. # We need to put a timeout on select, otherwise we might get # stuck in it indefinitely... (I don't like this solution.) while self._keepGoing: try: r, w, e = select.select([self._sock], [], [], 1.0) except ValueError: # Sigh. ValueError gets thrown sometimes when passing select # a closed socket. raise EOFError if r: break if not self._keepGoing: return rec = Record() rec.read(self._sock) if rec.type == FCGI_GET_VALUES: self._do_get_values(rec) elif rec.type == FCGI_BEGIN_REQUEST: self._do_begin_request(rec) elif rec.type == FCGI_ABORT_REQUEST: self._do_abort_request(rec) elif rec.type == FCGI_PARAMS: self._do_params(rec) elif rec.type == FCGI_STDIN: self._do_stdin(rec) elif rec.type == FCGI_DATA: self._do_data(rec) elif rec.requestId == FCGI_NULL_REQUEST_ID: self._do_unknown_type(rec) else: # Need to complain about this. pass def writeRecord(self, rec): """ Write a Record to the socket. """ rec.write(self._sock) def end_request(self, req, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE, remove=True): """ End a Request. Called by Request objects. An FCGI_END_REQUEST Record is sent to the web server. If the web server no longer requires the connection, the socket is closed, thereby ending this Connection (run() returns). """ rec = Record(FCGI_END_REQUEST, req.requestId) rec.contentData = struct.pack(FCGI_EndRequestBody, appStatus, protocolStatus) rec.contentLength = FCGI_EndRequestBody_LEN self.writeRecord(rec) if remove: del self._requests[req.requestId] if __debug__: _debug(2, 'end_request: flags = %d' % req.flags) if not (req.flags & FCGI_KEEP_CONN) and not self._requests: self._cleanupSocket() self._keepGoing = False def _do_get_values(self, inrec): """Handle an FCGI_GET_VALUES request from the web server.""" outrec = Record(FCGI_GET_VALUES_RESULT) pos = 0 while pos < inrec.contentLength: pos, (name, value) = decode_pair(inrec.contentData, pos) cap = self.server.capability.get(name) if cap is not None: outrec.contentData += encode_pair(name, str(cap)) outrec.contentLength = len(outrec.contentData) self.writeRecord(outrec) def _do_begin_request(self, inrec): """Handle an FCGI_BEGIN_REQUEST from the web server.""" role, flags = struct.unpack(FCGI_BeginRequestBody, inrec.contentData) req = self.server.request_class(self, self._inputStreamClass) req.requestId, req.role, req.flags = inrec.requestId, role, flags req.aborted = False if not self._multiplexed and self._requests: # Can't multiplex requests. self.end_request(req, 0L, FCGI_CANT_MPX_CONN, remove=False) else: self._requests[inrec.requestId] = req def _do_abort_request(self, inrec): """ Handle an FCGI_ABORT_REQUEST from the web server. We just mark a flag in the associated Request. """ req = self._requests.get(inrec.requestId) if req is not None: req.aborted = True def _start_request(self, req): """Run the request.""" # Not multiplexed, so run it inline. req.run() def _do_params(self, inrec): """ Handle an FCGI_PARAMS Record. If the last FCGI_PARAMS Record is received, start the request. """ req = self._requests.get(inrec.requestId) if req is not None: if inrec.contentLength: pos = 0 while pos < inrec.contentLength: pos, (name, value) = decode_pair(inrec.contentData, pos) req.params[name] = value else: self._start_request(req) def _do_stdin(self, inrec): """Handle the FCGI_STDIN stream.""" req = self._requests.get(inrec.requestId) if req is not None: req.stdin.add_data(inrec.contentData) def _do_data(self, inrec): """Handle the FCGI_DATA stream.""" req = self._requests.get(inrec.requestId) if req is not None: req.data.add_data(inrec.contentData) def _do_unknown_type(self, inrec): """Handle an unknown request type. Respond accordingly.""" outrec = Record(FCGI_UNKNOWN_TYPE) outrec.contentData = struct.pack(FCGI_UnknownTypeBody, inrec.type) outrec.contentLength = FCGI_UnknownTypeBody_LEN self.writeRecord(rec) class MultiplexedConnection(Connection): """ A version of Connection capable of handling multiple requests simultaneously. """ _multiplexed = True _inputStreamClass = MultiplexedInputStream def __init__(self, sock, addr, server): super(MultiplexedConnection, self).__init__(sock, addr, server) # Used to arbitrate access to self._requests. lock = threading.RLock() # Notification is posted everytime a request completes, allowing us # to quit cleanly. self._lock = threading.Condition(lock) def _cleanupSocket(self): # Wait for any outstanding requests before closing the socket. self._lock.acquire() while self._requests: self._lock.wait() self._lock.release() super(MultiplexedConnection, self)._cleanupSocket() def writeRecord(self, rec): # Must use locking to prevent intermingling of Records from different # threads. self._lock.acquire() try: # Probably faster than calling super. ;) rec.write(self._sock) finally: self._lock.release() def end_request(self, req, appStatus=0L, protocolStatus=FCGI_REQUEST_COMPLETE, remove=True): self._lock.acquire() try: super(MultiplexedConnection, self).end_request(req, appStatus, protocolStatus, remove) self._lock.notify() finally: self._lock.release() def _do_begin_request(self, inrec): self._lock.acquire() try: super(MultiplexedConnection, self)._do_begin_request(inrec) finally: self._lock.release() def _do_abort_request(self, inrec): self._lock.acquire() try: super(MultiplexedConnection, self)._do_abort_request(inrec) finally: self._lock.release() def _start_request(self, req): thread.start_new_thread(req.run, ()) def _do_params(self, inrec): self._lock.acquire() try: super(MultiplexedConnection, self)._do_params(inrec) finally: self._lock.release() def _do_stdin(self, inrec): self._lock.acquire() try: super(MultiplexedConnection, self)._do_stdin(inrec) finally: self._lock.release() def _do_data(self, inrec): self._lock.acquire() try: super(MultiplexedConnection, self)._do_data(inrec) finally: self._lock.release() class Server(object): """ The FastCGI server. Waits for connections from the web server, processing each request. If run in a normal CGI context, it will instead instantiate a CGIRequest and run the handler through there. """ request_class = Request cgirequest_class = CGIRequest # Limits the size of the InputStream's string buffer to this size + the # server's maximum Record size. Since the InputStream is not seekable, # we throw away already-read data once this certain amount has been read. inputStreamShrinkThreshold = 102400 - 8192 def __init__(self, handler=None, maxwrite=8192, bindAddress=None, umask=None, multiplexed=False): """ handler, if present, must reference a function or method that takes one argument: a Request object. If handler is not specified at creation time, Server *must* be subclassed. (The handler method below is abstract.) maxwrite is the maximum number of bytes (per Record) to write to the server. I've noticed mod_fastcgi has a relatively small receive buffer (8K or so). bindAddress, if present, must either be a string or a 2-tuple. If present, run() will open its own listening socket. You would use this if you wanted to run your application as an 'external' FastCGI app. (i.e. the webserver would no longer be responsible for starting your app) If a string, it will be interpreted as a filename and a UNIX socket will be opened. If a tuple, the first element, a string, is the interface name/IP to bind to, and the second element (an int) is the port number. Set multiplexed to True if you want to handle multiple requests per connection. Some FastCGI backends (namely mod_fastcgi) don't multiplex requests at all, so by default this is off (which saves on thread creation/locking overhead). If threads aren't available, this keyword is ignored; it's not possible to multiplex requests at all. """ if handler is not None: self.handler = handler self.maxwrite = maxwrite if thread_available: try: import resource # Attempt to glean the maximum number of connections # from the OS. maxConns = resource.getrlimit(resource.RLIMIT_NOFILE)[0] except ImportError: maxConns = 100 # Just some made up number. maxReqs = maxConns if multiplexed: self._connectionClass = MultiplexedConnection maxReqs *= 5 # Another made up number. else: self._connectionClass = Connection self.capability = { FCGI_MAX_CONNS: maxConns, FCGI_MAX_REQS: maxReqs, FCGI_MPXS_CONNS: multiplexed and 1 or 0 } else: self._connectionClass = Connection self.capability = { # If threads aren't available, these are pretty much correct. FCGI_MAX_CONNS: 1, FCGI_MAX_REQS: 1, FCGI_MPXS_CONNS: 0 } self._bindAddress = bindAddress self._umask = umask def _setupSocket(self): if self._bindAddress is None: # Run as a normal FastCGI? isFCGI = True sock = socket.fromfd(FCGI_LISTENSOCK_FILENO, socket.AF_INET, socket.SOCK_STREAM) try: sock.getpeername() except socket.error, e: if e[0] == errno.ENOTSOCK: # Not a socket, assume CGI context. isFCGI = False elif e[0] != errno.ENOTCONN: raise # FastCGI/CGI discrimination is broken on Mac OS X. # Set the environment variable FCGI_FORCE_CGI to "Y" or "y" # if you want to run your app as a simple CGI. (You can do # this with Apache's mod_env [not loaded by default in OS X # client, ha ha] and the SetEnv directive.) if not isFCGI or \ os.environ.get('FCGI_FORCE_CGI', 'N').upper().startswith('Y'): req = self.cgirequest_class(self) req.run() sys.exit(0) else: # Run as a server oldUmask = None if type(self._bindAddress) is str: # Unix socket sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) try: os.unlink(self._bindAddress) except OSError: pass if self._umask is not None: oldUmask = os.umask(self._umask) else: # INET socket assert type(self._bindAddress) is tuple assert len(self._bindAddress) == 2 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(self._bindAddress) sock.listen(socket.SOMAXCONN) if oldUmask is not None: os.umask(oldUmask) return sock def _cleanupSocket(self, sock): """Closes the main socket.""" sock.close() def _installSignalHandlers(self): self._oldSIGs = [(x,signal.getsignal(x)) for x in (signal.SIGHUP, signal.SIGINT, signal.SIGTERM)] signal.signal(signal.SIGHUP, self._hupHandler) signal.signal(signal.SIGINT, self._intHandler) signal.signal(signal.SIGTERM, self._intHandler) def _restoreSignalHandlers(self): for signum,handler in self._oldSIGs: signal.signal(signum, handler) def _hupHandler(self, signum, frame): self._hupReceived = True self._keepGoing = False def _intHandler(self, signum, frame): self._keepGoing = False def run(self, timeout=1.0): """ The main loop. Exits on SIGHUP, SIGINT, SIGTERM. Returns True if SIGHUP was received, False otherwise. """ web_server_addrs = os.environ.get('FCGI_WEB_SERVER_ADDRS') if web_server_addrs is not None: web_server_addrs = map(lambda x: x.strip(), web_server_addrs.split(',')) sock = self._setupSocket() self._keepGoing = True self._hupReceived = False # Install signal handlers. self._installSignalHandlers() while self._keepGoing: try: r, w, e = select.select([sock], [], [], timeout) except select.error, e: if e[0] == errno.EINTR: continue raise if r: try: clientSock, addr = sock.accept() except socket.error, e: if e[0] in (errno.EINTR, errno.EAGAIN): continue raise if web_server_addrs and \ (len(addr) != 2 or addr[0] not in web_server_addrs): clientSock.close() continue # Instantiate a new Connection and begin processing FastCGI # messages (either in a new thread or this thread). conn = self._connectionClass(clientSock, addr, self) thread.start_new_thread(conn.run, ()) self._mainloopPeriodic() # Restore signal handlers. self._restoreSignalHandlers() self._cleanupSocket(sock) return self._hupReceived def _mainloopPeriodic(self): """ Called with just about each iteration of the main loop. Meant to be overridden. """ pass def _exit(self, reload=False): """ Protected convenience method for subclasses to force an exit. Not really thread-safe, which is why it isn't public. """ if self._keepGoing: self._keepGoing = False self._hupReceived = reload def handler(self, req): """ Default handler, which just raises an exception. Unless a handler is passed at initialization time, this must be implemented by a subclass. """ raise NotImplementedError, self.__class__.__name__ + '.handler' def error(self, req): """ Called by Request if an exception occurs within the handler. May and should be overridden. """ import cgitb req.stdout.write('Content-Type: text/html\r\n\r\n' + cgitb.html(sys.exc_info())) class WSGIServer(Server): """ FastCGI server that supports the Web Server Gateway Interface. See . """ def __init__(self, application, environ=None, umask=None, multithreaded=True, **kw): """ environ, if present, must be a dictionary-like object. Its contents will be copied into application's environ. Useful for passing application-specific variables. Set multithreaded to False if your application is not MT-safe. """ if kw.has_key('handler'): del kw['handler'] # Doesn't make sense to let this through super(WSGIServer, self).__init__(**kw) if environ is None: environ = {} self.application = application self.environ = environ self.multithreaded = multithreaded # Used to force single-threadedness self._app_lock = thread.allocate_lock() def handler(self, req): """Special handler for WSGI.""" if req.role != FCGI_RESPONDER: return FCGI_UNKNOWN_ROLE, 0 # Mostly taken from example CGI gateway. environ = req.params environ.update(self.environ) environ['wsgi.version'] = (1,0) environ['wsgi.input'] = req.stdin if self._bindAddress is None: stderr = req.stderr else: stderr = TeeOutputStream((sys.stderr, req.stderr)) environ['wsgi.errors'] = stderr environ['wsgi.multithread'] = not isinstance(req, CGIRequest) and \ thread_available and self.multithreaded # Rationale for the following: If started by the web server # (self._bindAddress is None) in either FastCGI or CGI mode, the # possibility of being spawned multiple times simultaneously is quite # real. And, if started as an external server, multiple copies may be # spawned for load-balancing/redundancy. (Though I don't think # mod_fastcgi supports this?) environ['wsgi.multiprocess'] = True environ['wsgi.run_once'] = isinstance(req, CGIRequest) if environ.get('HTTPS', 'off') in ('on', '1'): environ['wsgi.url_scheme'] = 'https' else: environ['wsgi.url_scheme'] = 'http' self._sanitizeEnv(environ) headers_set = [] headers_sent = [] result = None def write(data): assert type(data) is str, 'write() argument must be string' assert headers_set, 'write() before start_response()' if not headers_sent: status, responseHeaders = headers_sent[:] = headers_set found = False for header,value in responseHeaders: if header.lower() == 'content-length': found = True break if not found and result is not None: try: if len(result) == 1: responseHeaders.append(('Content-Length', str(len(data)))) except: pass s = 'Status: %s\r\n' % status for header in responseHeaders: s += '%s: %s\r\n' % header s += '\r\n' req.stdout.write(s) req.stdout.write(data) req.stdout.flush() def start_response(status, response_headers, exc_info=None): if exc_info: try: if headers_sent: # Re-raise if too late raise exc_info[0], exc_info[1], exc_info[2] finally: exc_info = None # avoid dangling circular ref else: assert not headers_set, 'Headers already set!' assert type(status) is str, 'Status must be a string' assert len(status) >= 4, 'Status must be at least 4 characters' assert int(status[:3]), 'Status must begin with 3-digit code' assert status[3] == ' ', 'Status must have a space after code' assert type(response_headers) is list, 'Headers must be a list' if __debug__: for name,val in response_headers: assert type(name) is str, 'Header names must be strings' assert type(val) is str, 'Header values must be strings' headers_set[:] = [status, response_headers] return write if not self.multithreaded: self._app_lock.acquire() try: try: result = self.application(environ, start_response) try: for data in result: if data: write(data) if not headers_sent: write('') # in case body was empty finally: if hasattr(result, 'close'): result.close() except socket.error, e: if e[0] != errno.EPIPE: raise # Don't let EPIPE propagate beyond server finally: if not self.multithreaded: self._app_lock.release() return FCGI_REQUEST_COMPLETE, 0 def _sanitizeEnv(self, environ): """Ensure certain values are present, if required by WSGI.""" if not environ.has_key('SCRIPT_NAME'): environ['SCRIPT_NAME'] = '' if not environ.has_key('PATH_INFO'): environ['PATH_INFO'] = '' # If any of these are missing, it probably signifies a broken # server... for name,default in [('REQUEST_METHOD', 'GET'), ('SERVER_NAME', 'localhost'), ('SERVER_PORT', '80'), ('SERVER_PROTOCOL', 'HTTP/1.0')]: if not environ.has_key(name): environ['wsgi.errors'].write('%s: missing FastCGI param %s ' 'required by WSGI!\n' % (self.__class__.__name__, name)) environ[name] = default if __name__ == '__main__': def test_app(environ, start_response): """Probably not the most efficient example.""" import cgi start_response('200 OK', [('Content-Type', 'text/html')]) yield 'Hello World!\n' \ '\n' \ '

Hello World!

\n' \ '' names = environ.keys() names.sort() for name in names: yield '\n' % ( name, cgi.escape(`environ[name]`)) form = cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ, keep_blank_values=1) if form.list: yield '' for field in form.list: yield '\n' % ( field.name, field.value) yield '
%s%s
Form data
%s%s
\n' \ '\n' WSGIServer(test_app).run() Chula-0.7.0/chula/vendor/selenium.py0000644000175000017500000023223111370740256020334 0ustar jmcfarlanejmcfarlane """ Copyright 2006 ThoughtWorks, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. """ __docformat__ = "restructuredtext en" # This file has been automatically generated via XSL import httplib import urllib import re class selenium: """ Defines an object that runs Selenium commands. Element Locators ~~~~~~~~~~~~~~~~ Element Locators tell Selenium which HTML element a command refers to. The format of a locator is: \ *locatorType*\ **=**\ \ *argument* We support the following strategies for locating elements: * \ **identifier**\ =\ *id*: Select the element with the specified @id attribute. If no match is found, select the first element whose @name attribute is \ *id*. (This is normally the default; see below.) * \ **id**\ =\ *id*: Select the element with the specified @id attribute. * \ **name**\ =\ *name*: Select the first element with the specified @name attribute. * username * name=username The name may optionally be followed by one or more \ *element-filters*, separated from the name by whitespace. If the \ *filterType* is not specified, \ **value**\ is assumed. * name=flavour value=chocolate * \ **dom**\ =\ *javascriptExpression*: Find an element by evaluating the specified string. This allows you to traverse the HTML Document Object Model using JavaScript. Note that you must not return a value in this string; simply make it the last expression in the block. * dom=document.forms['myForm'].myDropdown * dom=document.images[56] * dom=function foo() { return document.links[1]; }; foo(); * \ **xpath**\ =\ *xpathExpression*: Locate an element using an XPath expression. * xpath=//img[@alt='The image alt text'] * xpath=//table[@id='table1']//tr[4]/td[2] * xpath=//a[contains(@href,'#id1')] * xpath=//a[contains(@href,'#id1')]/@class * xpath=(//table[@class='stylee'])//th[text()='theHeaderText']/../td * xpath=//input[@name='name2' and @value='yes'] * xpath=//\*[text()="right"] * \ **link**\ =\ *textPattern*: Select the link (anchor) element which contains text matching the specified \ *pattern*. * link=The link text * \ **css**\ =\ *cssSelectorSyntax*: Select the element using css selectors. Please refer to CSS2 selectors, CSS3 selectors for more information. You can also check the TestCssLocators test in the selenium test suite for an example of usage, which is included in the downloaded selenium core package. * css=a[href="#id3"] * css=span#firstChild + span Currently the css selector locator supports all css1, css2 and css3 selectors except namespace in css3, some pseudo classes(:nth-of-type, :nth-last-of-type, :first-of-type, :last-of-type, :only-of-type, :visited, :hover, :active, :focus, :indeterminate) and pseudo elements(::first-line, ::first-letter, ::selection, ::before, ::after). * \ **ui**\ =\ *uiSpecifierString*: Locate an element by resolving the UI specifier string to another locator, and evaluating it. See the Selenium UI-Element Reference for more details. * ui=loginPages::loginButton() * ui=settingsPages::toggle(label=Hide Email) * ui=forumPages::postBody(index=2)//a[2] Without an explicit locator prefix, Selenium uses the following default strategies: * \ **dom**\ , for locators starting with "document." * \ **xpath**\ , for locators starting with "//" * \ **identifier**\ , otherwise Element Filters ~~~~~~~~~~~~~~~ Element filters can be used with a locator to refine a list of candidate elements. They are currently used only in the 'name' element-locator. Filters look much like locators, ie. \ *filterType*\ **=**\ \ *argument* Supported element-filters are: \ **value=**\ \ *valuePattern* Matches elements based on their values. This is particularly useful for refining a list of similarly-named toggle-buttons. \ **index=**\ \ *index* Selects a single element based on its position in the list (offset from zero). String-match Patterns ~~~~~~~~~~~~~~~~~~~~~ Various Pattern syntaxes are available for matching string values: * \ **glob:**\ \ *pattern*: Match a string against a "glob" (aka "wildmat") pattern. "Glob" is a kind of limited regular-expression syntax typically used in command-line shells. In a glob pattern, "\*" represents any sequence of characters, and "?" represents any single character. Glob patterns match against the entire string. * \ **regexp:**\ \ *regexp*: Match a string using a regular-expression. The full power of JavaScript regular-expressions is available. * \ **regexpi:**\ \ *regexpi*: Match a string using a case-insensitive regular-expression. * \ **exact:**\ \ *string*: Match a string exactly, verbatim, without any of that fancy wildcard stuff. If no pattern prefix is specified, Selenium assumes that it's a "glob" pattern. For commands that return multiple values (such as verifySelectOptions), the string being matched is a comma-separated list of the return values, where both commas and backslashes in the values are backslash-escaped. When providing a pattern, the optional matching syntax (i.e. glob, regexp, etc.) is specified once, as usual, at the beginning of the pattern. """ ### This part is hard-coded in the XSL def __init__(self, host, port, browserStartCommand, browserURL): self.host = host self.port = port self.browserStartCommand = browserStartCommand self.browserURL = browserURL self.sessionId = None self.extensionJs = "" def setExtensionJs(self, extensionJs): self.extensionJs = extensionJs def start(self): result = self.get_string("getNewBrowserSession", [self.browserStartCommand, self.browserURL, self.extensionJs]) try: self.sessionId = result except ValueError: raise Exception, result def stop(self): self.do_command("testComplete", []) self.sessionId = None def do_command(self, verb, args): conn = httplib.HTTPConnection(self.host, self.port) body = u'cmd=' + urllib.quote_plus(unicode(verb).encode('utf-8')) for i in range(len(args)): body += '&' + unicode(i+1) + '=' + urllib.quote_plus(unicode(args[i]).encode('utf-8')) if (None != self.sessionId): body += "&sessionId=" + unicode(self.sessionId) headers = {"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"} conn.request("POST", "/selenium-server/driver/", body, headers) response = conn.getresponse() #print response.status, response.reason data = unicode(response.read(), "UTF-8") result = response.reason #print "Selenium Result: " + repr(data) + "\n\n" if (not data.startswith('OK')): raise Exception, data return data def get_string(self, verb, args): result = self.do_command(verb, args) return result[3:] def get_string_array(self, verb, args): csv = self.get_string(verb, args) token = "" tokens = [] escape = False for i in range(len(csv)): letter = csv[i] if (escape): token = token + letter escape = False continue if (letter == '\\'): escape = True elif (letter == ','): tokens.append(token) token = "" else: token = token + letter tokens.append(token) return tokens def get_number(self, verb, args): # Is there something I need to do here? return self.get_string(verb, args) def get_number_array(self, verb, args): # Is there something I need to do here? return self.get_string_array(verb, args) def get_boolean(self, verb, args): boolstr = self.get_string(verb, args) if ("true" == boolstr): return True if ("false" == boolstr): return False raise ValueError, "result is neither 'true' nor 'false': " + boolstr def get_boolean_array(self, verb, args): boolarr = self.get_string_array(verb, args) for i in range(len(boolarr)): if ("true" == boolstr): boolarr[i] = True continue if ("false" == boolstr): boolarr[i] = False continue raise ValueError, "result is neither 'true' nor 'false': " + boolarr[i] return boolarr ### From here on, everything's auto-generated from XML def click(self,locator): """ Clicks on a link, button, checkbox or radio button. If the click action causes a new page to load (like a link usually does), call waitForPageToLoad. 'locator' is an element locator """ self.do_command("click", [locator,]) def double_click(self,locator): """ Double clicks on a link, button, checkbox or radio button. If the double click action causes a new page to load (like a link usually does), call waitForPageToLoad. 'locator' is an element locator """ self.do_command("doubleClick", [locator,]) def context_menu(self,locator): """ Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element). 'locator' is an element locator """ self.do_command("contextMenu", [locator,]) def click_at(self,locator,coordString): """ Clicks on a link, button, checkbox or radio button. If the click action causes a new page to load (like a link usually does), call waitForPageToLoad. 'locator' is an element locator 'coordString' is specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator. """ self.do_command("clickAt", [locator,coordString,]) def double_click_at(self,locator,coordString): """ Doubleclicks on a link, button, checkbox or radio button. If the action causes a new page to load (like a link usually does), call waitForPageToLoad. 'locator' is an element locator 'coordString' is specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator. """ self.do_command("doubleClickAt", [locator,coordString,]) def context_menu_at(self,locator,coordString): """ Simulates opening the context menu for the specified element (as might happen if the user "right-clicked" on the element). 'locator' is an element locator 'coordString' is specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator. """ self.do_command("contextMenuAt", [locator,coordString,]) def fire_event(self,locator,eventName): """ Explicitly simulate an event, to trigger the corresponding "on\ *event*" handler. 'locator' is an element locator 'eventName' is the event name, e.g. "focus" or "blur" """ self.do_command("fireEvent", [locator,eventName,]) def focus(self,locator): """ Move the focus to the specified element; for example, if the element is an input field, move the cursor to that field. 'locator' is an element locator """ self.do_command("focus", [locator,]) def key_press(self,locator,keySequence): """ Simulates a user pressing and releasing a key. 'locator' is an element locator 'keySequence' is Either be a string("\" followed by the numeric keycode of the key to be pressed, normally the ASCII value of that key), or a single character. For example: "w", "\119". """ self.do_command("keyPress", [locator,keySequence,]) def shift_key_down(self): """ Press the shift key and hold it down until doShiftUp() is called or a new page is loaded. """ self.do_command("shiftKeyDown", []) def shift_key_up(self): """ Release the shift key. """ self.do_command("shiftKeyUp", []) def meta_key_down(self): """ Press the meta key and hold it down until doMetaUp() is called or a new page is loaded. """ self.do_command("metaKeyDown", []) def meta_key_up(self): """ Release the meta key. """ self.do_command("metaKeyUp", []) def alt_key_down(self): """ Press the alt key and hold it down until doAltUp() is called or a new page is loaded. """ self.do_command("altKeyDown", []) def alt_key_up(self): """ Release the alt key. """ self.do_command("altKeyUp", []) def control_key_down(self): """ Press the control key and hold it down until doControlUp() is called or a new page is loaded. """ self.do_command("controlKeyDown", []) def control_key_up(self): """ Release the control key. """ self.do_command("controlKeyUp", []) def key_down(self,locator,keySequence): """ Simulates a user pressing a key (without releasing it yet). 'locator' is an element locator 'keySequence' is Either be a string("\" followed by the numeric keycode of the key to be pressed, normally the ASCII value of that key), or a single character. For example: "w", "\119". """ self.do_command("keyDown", [locator,keySequence,]) def key_up(self,locator,keySequence): """ Simulates a user releasing a key. 'locator' is an element locator 'keySequence' is Either be a string("\" followed by the numeric keycode of the key to be pressed, normally the ASCII value of that key), or a single character. For example: "w", "\119". """ self.do_command("keyUp", [locator,keySequence,]) def mouse_over(self,locator): """ Simulates a user hovering a mouse over the specified element. 'locator' is an element locator """ self.do_command("mouseOver", [locator,]) def mouse_out(self,locator): """ Simulates a user moving the mouse pointer away from the specified element. 'locator' is an element locator """ self.do_command("mouseOut", [locator,]) def mouse_down(self,locator): """ Simulates a user pressing the left mouse button (without releasing it yet) on the specified element. 'locator' is an element locator """ self.do_command("mouseDown", [locator,]) def mouse_down_right(self,locator): """ Simulates a user pressing the right mouse button (without releasing it yet) on the specified element. 'locator' is an element locator """ self.do_command("mouseDownRight", [locator,]) def mouse_down_at(self,locator,coordString): """ Simulates a user pressing the left mouse button (without releasing it yet) at the specified location. 'locator' is an element locator 'coordString' is specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator. """ self.do_command("mouseDownAt", [locator,coordString,]) def mouse_down_right_at(self,locator,coordString): """ Simulates a user pressing the right mouse button (without releasing it yet) at the specified location. 'locator' is an element locator 'coordString' is specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator. """ self.do_command("mouseDownRightAt", [locator,coordString,]) def mouse_up(self,locator): """ Simulates the event that occurs when the user releases the mouse button (i.e., stops holding the button down) on the specified element. 'locator' is an element locator """ self.do_command("mouseUp", [locator,]) def mouse_up_right(self,locator): """ Simulates the event that occurs when the user releases the right mouse button (i.e., stops holding the button down) on the specified element. 'locator' is an element locator """ self.do_command("mouseUpRight", [locator,]) def mouse_up_at(self,locator,coordString): """ Simulates the event that occurs when the user releases the mouse button (i.e., stops holding the button down) at the specified location. 'locator' is an element locator 'coordString' is specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator. """ self.do_command("mouseUpAt", [locator,coordString,]) def mouse_up_right_at(self,locator,coordString): """ Simulates the event that occurs when the user releases the right mouse button (i.e., stops holding the button down) at the specified location. 'locator' is an element locator 'coordString' is specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator. """ self.do_command("mouseUpRightAt", [locator,coordString,]) def mouse_move(self,locator): """ Simulates a user pressing the mouse button (without releasing it yet) on the specified element. 'locator' is an element locator """ self.do_command("mouseMove", [locator,]) def mouse_move_at(self,locator,coordString): """ Simulates a user pressing the mouse button (without releasing it yet) on the specified element. 'locator' is an element locator 'coordString' is specifies the x,y position (i.e. - 10,20) of the mouse event relative to the element returned by the locator. """ self.do_command("mouseMoveAt", [locator,coordString,]) def type(self,locator,value): """ Sets the value of an input field, as though you typed it in. Can also be used to set the value of combo boxes, check boxes, etc. In these cases, value should be the value of the option selected, not the visible text. 'locator' is an element locator 'value' is the value to type """ self.do_command("type", [locator,value,]) def type_keys(self,locator,value): """ Simulates keystroke events on the specified element, as though you typed the value key-by-key. This is a convenience method for calling keyDown, keyUp, keyPress for every character in the specified string; this is useful for dynamic UI widgets (like auto-completing combo boxes) that require explicit key events. Unlike the simple "type" command, which forces the specified value into the page directly, this command may or may not have any visible effect, even in cases where typing keys would normally have a visible effect. For example, if you use "typeKeys" on a form element, you may or may not see the results of what you typed in the field. In some cases, you may need to use the simple "type" command to set the value of the field and then the "typeKeys" command to send the keystroke events corresponding to what you just typed. 'locator' is an element locator 'value' is the value to type """ self.do_command("typeKeys", [locator,value,]) def set_speed(self,value): """ Set execution speed (i.e., set the millisecond length of a delay which will follow each selenium operation). By default, there is no such delay, i.e., the delay is 0 milliseconds. 'value' is the number of milliseconds to pause after operation """ self.do_command("setSpeed", [value,]) def get_speed(self): """ Get execution speed (i.e., get the millisecond length of the delay following each selenium operation). By default, there is no such delay, i.e., the delay is 0 milliseconds. See also setSpeed. """ return self.get_string("getSpeed", []) def check(self,locator): """ Check a toggle-button (checkbox/radio) 'locator' is an element locator """ self.do_command("check", [locator,]) def uncheck(self,locator): """ Uncheck a toggle-button (checkbox/radio) 'locator' is an element locator """ self.do_command("uncheck", [locator,]) def select(self,selectLocator,optionLocator): """ Select an option from a drop-down using an option locator. Option locators provide different ways of specifying options of an HTML Select element (e.g. for selecting a specific option, or for asserting that the selected option satisfies a specification). There are several forms of Select Option Locator. * \ **label**\ =\ *labelPattern*: matches options based on their labels, i.e. the visible text. (This is the default.) * label=regexp:^[Oo]ther * \ **value**\ =\ *valuePattern*: matches options based on their values. * value=other * \ **id**\ =\ *id*: matches options based on their ids. * id=option1 * \ **index**\ =\ *index*: matches an option based on its index (offset from zero). * index=2 If no option locator prefix is provided, the default behaviour is to match on \ **label**\ . 'selectLocator' is an element locator identifying a drop-down menu 'optionLocator' is an option locator (a label by default) """ self.do_command("select", [selectLocator,optionLocator,]) def add_selection(self,locator,optionLocator): """ Add a selection to the set of selected options in a multi-select element using an option locator. @see #doSelect for details of option locators 'locator' is an element locator identifying a multi-select box 'optionLocator' is an option locator (a label by default) """ self.do_command("addSelection", [locator,optionLocator,]) def remove_selection(self,locator,optionLocator): """ Remove a selection from the set of selected options in a multi-select element using an option locator. @see #doSelect for details of option locators 'locator' is an element locator identifying a multi-select box 'optionLocator' is an option locator (a label by default) """ self.do_command("removeSelection", [locator,optionLocator,]) def remove_all_selections(self,locator): """ Unselects all of the selected options in a multi-select element. 'locator' is an element locator identifying a multi-select box """ self.do_command("removeAllSelections", [locator,]) def submit(self,formLocator): """ Submit the specified form. This is particularly useful for forms without submit buttons, e.g. single-input "Search" forms. 'formLocator' is an element locator for the form you want to submit """ self.do_command("submit", [formLocator,]) def open(self,url): """ Opens an URL in the test frame. This accepts both relative and absolute URLs. The "open" command waits for the page to load before proceeding, ie. the "AndWait" suffix is implicit. \ *Note*: The URL must be on the same domain as the runner HTML due to security restrictions in the browser (Same Origin Policy). If you need to open an URL on another domain, use the Selenium Server to start a new browser session on that domain. 'url' is the URL to open; may be relative or absolute """ self.do_command("open", [url,]) def open_window(self,url,windowID): """ Opens a popup window (if a window with that ID isn't already open). After opening the window, you'll need to select it using the selectWindow command. This command can also be a useful workaround for bug SEL-339. In some cases, Selenium will be unable to intercept a call to window.open (if the call occurs during or before the "onLoad" event, for example). In those cases, you can force Selenium to notice the open window's name by using the Selenium openWindow command, using an empty (blank) url, like this: openWindow("", "myFunnyWindow"). 'url' is the URL to open, which can be blank 'windowID' is the JavaScript window ID of the window to select """ self.do_command("openWindow", [url,windowID,]) def select_window(self,windowID): """ Selects a popup window using a window locator; once a popup window has been selected, all commands go to that window. To select the main window again, use null as the target. Window locators provide different ways of specifying the window object: by title, by internal JavaScript "name," or by JavaScript variable. * \ **title**\ =\ *My Special Window*: Finds the window using the text that appears in the title bar. Be careful; two windows can share the same title. If that happens, this locator will just pick one. * \ **name**\ =\ *myWindow*: Finds the window using its internal JavaScript "name" property. This is the second parameter "windowName" passed to the JavaScript method window.open(url, windowName, windowFeatures, replaceFlag) (which Selenium intercepts). * \ **var**\ =\ *variableName*: Some pop-up windows are unnamed (anonymous), but are associated with a JavaScript variable name in the current application window, e.g. "window.foo = window.open(url);". In those cases, you can open the window using "var=foo". If no window locator prefix is provided, we'll try to guess what you mean like this: 1.) if windowID is null, (or the string "null") then it is assumed the user is referring to the original window instantiated by the browser). 2.) if the value of the "windowID" parameter is a JavaScript variable name in the current application window, then it is assumed that this variable contains the return value from a call to the JavaScript window.open() method. 3.) Otherwise, selenium looks in a hash it maintains that maps string names to window "names". 4.) If \ *that* fails, we'll try looping over all of the known windows to try to find the appropriate "title". Since "title" is not necessarily unique, this may have unexpected behavior. If you're having trouble figuring out the name of a window that you want to manipulate, look at the Selenium log messages which identify the names of windows created via window.open (and therefore intercepted by Selenium). You will see messages like the following for each window as it is opened: ``debug: window.open call intercepted; window ID (which you can use with selectWindow()) is "myNewWindow"`` In some cases, Selenium will be unable to intercept a call to window.open (if the call occurs during or before the "onLoad" event, for example). (This is bug SEL-339.) In those cases, you can force Selenium to notice the open window's name by using the Selenium openWindow command, using an empty (blank) url, like this: openWindow("", "myFunnyWindow"). 'windowID' is the JavaScript window ID of the window to select """ self.do_command("selectWindow", [windowID,]) def select_pop_up(self,windowID): """ Simplifies the process of selecting a popup window (and does not offer functionality beyond what ``selectWindow()`` already provides). * If ``windowID`` is either not specified, or specified as "null", the first non-top window is selected. The top window is the one that would be selected by ``selectWindow()`` without providing a ``windowID`` . This should not be used when more than one popup window is in play. * Otherwise, the window will be looked up considering ``windowID`` as the following in order: 1) the "name" of the window, as specified to ``window.open()``; 2) a javascript variable which is a reference to a window; and 3) the title of the window. This is the same ordered lookup performed by ``selectWindow`` . 'windowID' is an identifier for the popup window, which can take on a number of different meanings """ self.do_command("selectPopUp", [windowID,]) def deselect_pop_up(self): """ Selects the main window. Functionally equivalent to using ``selectWindow()`` and specifying no value for ``windowID``. """ self.do_command("deselectPopUp", []) def select_frame(self,locator): """ Selects a frame within the current window. (You may invoke this command multiple times to select nested frames.) To select the parent frame, use "relative=parent" as a locator; to select the top frame, use "relative=top". You can also select a frame by its 0-based index number; select the first frame with "index=0", or the third frame with "index=2". You may also use a DOM expression to identify the frame you want directly, like this: ``dom=frames["main"].frames["subframe"]`` 'locator' is an element locator identifying a frame or iframe """ self.do_command("selectFrame", [locator,]) def get_whether_this_frame_match_frame_expression(self,currentFrameString,target): """ Determine whether current/locator identify the frame containing this running code. This is useful in proxy injection mode, where this code runs in every browser frame and window, and sometimes the selenium server needs to identify the "current" frame. In this case, when the test calls selectFrame, this routine is called for each frame to figure out which one has been selected. The selected frame will return true, while all others will return false. 'currentFrameString' is starting frame 'target' is new frame (which might be relative to the current one) """ return self.get_boolean("getWhetherThisFrameMatchFrameExpression", [currentFrameString,target,]) def get_whether_this_window_match_window_expression(self,currentWindowString,target): """ Determine whether currentWindowString plus target identify the window containing this running code. This is useful in proxy injection mode, where this code runs in every browser frame and window, and sometimes the selenium server needs to identify the "current" window. In this case, when the test calls selectWindow, this routine is called for each window to figure out which one has been selected. The selected window will return true, while all others will return false. 'currentWindowString' is starting window 'target' is new window (which might be relative to the current one, e.g., "_parent") """ return self.get_boolean("getWhetherThisWindowMatchWindowExpression", [currentWindowString,target,]) def wait_for_pop_up(self,windowID,timeout): """ Waits for a popup window to appear and load up. 'windowID' is the JavaScript window "name" of the window that will appear (not the text of the title bar) If unspecified, or specified as "null", this command will wait for the first non-top window to appear (don't rely on this if you are working with multiple popups simultaneously). 'timeout' is a timeout in milliseconds, after which the action will return with an error. If this value is not specified, the default Selenium timeout will be used. See the setTimeout() command. """ self.do_command("waitForPopUp", [windowID,timeout,]) def choose_cancel_on_next_confirmation(self): """ By default, Selenium's overridden window.confirm() function will return true, as if the user had manually clicked OK; after running this command, the next call to confirm() will return false, as if the user had clicked Cancel. Selenium will then resume using the default behavior for future confirmations, automatically returning true (OK) unless/until you explicitly call this command for each confirmation. Take note - every time a confirmation comes up, you must consume it with a corresponding getConfirmation, or else the next selenium operation will fail. """ self.do_command("chooseCancelOnNextConfirmation", []) def choose_ok_on_next_confirmation(self): """ Undo the effect of calling chooseCancelOnNextConfirmation. Note that Selenium's overridden window.confirm() function will normally automatically return true, as if the user had manually clicked OK, so you shouldn't need to use this command unless for some reason you need to change your mind prior to the next confirmation. After any confirmation, Selenium will resume using the default behavior for future confirmations, automatically returning true (OK) unless/until you explicitly call chooseCancelOnNextConfirmation for each confirmation. Take note - every time a confirmation comes up, you must consume it with a corresponding getConfirmation, or else the next selenium operation will fail. """ self.do_command("chooseOkOnNextConfirmation", []) def answer_on_next_prompt(self,answer): """ Instructs Selenium to return the specified answer string in response to the next JavaScript prompt [window.prompt()]. 'answer' is the answer to give in response to the prompt pop-up """ self.do_command("answerOnNextPrompt", [answer,]) def go_back(self): """ Simulates the user clicking the "back" button on their browser. """ self.do_command("goBack", []) def refresh(self): """ Simulates the user clicking the "Refresh" button on their browser. """ self.do_command("refresh", []) def close(self): """ Simulates the user clicking the "close" button in the titlebar of a popup window or tab. """ self.do_command("close", []) def is_alert_present(self): """ Has an alert occurred? This function never throws an exception """ return self.get_boolean("isAlertPresent", []) def is_prompt_present(self): """ Has a prompt occurred? This function never throws an exception """ return self.get_boolean("isPromptPresent", []) def is_confirmation_present(self): """ Has confirm() been called? This function never throws an exception """ return self.get_boolean("isConfirmationPresent", []) def get_alert(self): """ Retrieves the message of a JavaScript alert generated during the previous action, or fail if there were no alerts. Getting an alert has the same effect as manually clicking OK. If an alert is generated but you do not consume it with getAlert, the next Selenium action will fail. Under Selenium, JavaScript alerts will NOT pop up a visible alert dialog. Selenium does NOT support JavaScript alerts that are generated in a page's onload() event handler. In this case a visible dialog WILL be generated and Selenium will hang until someone manually clicks OK. """ return self.get_string("getAlert", []) def get_confirmation(self): """ Retrieves the message of a JavaScript confirmation dialog generated during the previous action. By default, the confirm function will return true, having the same effect as manually clicking OK. This can be changed by prior execution of the chooseCancelOnNextConfirmation command. If an confirmation is generated but you do not consume it with getConfirmation, the next Selenium action will fail. NOTE: under Selenium, JavaScript confirmations will NOT pop up a visible dialog. NOTE: Selenium does NOT support JavaScript confirmations that are generated in a page's onload() event handler. In this case a visible dialog WILL be generated and Selenium will hang until you manually click OK. """ return self.get_string("getConfirmation", []) def get_prompt(self): """ Retrieves the message of a JavaScript question prompt dialog generated during the previous action. Successful handling of the prompt requires prior execution of the answerOnNextPrompt command. If a prompt is generated but you do not get/verify it, the next Selenium action will fail. NOTE: under Selenium, JavaScript prompts will NOT pop up a visible dialog. NOTE: Selenium does NOT support JavaScript prompts that are generated in a page's onload() event handler. In this case a visible dialog WILL be generated and Selenium will hang until someone manually clicks OK. """ return self.get_string("getPrompt", []) def get_location(self): """ Gets the absolute URL of the current page. """ return self.get_string("getLocation", []) def get_title(self): """ Gets the title of the current page. """ return self.get_string("getTitle", []) def get_body_text(self): """ Gets the entire text of the page. """ return self.get_string("getBodyText", []) def get_value(self,locator): """ Gets the (whitespace-trimmed) value of an input field (or anything else with a value parameter). For checkbox/radio elements, the value will be "on" or "off" depending on whether the element is checked or not. 'locator' is an element locator """ return self.get_string("getValue", [locator,]) def get_text(self,locator): """ Gets the text of an element. This works for any element that contains text. This command uses either the textContent (Mozilla-like browsers) or the innerText (IE-like browsers) of the element, which is the rendered text shown to the user. 'locator' is an element locator """ return self.get_string("getText", [locator,]) def highlight(self,locator): """ Briefly changes the backgroundColor of the specified element yellow. Useful for debugging. 'locator' is an element locator """ self.do_command("highlight", [locator,]) def get_eval(self,script): """ Gets the result of evaluating the specified JavaScript snippet. The snippet may have multiple lines, but only the result of the last line will be returned. Note that, by default, the snippet will run in the context of the "selenium" object itself, so ``this`` will refer to the Selenium object. Use ``window`` to refer to the window of your application, e.g. ``window.document.getElementById('foo')`` If you need to use a locator to refer to a single element in your application page, you can use ``this.browserbot.findElement("id=foo")`` where "id=foo" is your locator. 'script' is the JavaScript snippet to run """ return self.get_string("getEval", [script,]) def is_checked(self,locator): """ Gets whether a toggle-button (checkbox/radio) is checked. Fails if the specified element doesn't exist or isn't a toggle-button. 'locator' is an element locator pointing to a checkbox or radio button """ return self.get_boolean("isChecked", [locator,]) def get_table(self,tableCellAddress): """ Gets the text from a cell of a table. The cellAddress syntax tableLocator.row.column, where row and column start at 0. 'tableCellAddress' is a cell address, e.g. "foo.1.4" """ return self.get_string("getTable", [tableCellAddress,]) def get_selected_labels(self,selectLocator): """ Gets all option labels (visible text) for selected options in the specified select or multi-select element. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string_array("getSelectedLabels", [selectLocator,]) def get_selected_label(self,selectLocator): """ Gets option label (visible text) for selected option in the specified select element. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string("getSelectedLabel", [selectLocator,]) def get_selected_values(self,selectLocator): """ Gets all option values (value attributes) for selected options in the specified select or multi-select element. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string_array("getSelectedValues", [selectLocator,]) def get_selected_value(self,selectLocator): """ Gets option value (value attribute) for selected option in the specified select element. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string("getSelectedValue", [selectLocator,]) def get_selected_indexes(self,selectLocator): """ Gets all option indexes (option number, starting at 0) for selected options in the specified select or multi-select element. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string_array("getSelectedIndexes", [selectLocator,]) def get_selected_index(self,selectLocator): """ Gets option index (option number, starting at 0) for selected option in the specified select element. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string("getSelectedIndex", [selectLocator,]) def get_selected_ids(self,selectLocator): """ Gets all option element IDs for selected options in the specified select or multi-select element. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string_array("getSelectedIds", [selectLocator,]) def get_selected_id(self,selectLocator): """ Gets option element ID for selected option in the specified select element. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string("getSelectedId", [selectLocator,]) def is_something_selected(self,selectLocator): """ Determines whether some option in a drop-down menu is selected. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_boolean("isSomethingSelected", [selectLocator,]) def get_select_options(self,selectLocator): """ Gets all option labels in the specified select drop-down. 'selectLocator' is an element locator identifying a drop-down menu """ return self.get_string_array("getSelectOptions", [selectLocator,]) def get_attribute(self,attributeLocator): """ Gets the value of an element attribute. The value of the attribute may differ across browsers (this is the case for the "style" attribute, for example). 'attributeLocator' is an element locator followed by an @ sign and then the name of the attribute, e.g. "foo@bar" """ return self.get_string("getAttribute", [attributeLocator,]) def is_text_present(self,pattern): """ Verifies that the specified text pattern appears somewhere on the rendered page shown to the user. 'pattern' is a pattern to match with the text of the page """ return self.get_boolean("isTextPresent", [pattern,]) def is_element_present(self,locator): """ Verifies that the specified element is somewhere on the page. 'locator' is an element locator """ return self.get_boolean("isElementPresent", [locator,]) def is_visible(self,locator): """ Determines if the specified element is visible. An element can be rendered invisible by setting the CSS "visibility" property to "hidden", or the "display" property to "none", either for the element itself or one if its ancestors. This method will fail if the element is not present. 'locator' is an element locator """ return self.get_boolean("isVisible", [locator,]) def is_editable(self,locator): """ Determines whether the specified input element is editable, ie hasn't been disabled. This method will fail if the specified element isn't an input element. 'locator' is an element locator """ return self.get_boolean("isEditable", [locator,]) def get_all_buttons(self): """ Returns the IDs of all buttons on the page. If a given button has no ID, it will appear as "" in this array. """ return self.get_string_array("getAllButtons", []) def get_all_links(self): """ Returns the IDs of all links on the page. If a given link has no ID, it will appear as "" in this array. """ return self.get_string_array("getAllLinks", []) def get_all_fields(self): """ Returns the IDs of all input fields on the page. If a given field has no ID, it will appear as "" in this array. """ return self.get_string_array("getAllFields", []) def get_attribute_from_all_windows(self,attributeName): """ Returns every instance of some attribute from all known windows. 'attributeName' is name of an attribute on the windows """ return self.get_string_array("getAttributeFromAllWindows", [attributeName,]) def dragdrop(self,locator,movementsString): """ deprecated - use dragAndDrop instead 'locator' is an element locator 'movementsString' is offset in pixels from the current location to which the element should be moved, e.g., "+70,-300" """ self.do_command("dragdrop", [locator,movementsString,]) def set_mouse_speed(self,pixels): """ Configure the number of pixels between "mousemove" events during dragAndDrop commands (default=10). Setting this value to 0 means that we'll send a "mousemove" event to every single pixel in between the start location and the end location; that can be very slow, and may cause some browsers to force the JavaScript to timeout. If the mouse speed is greater than the distance between the two dragged objects, we'll just send one "mousemove" at the start location and then one final one at the end location. 'pixels' is the number of pixels between "mousemove" events """ self.do_command("setMouseSpeed", [pixels,]) def get_mouse_speed(self): """ Returns the number of pixels between "mousemove" events during dragAndDrop commands (default=10). """ return self.get_number("getMouseSpeed", []) def drag_and_drop(self,locator,movementsString): """ Drags an element a certain distance and then drops it 'locator' is an element locator 'movementsString' is offset in pixels from the current location to which the element should be moved, e.g., "+70,-300" """ self.do_command("dragAndDrop", [locator,movementsString,]) def drag_and_drop_to_object(self,locatorOfObjectToBeDragged,locatorOfDragDestinationObject): """ Drags an element and drops it on another element 'locatorOfObjectToBeDragged' is an element to be dragged 'locatorOfDragDestinationObject' is an element whose location (i.e., whose center-most pixel) will be the point where locatorOfObjectToBeDragged is dropped """ self.do_command("dragAndDropToObject", [locatorOfObjectToBeDragged,locatorOfDragDestinationObject,]) def window_focus(self): """ Gives focus to the currently selected window """ self.do_command("windowFocus", []) def window_maximize(self): """ Resize currently selected window to take up the entire screen """ self.do_command("windowMaximize", []) def get_all_window_ids(self): """ Returns the IDs of all windows that the browser knows about. """ return self.get_string_array("getAllWindowIds", []) def get_all_window_names(self): """ Returns the names of all windows that the browser knows about. """ return self.get_string_array("getAllWindowNames", []) def get_all_window_titles(self): """ Returns the titles of all windows that the browser knows about. """ return self.get_string_array("getAllWindowTitles", []) def get_html_source(self): """ Returns the entire HTML source between the opening and closing "html" tags. """ return self.get_string("getHtmlSource", []) def set_cursor_position(self,locator,position): """ Moves the text cursor to the specified position in the given input element or textarea. This method will fail if the specified element isn't an input element or textarea. 'locator' is an element locator pointing to an input element or textarea 'position' is the numerical position of the cursor in the field; position should be 0 to move the position to the beginning of the field. You can also set the cursor to -1 to move it to the end of the field. """ self.do_command("setCursorPosition", [locator,position,]) def get_element_index(self,locator): """ Get the relative index of an element to its parent (starting from 0). The comment node and empty text node will be ignored. 'locator' is an element locator pointing to an element """ return self.get_number("getElementIndex", [locator,]) def is_ordered(self,locator1,locator2): """ Check if these two elements have same parent and are ordered siblings in the DOM. Two same elements will not be considered ordered. 'locator1' is an element locator pointing to the first element 'locator2' is an element locator pointing to the second element """ return self.get_boolean("isOrdered", [locator1,locator2,]) def get_element_position_left(self,locator): """ Retrieves the horizontal position of an element 'locator' is an element locator pointing to an element OR an element itself """ return self.get_number("getElementPositionLeft", [locator,]) def get_element_position_top(self,locator): """ Retrieves the vertical position of an element 'locator' is an element locator pointing to an element OR an element itself """ return self.get_number("getElementPositionTop", [locator,]) def get_element_width(self,locator): """ Retrieves the width of an element 'locator' is an element locator pointing to an element """ return self.get_number("getElementWidth", [locator,]) def get_element_height(self,locator): """ Retrieves the height of an element 'locator' is an element locator pointing to an element """ return self.get_number("getElementHeight", [locator,]) def get_cursor_position(self,locator): """ Retrieves the text cursor position in the given input element or textarea; beware, this may not work perfectly on all browsers. Specifically, if the cursor/selection has been cleared by JavaScript, this command will tend to return the position of the last location of the cursor, even though the cursor is now gone from the page. This is filed as SEL-243. This method will fail if the specified element isn't an input element or textarea, or there is no cursor in the element. 'locator' is an element locator pointing to an input element or textarea """ return self.get_number("getCursorPosition", [locator,]) def get_expression(self,expression): """ Returns the specified expression. This is useful because of JavaScript preprocessing. It is used to generate commands like assertExpression and waitForExpression. 'expression' is the value to return """ return self.get_string("getExpression", [expression,]) def get_xpath_count(self,xpath): """ Returns the number of nodes that match the specified xpath, eg. "//table" would give the number of tables. 'xpath' is the xpath expression to evaluate. do NOT wrap this expression in a 'count()' function; we will do that for you. """ return self.get_number("getXpathCount", [xpath,]) def assign_id(self,locator,identifier): """ Temporarily sets the "id" attribute of the specified element, so you can locate it in the future using its ID rather than a slow/complicated XPath. This ID will disappear once the page is reloaded. 'locator' is an element locator pointing to an element 'identifier' is a string to be used as the ID of the specified element """ self.do_command("assignId", [locator,identifier,]) def allow_native_xpath(self,allow): """ Specifies whether Selenium should use the native in-browser implementation of XPath (if any native version is available); if you pass "false" to this function, we will always use our pure-JavaScript xpath library. Using the pure-JS xpath library can improve the consistency of xpath element locators between different browser vendors, but the pure-JS version is much slower than the native implementations. 'allow' is boolean, true means we'll prefer to use native XPath; false means we'll only use JS XPath """ self.do_command("allowNativeXpath", [allow,]) def ignore_attributes_without_value(self,ignore): """ Specifies whether Selenium will ignore xpath attributes that have no value, i.e. are the empty string, when using the non-native xpath evaluation engine. You'd want to do this for performance reasons in IE. However, this could break certain xpaths, for example an xpath that looks for an attribute whose value is NOT the empty string. The hope is that such xpaths are relatively rare, but the user should have the option of using them. Note that this only influences xpath evaluation when using the ajaxslt engine (i.e. not "javascript-xpath"). 'ignore' is boolean, true means we'll ignore attributes without value at the expense of xpath "correctness"; false means we'll sacrifice speed for correctness. """ self.do_command("ignoreAttributesWithoutValue", [ignore,]) def wait_for_condition(self,script,timeout): """ Runs the specified JavaScript snippet repeatedly until it evaluates to "true". The snippet may have multiple lines, but only the result of the last line will be considered. Note that, by default, the snippet will be run in the runner's test window, not in the window of your application. To get the window of your application, you can use the JavaScript snippet ``selenium.browserbot.getCurrentWindow()``, and then run your JavaScript in there 'script' is the JavaScript snippet to run 'timeout' is a timeout in milliseconds, after which this command will return with an error """ self.do_command("waitForCondition", [script,timeout,]) def set_timeout(self,timeout): """ Specifies the amount of time that Selenium will wait for actions to complete. Actions that require waiting include "open" and the "waitFor\*" actions. The default timeout is 30 seconds. 'timeout' is a timeout in milliseconds, after which the action will return with an error """ self.do_command("setTimeout", [timeout,]) def wait_for_page_to_load(self,timeout): """ Waits for a new page to load. You can use this command instead of the "AndWait" suffixes, "clickAndWait", "selectAndWait", "typeAndWait" etc. (which are only available in the JS API). Selenium constantly keeps track of new pages loading, and sets a "newPageLoaded" flag when it first notices a page load. Running any other Selenium command after turns the flag to false. Hence, if you want to wait for a page to load, you must wait immediately after a Selenium command that caused a page-load. 'timeout' is a timeout in milliseconds, after which this command will return with an error """ self.do_command("waitForPageToLoad", [timeout,]) def wait_for_frame_to_load(self,frameAddress,timeout): """ Waits for a new frame to load. Selenium constantly keeps track of new pages and frames loading, and sets a "newPageLoaded" flag when it first notices a page load. See waitForPageToLoad for more information. 'frameAddress' is FrameAddress from the server side 'timeout' is a timeout in milliseconds, after which this command will return with an error """ self.do_command("waitForFrameToLoad", [frameAddress,timeout,]) def get_cookie(self): """ Return all cookies of the current page under test. """ return self.get_string("getCookie", []) def get_cookie_by_name(self,name): """ Returns the value of the cookie with the specified name, or throws an error if the cookie is not present. 'name' is the name of the cookie """ return self.get_string("getCookieByName", [name,]) def is_cookie_present(self,name): """ Returns true if a cookie with the specified name is present, or false otherwise. 'name' is the name of the cookie """ return self.get_boolean("isCookiePresent", [name,]) def create_cookie(self,nameValuePair,optionsString): """ Create a new cookie whose path and domain are same with those of current page under test, unless you specified a path for this cookie explicitly. 'nameValuePair' is name and value of the cookie in a format "name=value" 'optionsString' is options for the cookie. Currently supported options include 'path', 'max_age' and 'domain'. the optionsString's format is "path=/path/, max_age=60, domain=.foo.com". The order of options are irrelevant, the unit of the value of 'max_age' is second. Note that specifying a domain that isn't a subset of the current domain will usually fail. """ self.do_command("createCookie", [nameValuePair,optionsString,]) def delete_cookie(self,name,optionsString): """ Delete a named cookie with specified path and domain. Be careful; to delete a cookie, you need to delete it using the exact same path and domain that were used to create the cookie. If the path is wrong, or the domain is wrong, the cookie simply won't be deleted. Also note that specifying a domain that isn't a subset of the current domain will usually fail. Since there's no way to discover at runtime the original path and domain of a given cookie, we've added an option called 'recurse' to try all sub-domains of the current domain with all paths that are a subset of the current path. Beware; this option can be slow. In big-O notation, it operates in O(n\*m) time, where n is the number of dots in the domain name and m is the number of slashes in the path. 'name' is the name of the cookie to be deleted 'optionsString' is options for the cookie. Currently supported options include 'path', 'domain' and 'recurse.' The optionsString's format is "path=/path/, domain=.foo.com, recurse=true". The order of options are irrelevant. Note that specifying a domain that isn't a subset of the current domain will usually fail. """ self.do_command("deleteCookie", [name,optionsString,]) def delete_all_visible_cookies(self): """ Calls deleteCookie with recurse=true on all cookies visible to the current page. As noted on the documentation for deleteCookie, recurse=true can be much slower than simply deleting the cookies using a known domain/path. """ self.do_command("deleteAllVisibleCookies", []) def set_browser_log_level(self,logLevel): """ Sets the threshold for browser-side logging messages; log messages beneath this threshold will be discarded. Valid logLevel strings are: "debug", "info", "warn", "error" or "off". To see the browser logs, you need to either show the log window in GUI mode, or enable browser-side logging in Selenium RC. 'logLevel' is one of the following: "debug", "info", "warn", "error" or "off" """ self.do_command("setBrowserLogLevel", [logLevel,]) def run_script(self,script): """ Creates a new "script" tag in the body of the current test window, and adds the specified text into the body of the command. Scripts run in this way can often be debugged more easily than scripts executed using Selenium's "getEval" command. Beware that JS exceptions thrown in these script tags aren't managed by Selenium, so you should probably wrap your script in try/catch blocks if there is any chance that the script will throw an exception. 'script' is the JavaScript snippet to run """ self.do_command("runScript", [script,]) def add_location_strategy(self,strategyName,functionDefinition): """ Defines a new function for Selenium to locate elements on the page. For example, if you define the strategy "foo", and someone runs click("foo=blah"), we'll run your function, passing you the string "blah", and click on the element that your function returns, or throw an "Element not found" error if your function returns null. We'll pass three arguments to your function: * locator: the string the user passed in * inWindow: the currently selected window * inDocument: the currently selected document The function must return null if the element can't be found. 'strategyName' is the name of the strategy to define; this should use only letters [a-zA-Z] with no spaces or other punctuation. 'functionDefinition' is a string defining the body of a function in JavaScript. For example: ``return inDocument.getElementById(locator);`` """ self.do_command("addLocationStrategy", [strategyName,functionDefinition,]) def capture_entire_page_screenshot(self,filename,kwargs): """ Saves the entire contents of the current window canvas to a PNG file. Contrast this with the captureScreenshot command, which captures the contents of the OS viewport (i.e. whatever is currently being displayed on the monitor), and is implemented in the RC only. Currently this only works in Firefox when running in chrome mode, and in IE non-HTA using the EXPERIMENTAL "Snapsie" utility. The Firefox implementation is mostly borrowed from the Screengrab! Firefox extension. Please see http://www.screengrab.org and http://snapsie.sourceforge.net/ for details. 'filename' is the path to the file to persist the screenshot as. No filename extension will be appended by default. Directories will not be created if they do not exist, and an exception will be thrown, possibly by native code. 'kwargs' is a kwargs string that modifies the way the screenshot is captured. Example: "background=#CCFFDD" . Currently valid options: * background the background CSS for the HTML document. This may be useful to set for capturing screenshots of less-than-ideal layouts, for example where absolute positioning causes the calculation of the canvas dimension to fail and a black background is exposed (possibly obscuring black text). """ self.do_command("captureEntirePageScreenshot", [filename,kwargs,]) def rollup(self,rollupName,kwargs): """ Executes a command rollup, which is a series of commands with a unique name, and optionally arguments that control the generation of the set of commands. If any one of the rolled-up commands fails, the rollup is considered to have failed. Rollups may also contain nested rollups. 'rollupName' is the name of the rollup command 'kwargs' is keyword arguments string that influences how the rollup expands into commands """ self.do_command("rollup", [rollupName,kwargs,]) def add_script(self,scriptContent,scriptTagId): """ Loads script content into a new script tag in the Selenium document. This differs from the runScript command in that runScript adds the script tag to the document of the AUT, not the Selenium document. The following entities in the script content are replaced by the characters they represent: < > & The corresponding remove command is removeScript. 'scriptContent' is the Javascript content of the script to add 'scriptTagId' is (optional) the id of the new script tag. If specified, and an element with this id already exists, this operation will fail. """ self.do_command("addScript", [scriptContent,scriptTagId,]) def remove_script(self,scriptTagId): """ Removes a script tag from the Selenium document identified by the given id. Does nothing if the referenced tag doesn't exist. 'scriptTagId' is the id of the script element to remove. """ self.do_command("removeScript", [scriptTagId,]) def use_xpath_library(self,libraryName): """ Allows choice of one of the available libraries. 'libraryName' is name of the desired library Only the following three can be chosen: * "ajaxslt" - Google's library * "javascript-xpath" - Cybozu Labs' faster library * "default" - The default library. Currently the default library is "ajaxslt" . If libraryName isn't one of these three, then no change will be made. """ self.do_command("useXpathLibrary", [libraryName,]) def set_context(self,context): """ Writes a message to the status bar and adds a note to the browser-side log. 'context' is the message to be sent to the browser """ self.do_command("setContext", [context,]) def attach_file(self,fieldLocator,fileLocator): """ Sets a file input (upload) field to the file listed in fileLocator 'fieldLocator' is an element locator 'fileLocator' is a URL pointing to the specified file. Before the file can be set in the input field (fieldLocator), Selenium RC may need to transfer the file to the local machine before attaching the file in a web page form. This is common in selenium grid configurations where the RC server driving the browser is not the same machine that started the test. Supported Browsers: Firefox ("\*chrome") only. """ self.do_command("attachFile", [fieldLocator,fileLocator,]) def capture_screenshot(self,filename): """ Captures a PNG screenshot to the specified file. 'filename' is the absolute path to the file to be written, e.g. "c:\blah\screenshot.png" """ self.do_command("captureScreenshot", [filename,]) def capture_screenshot_to_string(self): """ Capture a PNG screenshot. It then returns the file as a base 64 encoded string. """ return self.get_string("captureScreenshotToString", []) def captureNetworkTraffic(self, type): """ Returns the network traffic seen by the browser, including headers, AJAX requests, status codes, and timings. When this function is called, the traffic log is cleared, so the returned content is only the traffic seen since the last call. 'type' is The type of data to return the network traffic as. Valid values are: json, xml, or plain. """ return self.get_string("captureNetworkTraffic", [type,]) def capture_entire_page_screenshot_to_string(self,kwargs): """ Downloads a screenshot of the browser current window canvas to a based 64 encoded PNG file. The \ *entire* windows canvas is captured, including parts rendered outside of the current view port. Currently this only works in Mozilla and when running in chrome mode. 'kwargs' is A kwargs string that modifies the way the screenshot is captured. Example: "background=#CCFFDD". This may be useful to set for capturing screenshots of less-than-ideal layouts, for example where absolute positioning causes the calculation of the canvas dimension to fail and a black background is exposed (possibly obscuring black text). """ return self.get_string("captureEntirePageScreenshotToString", [kwargs,]) def shut_down_selenium_server(self): """ Kills the running Selenium Server and all browser sessions. After you run this command, you will no longer be able to send commands to the server; you can't remotely start the server once it has been stopped. Normally you should prefer to run the "stop" command, which terminates the current browser session, rather than shutting down the entire server. """ self.do_command("shutDownSeleniumServer", []) def retrieve_last_remote_control_logs(self): """ Retrieve the last messages logged on a specific remote control. Useful for error reports, especially when running multiple remote controls in a distributed environment. The maximum number of log messages that can be retrieve is configured on remote control startup. """ return self.get_string("retrieveLastRemoteControlLogs", []) def key_down_native(self,keycode): """ Simulates a user pressing a key (without releasing it yet) by sending a native operating system keystroke. This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular element, focus on the element first before running this command. 'keycode' is an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes! """ self.do_command("keyDownNative", [keycode,]) def key_up_native(self,keycode): """ Simulates a user releasing a key by sending a native operating system keystroke. This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular element, focus on the element first before running this command. 'keycode' is an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes! """ self.do_command("keyUpNative", [keycode,]) def key_press_native(self,keycode): """ Simulates a user pressing and releasing a key by sending a native operating system keystroke. This function uses the java.awt.Robot class to send a keystroke; this more accurately simulates typing a key on the keyboard. It does not honor settings from the shiftKeyDown, controlKeyDown, altKeyDown and metaKeyDown commands, and does not target any particular HTML element. To send a keystroke to a particular element, focus on the element first before running this command. 'keycode' is an integer keycode number corresponding to a java.awt.event.KeyEvent; note that Java keycodes are NOT the same thing as JavaScript keycodes! """ self.do_command("keyPressNative", [keycode,]) Chula-0.7.0/chula/vendor/__init__.py0000644000175000017500000000000011370740256020235 0ustar jmcfarlanejmcfarlaneChula-0.7.0/chula/vendor/memcache.py0000644000175000017500000012771011370740256020262 0ustar jmcfarlanejmcfarlane#!/usr/bin/env python """ client module for memcached (memory cache daemon) Overview ======== See U{the MemCached homepage} for more about memcached. Usage summary ============= This should give you a feel for how this module operates:: import memcache mc = memcache.Client(['127.0.0.1:11211'], debug=0) mc.set("some_key", "Some value") value = mc.get("some_key") mc.set("another_key", 3) mc.delete("another_key") mc.set("key", "1") # note that the key used for incr/decr must be a string. mc.incr("key") mc.decr("key") The standard way to use memcache with a database is like this:: key = derive_key(obj) obj = mc.get(key) if not obj: obj = backend_api.get(...) mc.set(key, obj) # we now have obj, and future passes through this code # will use the object from the cache. Detailed Documentation ====================== More detailed documentation is available in the L{Client} class. """ import sys import socket import time import os import re try: import cPickle as pickle except ImportError: import pickle from binascii import crc32 # zlib version is not cross-platform def cmemcache_hash(key): return((((crc32(key) & 0xffffffff) >> 16) & 0x7fff) or 1) serverHashFunction = cmemcache_hash def useOldServerHashFunction(): """Use the old python-memcache server hash function.""" serverHashFunction = crc32 try: from zlib import compress, decompress _supports_compress = True except ImportError: _supports_compress = False # quickly define a decompress just in case we recv compressed data. def decompress(val): raise _Error("received compressed data but I don't support compession (import error)") try: from cStringIO import StringIO except ImportError: from StringIO import StringIO __author__ = "Evan Martin " __version__ = "1.45" __copyright__ = "Copyright (C) 2003 Danga Interactive" __license__ = "Python" SERVER_MAX_KEY_LENGTH = 250 # Storing values larger than 1MB requires recompiling memcached. If you do, # this value can be changed by doing "memcache.SERVER_MAX_VALUE_LENGTH = N" # after importing this module. SERVER_MAX_VALUE_LENGTH = 1024*1024 class _Error(Exception): pass try: # Only exists in Python 2.4+ from threading import local except ImportError: # TODO: add the pure-python local implementation class local(object): pass class Client(local): """ Object representing a pool of memcache servers. See L{memcache} for an overview. In all cases where a key is used, the key can be either: 1. A simple hashable type (string, integer, etc.). 2. A tuple of C{(hashvalue, key)}. This is useful if you want to avoid making this module calculate a hash value. You may prefer, for example, to keep all of a given user's objects on the same memcache server, so you could use the user's unique id as the hash value. @group Setup: __init__, set_servers, forget_dead_hosts, disconnect_all, debuglog @group Insertion: set, add, replace, set_multi @group Retrieval: get, get_multi @group Integers: incr, decr @group Removal: delete, delete_multi @sort: __init__, set_servers, forget_dead_hosts, disconnect_all, debuglog,\ set, set_multi, add, replace, get, get_multi, incr, decr, delete, delete_multi """ _FLAG_PICKLE = 1<<0 _FLAG_INTEGER = 1<<1 _FLAG_LONG = 1<<2 _FLAG_COMPRESSED = 1<<3 _SERVER_RETRIES = 10 # how many times to try finding a free server. # exceptions for Client class MemcachedKeyError(Exception): pass class MemcachedKeyLengthError(MemcachedKeyError): pass class MemcachedKeyCharacterError(MemcachedKeyError): pass class MemcachedKeyNoneError(MemcachedKeyError): pass class MemcachedKeyTypeError(MemcachedKeyError): pass class MemcachedStringEncodingError(Exception): pass def __init__(self, servers, debug=0, pickleProtocol=0, pickler=pickle.Pickler, unpickler=pickle.Unpickler, pload=None, pid=None, server_max_key_length=SERVER_MAX_KEY_LENGTH, server_max_value_length=SERVER_MAX_VALUE_LENGTH): """ Create a new Client object with the given list of servers. @param servers: C{servers} is passed to L{set_servers}. @param debug: whether to display error messages when a server can't be contacted. @param pickleProtocol: number to mandate protocol used by (c)Pickle. @param pickler: optional override of default Pickler to allow subclassing. @param unpickler: optional override of default Unpickler to allow subclassing. @param pload: optional persistent_load function to call on pickle loading. Useful for cPickle since subclassing isn't allowed. @param pid: optional persistent_id function to call on pickle storing. Useful for cPickle since subclassing isn't allowed. """ local.__init__(self) self.debug = debug self.set_servers(servers) self.stats = {} self.cas_ids = {} # Allow users to modify pickling/unpickling behavior self.pickleProtocol = pickleProtocol self.pickler = pickler self.unpickler = unpickler self.persistent_load = pload self.persistent_id = pid self.server_max_key_length = server_max_key_length self.server_max_value_length = server_max_value_length # figure out the pickler style file = StringIO() try: pickler = self.pickler(file, protocol = self.pickleProtocol) self.picklerIsKeyword = True except TypeError: self.picklerIsKeyword = False def set_servers(self, servers): """ Set the pool of servers used by this client. @param servers: an array of servers. Servers can be passed in two forms: 1. Strings of the form C{"host:port"}, which implies a default weight of 1. 2. Tuples of the form C{("host:port", weight)}, where C{weight} is an integer weight value. """ self.servers = [_Host(s, self.debug) for s in servers] self._init_buckets() def get_stats(self): '''Get statistics from each of the servers. @return: A list of tuples ( server_identifier, stats_dictionary ). The dictionary contains a number of name/value pairs specifying the name of the status field and the string value associated with it. The values are not converted from strings. ''' data = [] for s in self.servers: if not s.connect(): continue if s.family == socket.AF_INET: name = '%s:%s (%s)' % ( s.ip, s.port, s.weight ) else: name = 'unix:%s (%s)' % ( s.address, s.weight ) s.send_cmd('stats') serverData = {} data.append(( name, serverData )) readline = s.readline while 1: line = readline() if not line or line.strip() == 'END': break stats = line.split(' ', 2) serverData[stats[1]] = stats[2] return(data) def get_slabs(self): data = [] for s in self.servers: if not s.connect(): continue if s.family == socket.AF_INET: name = '%s:%s (%s)' % ( s.ip, s.port, s.weight ) else: name = 'unix:%s (%s)' % ( s.address, s.weight ) serverData = {} data.append(( name, serverData )) s.send_cmd('stats items') readline = s.readline while 1: line = readline() if not line or line.strip() == 'END': break item = line.split(' ', 2) #0 = STAT, 1 = ITEM, 2 = Value slab = item[1].split(':', 2) #0 = items, 1 = Slab #, 2 = Name if slab[1] not in serverData: serverData[slab[1]] = {} serverData[slab[1]][slab[2]] = item[2] return data def flush_all(self): 'Expire all data currently in the memcache servers.' for s in self.servers: if not s.connect(): continue s.send_cmd('flush_all') s.expect("OK") def debuglog(self, str): if self.debug: sys.stderr.write("MemCached: %s\n" % str) def _statlog(self, func): if func not in self.stats: self.stats[func] = 1 else: self.stats[func] += 1 def forget_dead_hosts(self): """ Reset every host in the pool to an "alive" state. """ for s in self.servers: s.deaduntil = 0 def _init_buckets(self): self.buckets = [] for server in self.servers: for i in range(server.weight): self.buckets.append(server) def _get_server(self, key): if isinstance(key, tuple): serverhash, key = key else: serverhash = serverHashFunction(key) for i in range(Client._SERVER_RETRIES): server = self.buckets[serverhash % len(self.buckets)] if server.connect(): #print "(using server %s)" % server, return server, key serverhash = serverHashFunction(str(serverhash) + str(i)) return None, None def disconnect_all(self): for s in self.servers: s.close_socket() def delete_multi(self, keys, time=0, key_prefix=''): ''' Delete multiple keys in the memcache doing just one query. >>> notset_keys = mc.set_multi({'key1' : 'val1', 'key2' : 'val2'}) >>> mc.get_multi(['key1', 'key2']) == {'key1' : 'val1', 'key2' : 'val2'} 1 >>> mc.delete_multi(['key1', 'key2']) 1 >>> mc.get_multi(['key1', 'key2']) == {} 1 This method is recommended over iterated regular L{delete}s as it reduces total latency, since your app doesn't have to wait for each round-trip of L{delete} before sending the next one. @param keys: An iterable of keys to clear @param time: number of seconds any subsequent set / update commands should fail. Defaults to 0 for no delay. @param key_prefix: Optional string to prepend to each key when sending to memcache. See docs for L{get_multi} and L{set_multi}. @return: 1 if no failure in communication with any memcacheds. @rtype: int ''' self._statlog('delete_multi') server_keys, prefixed_to_orig_key = self._map_and_prefix_keys(keys, key_prefix) # send out all requests on each server before reading anything dead_servers = [] rc = 1 for server in server_keys.iterkeys(): bigcmd = [] write = bigcmd.append if time != None: for key in server_keys[server]: # These are mangled keys write("delete %s %d\r\n" % (key, time)) else: for key in server_keys[server]: # These are mangled keys write("delete %s\r\n" % key) try: server.send_cmds(''.join(bigcmd)) except socket.error, msg: rc = 0 if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) dead_servers.append(server) # if any servers died on the way, don't expect them to respond. for server in dead_servers: del server_keys[server] notstored = [] # original keys. for server, keys in server_keys.iteritems(): try: for key in keys: server.expect("DELETED") except socket.error, msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) rc = 0 return rc def delete(self, key, time=0): '''Deletes a key from the memcache. @return: Nonzero on success. @param time: number of seconds any subsequent set / update commands should fail. Defaults to 0 for no delay. @rtype: int ''' self.check_key(key) server, key = self._get_server(key) if not server: return 0 self._statlog('delete') if time != None: cmd = "delete %s %d" % (key, time) else: cmd = "delete %s" % key try: server.send_cmd(cmd) server.expect("DELETED") except socket.error, msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) return 0 return 1 def incr(self, key, delta=1): """ Sends a command to the server to atomically increment the value for C{key} by C{delta}, or by 1 if C{delta} is unspecified. Returns None if C{key} doesn't exist on server, otherwise it returns the new value after incrementing. Note that the value for C{key} must already exist in the memcache, and it must be the string representation of an integer. >>> mc.set("counter", "20") # returns 1, indicating success 1 >>> mc.incr("counter") 21 >>> mc.incr("counter") 22 Overflow on server is not checked. Be aware of values approaching 2**32. See L{decr}. @param delta: Integer amount to increment by (should be zero or greater). @return: New value after incrementing. @rtype: int """ return self._incrdecr("incr", key, delta) def decr(self, key, delta=1): """ Like L{incr}, but decrements. Unlike L{incr}, underflow is checked and new values are capped at 0. If server value is 1, a decrement of 2 returns 0, not -1. @param delta: Integer amount to decrement by (should be zero or greater). @return: New value after decrementing. @rtype: int """ return self._incrdecr("decr", key, delta) def _incrdecr(self, cmd, key, delta): self.check_key(key) server, key = self._get_server(key) if not server: return 0 self._statlog(cmd) cmd = "%s %s %d" % (cmd, key, delta) try: server.send_cmd(cmd) line = server.readline() if line.strip() =='NOT_FOUND': return None return int(line) except socket.error, msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) return None def add(self, key, val, time = 0, min_compress_len = 0): ''' Add new key with value. Like L{set}, but only stores in memcache if the key doesn't already exist. @return: Nonzero on success. @rtype: int ''' return self._set("add", key, val, time, min_compress_len) def append(self, key, val, time=0, min_compress_len=0): '''Append the value to the end of the existing key's value. Only stores in memcache if key already exists. Also see L{prepend}. @return: Nonzero on success. @rtype: int ''' return self._set("append", key, val, time, min_compress_len) def prepend(self, key, val, time=0, min_compress_len=0): '''Prepend the value to the beginning of the existing key's value. Only stores in memcache if key already exists. Also see L{append}. @return: Nonzero on success. @rtype: int ''' return self._set("prepend", key, val, time, min_compress_len) def replace(self, key, val, time=0, min_compress_len=0): '''Replace existing key with value. Like L{set}, but only stores in memcache if the key already exists. The opposite of L{add}. @return: Nonzero on success. @rtype: int ''' return self._set("replace", key, val, time, min_compress_len) def set(self, key, val, time=0, min_compress_len=0): '''Unconditionally sets a key to a given value in the memcache. The C{key} can optionally be an tuple, with the first element being the server hash value and the second being the key. If you want to avoid making this module calculate a hash value. You may prefer, for example, to keep all of a given user's objects on the same memcache server, so you could use the user's unique id as the hash value. @return: Nonzero on success. @rtype: int @param time: Tells memcached the time which this value should expire, either as a delta number of seconds, or an absolute unix time-since-the-epoch value. See the memcached protocol docs section "Storage Commands" for more info on . We default to 0 == cache forever. @param min_compress_len: The threshold length to kick in auto-compression of the value using the zlib.compress() routine. If the value being cached is a string, then the length of the string is measured, else if the value is an object, then the length of the pickle result is measured. If the resulting attempt at compression yeilds a larger string than the input, then it is discarded. For backwards compatability, this parameter defaults to 0, indicating don't ever try to compress. ''' return self._set("set", key, val, time, min_compress_len) def cas(self, key, val, time=0, min_compress_len=0): '''Sets a key to a given value in the memcache if it hasn't been altered since last fetched. (See L{gets}). The C{key} can optionally be an tuple, with the first element being the server hash value and the second being the key. If you want to avoid making this module calculate a hash value. You may prefer, for example, to keep all of a given user's objects on the same memcache server, so you could use the user's unique id as the hash value. @return: Nonzero on success. @rtype: int @param time: Tells memcached the time which this value should expire, either as a delta number of seconds, or an absolute unix time-since-the-epoch value. See the memcached protocol docs section "Storage Commands" for more info on . We default to 0 == cache forever. @param min_compress_len: The threshold length to kick in auto-compression of the value using the zlib.compress() routine. If the value being cached is a string, then the length of the string is measured, else if the value is an object, then the length of the pickle result is measured. If the resulting attempt at compression yeilds a larger string than the input, then it is discarded. For backwards compatability, this parameter defaults to 0, indicating don't ever try to compress. ''' return self._set("cas", key, val, time, min_compress_len) def _map_and_prefix_keys(self, key_iterable, key_prefix): """Compute the mapping of server (_Host instance) -> list of keys to stuff onto that server, as well as the mapping of prefixed key -> original key. """ # Check it just once ... key_extra_len=len(key_prefix) if key_prefix: self.check_key(key_prefix) # server (_Host) -> list of unprefixed server keys in mapping server_keys = {} prefixed_to_orig_key = {} # build up a list for each server of all the keys we want. for orig_key in key_iterable: if isinstance(orig_key, tuple): # Tuple of hashvalue, key ala _get_server(). Caller is essentially telling us what server to stuff this on. # Ensure call to _get_server gets a Tuple as well. str_orig_key = str(orig_key[1]) server, key = self._get_server((orig_key[0], key_prefix + str_orig_key)) # Gotta pre-mangle key before hashing to a server. Returns the mangled key. else: str_orig_key = str(orig_key) # set_multi supports int / long keys. server, key = self._get_server(key_prefix + str_orig_key) # Now check to make sure key length is proper ... self.check_key(str_orig_key, key_extra_len=key_extra_len) if not server: continue if server not in server_keys: server_keys[server] = [] server_keys[server].append(key) prefixed_to_orig_key[key] = orig_key return (server_keys, prefixed_to_orig_key) def set_multi(self, mapping, time=0, key_prefix='', min_compress_len=0): ''' Sets multiple keys in the memcache doing just one query. >>> notset_keys = mc.set_multi({'key1' : 'val1', 'key2' : 'val2'}) >>> mc.get_multi(['key1', 'key2']) == {'key1' : 'val1', 'key2' : 'val2'} 1 This method is recommended over regular L{set} as it lowers the number of total packets flying around your network, reducing total latency, since your app doesn't have to wait for each round-trip of L{set} before sending the next one. @param mapping: A dict of key/value pairs to set. @param time: Tells memcached the time which this value should expire, either as a delta number of seconds, or an absolute unix time-since-the-epoch value. See the memcached protocol docs section "Storage Commands" for more info on . We default to 0 == cache forever. @param key_prefix: Optional string to prepend to each key when sending to memcache. Allows you to efficiently stuff these keys into a pseudo-namespace in memcache: >>> notset_keys = mc.set_multi({'key1' : 'val1', 'key2' : 'val2'}, key_prefix='subspace_') >>> len(notset_keys) == 0 True >>> mc.get_multi(['subspace_key1', 'subspace_key2']) == {'subspace_key1' : 'val1', 'subspace_key2' : 'val2'} True Causes key 'subspace_key1' and 'subspace_key2' to be set. Useful in conjunction with a higher-level layer which applies namespaces to data in memcache. In this case, the return result would be the list of notset original keys, prefix not applied. @param min_compress_len: The threshold length to kick in auto-compression of the value using the zlib.compress() routine. If the value being cached is a string, then the length of the string is measured, else if the value is an object, then the length of the pickle result is measured. If the resulting attempt at compression yeilds a larger string than the input, then it is discarded. For backwards compatability, this parameter defaults to 0, indicating don't ever try to compress. @return: List of keys which failed to be stored [ memcache out of memory, etc. ]. @rtype: list ''' self._statlog('set_multi') server_keys, prefixed_to_orig_key = self._map_and_prefix_keys(mapping.iterkeys(), key_prefix) # send out all requests on each server before reading anything dead_servers = [] for server in server_keys.iterkeys(): bigcmd = [] write = bigcmd.append try: for key in server_keys[server]: # These are mangled keys store_info = self._val_to_store_info(mapping[prefixed_to_orig_key[key]], min_compress_len) write("set %s %d %d %d\r\n%s\r\n" % (key, store_info[0], time, store_info[1], store_info[2])) server.send_cmds(''.join(bigcmd)) except socket.error, msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) dead_servers.append(server) # if any servers died on the way, don't expect them to respond. for server in dead_servers: del server_keys[server] # short-circuit if there are no servers, just return all keys if not server_keys: return(mapping.keys()) notstored = [] # original keys. for server, keys in server_keys.iteritems(): try: for key in keys: line = server.readline() if line == 'STORED': continue else: notstored.append(prefixed_to_orig_key[key]) #un-mangle. except (_Error, socket.error), msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) return notstored def _val_to_store_info(self, val, min_compress_len): """ Transform val to a storable representation, returning a tuple of the flags, the length of the new value, and the new value itself. """ flags = 0 if isinstance(val, str): pass elif isinstance(val, int): flags |= Client._FLAG_INTEGER val = "%d" % val # force no attempt to compress this silly string. min_compress_len = 0 elif isinstance(val, long): flags |= Client._FLAG_LONG val = "%d" % val # force no attempt to compress this silly string. min_compress_len = 0 else: flags |= Client._FLAG_PICKLE file = StringIO() if self.picklerIsKeyword: pickler = self.pickler(file, protocol = self.pickleProtocol) else: pickler = self.pickler(file, self.pickleProtocol) if self.persistent_id: pickler.persistent_id = self.persistent_id pickler.dump(val) val = file.getvalue() lv = len(val) # We should try to compress if min_compress_len > 0 and we could # import zlib and this string is longer than our min threshold. if min_compress_len and _supports_compress and lv > min_compress_len: comp_val = compress(val) # Only retain the result if the compression result is smaller # than the original. if len(comp_val) < lv: flags |= Client._FLAG_COMPRESSED val = comp_val # silently do not store if value length exceeds maximum if self.server_max_value_length != 0 and \ len(val) >= self.server_max_value_length: return(0) return (flags, len(val), val) def _set(self, cmd, key, val, time, min_compress_len = 0): self.check_key(key) server, key = self._get_server(key) if not server: return 0 self._statlog(cmd) store_info = self._val_to_store_info(val, min_compress_len) if not store_info: return(0) if cmd == 'cas': if key not in self.cas_ids: return self._set('set', key, val, time, min_compress_len) fullcmd = "%s %s %d %d %d %d\r\n%s" % ( cmd, key, store_info[0], time, store_info[1], self.cas_ids[key], store_info[2]) else: fullcmd = "%s %s %d %d %d\r\n%s" % ( cmd, key, store_info[0], time, store_info[1], store_info[2]) try: server.send_cmd(fullcmd) return(server.expect("STORED") == "STORED") except socket.error, msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) return 0 def _get(self, cmd, key): self.check_key(key) server, key = self._get_server(key) if not server: return None self._statlog(cmd) try: server.send_cmd("%s %s" % (cmd, key)) rkey = flags = rlen = cas_id = None if cmd == 'gets': rkey, flags, rlen, cas_id, = self._expect_cas_value(server) if rkey: self.cas_ids[rkey] = cas_id else: rkey, flags, rlen, = self._expectvalue(server) if not rkey: return None value = self._recv_value(server, flags, rlen) server.expect("END") except (_Error, socket.error), msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) return None return value def get(self, key): '''Retrieves a key from the memcache. @return: The value or None. ''' return self._get('get', key) def gets(self, key): '''Retrieves a key from the memcache. Used in conjunction with 'cas'. @return: The value or None. ''' return self._get('gets', key) def get_multi(self, keys, key_prefix=''): ''' Retrieves multiple keys from the memcache doing just one query. >>> success = mc.set("foo", "bar") >>> success = mc.set("baz", 42) >>> mc.get_multi(["foo", "baz", "foobar"]) == {"foo": "bar", "baz": 42} 1 >>> mc.set_multi({'k1' : 1, 'k2' : 2}, key_prefix='pfx_') == [] 1 This looks up keys 'pfx_k1', 'pfx_k2', ... . Returned dict will just have unprefixed keys 'k1', 'k2'. >>> mc.get_multi(['k1', 'k2', 'nonexist'], key_prefix='pfx_') == {'k1' : 1, 'k2' : 2} 1 get_mult [ and L{set_multi} ] can take str()-ables like ints / longs as keys too. Such as your db pri key fields. They're rotored through str() before being passed off to memcache, with or without the use of a key_prefix. In this mode, the key_prefix could be a table name, and the key itself a db primary key number. >>> mc.set_multi({42: 'douglass adams', 46 : 'and 2 just ahead of me'}, key_prefix='numkeys_') == [] 1 >>> mc.get_multi([46, 42], key_prefix='numkeys_') == {42: 'douglass adams', 46 : 'and 2 just ahead of me'} 1 This method is recommended over regular L{get} as it lowers the number of total packets flying around your network, reducing total latency, since your app doesn't have to wait for each round-trip of L{get} before sending the next one. See also L{set_multi}. @param keys: An array of keys. @param key_prefix: A string to prefix each key when we communicate with memcache. Facilitates pseudo-namespaces within memcache. Returned dictionary keys will not have this prefix. @return: A dictionary of key/value pairs that were available. If key_prefix was provided, the keys in the retured dictionary will not have it present. ''' self._statlog('get_multi') server_keys, prefixed_to_orig_key = self._map_and_prefix_keys(keys, key_prefix) # send out all requests on each server before reading anything dead_servers = [] for server in server_keys.iterkeys(): try: server.send_cmd("get %s" % " ".join(server_keys[server])) except socket.error, msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) dead_servers.append(server) # if any servers died on the way, don't expect them to respond. for server in dead_servers: del server_keys[server] retvals = {} for server in server_keys.iterkeys(): try: line = server.readline() while line and line != 'END': rkey, flags, rlen = self._expectvalue(server, line) # Bo Yang reports that this can sometimes be None if rkey is not None: val = self._recv_value(server, flags, rlen) retvals[prefixed_to_orig_key[rkey]] = val # un-prefix returned key. line = server.readline() except (_Error, socket.error), msg: if isinstance(msg, tuple): msg = msg[1] server.mark_dead(msg) return retvals def _expect_cas_value(self, server, line=None): if not line: line = server.readline() if line[:5] == 'VALUE': resp, rkey, flags, len, cas_id = line.split() return (rkey, int(flags), int(len), int(cas_id)) else: return (None, None, None, None) def _expectvalue(self, server, line=None): if not line: line = server.readline() if line[:5] == 'VALUE': resp, rkey, flags, len = line.split() flags = int(flags) rlen = int(len) return (rkey, flags, rlen) else: return (None, None, None) def _recv_value(self, server, flags, rlen): rlen += 2 # include \r\n buf = server.recv(rlen) if len(buf) != rlen: raise _Error("received %d bytes when expecting %d" % (len(buf), rlen)) if len(buf) == rlen: buf = buf[:-2] # strip \r\n if flags & Client._FLAG_COMPRESSED: buf = decompress(buf) if flags == 0 or flags == Client._FLAG_COMPRESSED: # Either a bare string or a compressed string now decompressed... val = buf elif flags & Client._FLAG_INTEGER: val = int(buf) elif flags & Client._FLAG_LONG: val = long(buf) elif flags & Client._FLAG_PICKLE: try: file = StringIO(buf) unpickler = self.unpickler(file) if self.persistent_load: unpickler.persistent_load = self.persistent_load val = unpickler.load() except Exception, e: self.debuglog('Pickle error: %s\n' % e) val = None else: self.debuglog("unknown flags on get: %x\n" % flags) return val def check_key(self, key, key_extra_len=0): """Checks sanity of key. Fails if: Key length is > SERVER_MAX_KEY_LENGTH (Raises MemcachedKeyLength). Contains control characters (Raises MemcachedKeyCharacterError). Is not a string (Raises MemcachedStringEncodingError) Is an unicode string (Raises MemcachedStringEncodingError) Is not a string (Raises MemcachedKeyError) Is None (Raises MemcachedKeyError) """ if isinstance(key, tuple): key = key[1] if not key: raise Client.MemcachedKeyNoneError("Key is None") if isinstance(key, unicode): raise Client.MemcachedStringEncodingError( "Keys must be str()'s, not unicode. Convert your unicode " "strings using mystring.encode(charset)!") if not isinstance(key, str): raise Client.MemcachedKeyTypeError("Key must be str()'s") if isinstance(key, basestring): if self.server_max_key_length != 0 and \ len(key) + key_extra_len > self.server_max_key_length: raise Client.MemcachedKeyLengthError("Key length is > %s" % self.server_max_key_length) for char in key: if ord(char) < 33 or ord(char) == 127: raise Client.MemcachedKeyCharacterError( "Control characters not allowed") class _Host(object): _DEAD_RETRY = 30 # number of seconds before retrying a dead server. _SOCKET_TIMEOUT = 3 # number of seconds before sockets timeout. def __init__(self, host, debug=0): self.debug = debug if isinstance(host, tuple): host, self.weight = host else: self.weight = 1 # parse the connection string m = re.match(r'^(?Punix):(?P.*)$', host) if not m: m = re.match(r'^(?Pinet):' r'(?P[^:]+)(:(?P[0-9]+))?$', host) if not m: m = re.match(r'^(?P[^:]+):(?P[0-9]+)$', host) if not m: raise ValueError('Unable to parse connection string: "%s"' % host) hostData = m.groupdict() if hostData.get('proto') == 'unix': self.family = socket.AF_UNIX self.address = hostData['path'] else: self.family = socket.AF_INET self.ip = hostData['host'] self.port = int(hostData.get('port', 11211)) self.address = ( self.ip, self.port ) self.deaduntil = 0 self.socket = None self.buffer = '' def debuglog(self, str): if self.debug: sys.stderr.write("MemCached: %s\n" % str) def _check_dead(self): if self.deaduntil and self.deaduntil > time.time(): return 1 self.deaduntil = 0 return 0 def connect(self): if self._get_socket(): return 1 return 0 def mark_dead(self, reason): self.debuglog("MemCache: %s: %s. Marking dead." % (self, reason)) self.deaduntil = time.time() + _Host._DEAD_RETRY self.close_socket() def _get_socket(self): if self._check_dead(): return None if self.socket: return self.socket s = socket.socket(self.family, socket.SOCK_STREAM) if hasattr(s, 'settimeout'): s.settimeout(self._SOCKET_TIMEOUT) try: s.connect(self.address) except socket.timeout, msg: self.mark_dead("connect: %s" % msg) return None except socket.error, msg: if isinstance(msg, tuple): msg = msg[1] self.mark_dead("connect: %s" % msg[1]) return None self.socket = s self.buffer = '' return s def close_socket(self): if self.socket: self.socket.close() self.socket = None def send_cmd(self, cmd): self.socket.sendall(cmd + '\r\n') def send_cmds(self, cmds): """ cmds already has trailing \r\n's applied """ self.socket.sendall(cmds) def readline(self): buf = self.buffer recv = self.socket.recv while True: index = buf.find('\r\n') if index >= 0: break data = recv(4096) if not data: self.mark_dead('Connection closed while reading from %s' % repr(self)) self.buffer = '' return None buf += data self.buffer = buf[index+2:] return buf[:index] def expect(self, text): line = self.readline() if line != text: self.debuglog("while expecting '%s', got unexpected response '%s'" % (text, line)) return line def recv(self, rlen): self_socket_recv = self.socket.recv buf = self.buffer while len(buf) < rlen: foo = self_socket_recv(max(rlen - len(buf), 4096)) buf += foo if not foo: raise _Error( 'Read %d bytes, expecting %d, ' 'read returned 0 length bytes' % ( len(buf), rlen )) self.buffer = buf[rlen:] return buf[:rlen] def __str__(self): d = '' if self.deaduntil: d = " (dead until %d)" % self.deaduntil if self.family == socket.AF_INET: return "inet:%s:%d%s" % (self.address[0], self.address[1], d) else: return "unix:%s%s" % (self.address, d) def _doctest(): import doctest, memcache servers = ["127.0.0.1:11211"] mc = Client(servers, debug=1) globs = {"mc": mc} return doctest.testmod(memcache, globs=globs) if __name__ == "__main__": failures = 0 print "Testing docstrings..." _doctest() print "Running tests:" print serverList = [["127.0.0.1:11211"]] if '--do-unix' in sys.argv: serverList.append([os.path.join(os.getcwd(), 'memcached.socket')]) for servers in serverList: mc = Client(servers, debug=1) def to_s(val): if not isinstance(val, basestring): return "%s (%s)" % (val, type(val)) return "%s" % val def test_setget(key, val): print "Testing set/get {'%s': %s} ..." % (to_s(key), to_s(val)), mc.set(key, val) newval = mc.get(key) if newval == val: print "OK" return 1 else: print "FAIL"; failures = failures + 1 return 0 class FooStruct(object): def __init__(self): self.bar = "baz" def __str__(self): return "A FooStruct" def __eq__(self, other): if isinstance(other, FooStruct): return self.bar == other.bar return 0 test_setget("a_string", "some random string") test_setget("an_integer", 42) if test_setget("long", long(1<<30)): print "Testing delete ...", if mc.delete("long"): print "OK" else: print "FAIL"; failures = failures + 1 print "Testing get_multi ...", print mc.get_multi(["a_string", "an_integer"]) print "Testing get(unknown value) ...", print to_s(mc.get("unknown_value")) f = FooStruct() test_setget("foostruct", f) print "Testing incr ...", x = mc.incr("an_integer", 1) if x == 43: print "OK" else: print "FAIL"; failures = failures + 1 print "Testing decr ...", x = mc.decr("an_integer", 1) if x == 42: print "OK" else: print "FAIL"; failures = failures + 1 sys.stdout.flush() # sanity tests print "Testing sending spaces...", sys.stdout.flush() try: x = mc.set("this has spaces", 1) except Client.MemcachedKeyCharacterError, msg: print "OK" else: print "FAIL"; failures = failures + 1 print "Testing sending control characters...", try: x = mc.set("this\x10has\x11control characters\x02", 1) except Client.MemcachedKeyCharacterError, msg: print "OK" else: print "FAIL"; failures = failures + 1 print "Testing using insanely long key...", try: x = mc.set('a'*SERVER_MAX_KEY_LENGTH + 'aaaa', 1) except Client.MemcachedKeyLengthError, msg: print "OK" else: print "FAIL"; failures = failures + 1 print "Testing sending a unicode-string key...", try: x = mc.set(u'keyhere', 1) except Client.MemcachedStringEncodingError, msg: print "OK", else: print "FAIL",; failures = failures + 1 try: x = mc.set((u'a'*SERVER_MAX_KEY_LENGTH).encode('utf-8'), 1) except: print "FAIL",; failures = failures + 1 else: print "OK", import pickle s = pickle.loads('V\\u4f1a\np0\n.') try: x = mc.set((s*SERVER_MAX_KEY_LENGTH).encode('utf-8'), 1) except Client.MemcachedKeyLengthError: print "OK" else: print "FAIL"; failures = failures + 1 print "Testing using a value larger than the memcached value limit...", x = mc.set('keyhere', 'a'*SERVER_MAX_VALUE_LENGTH) if mc.get('keyhere') == None: print "OK", else: print "FAIL",; failures = failures + 1 x = mc.set('keyhere', 'a'*SERVER_MAX_VALUE_LENGTH + 'aaa') if mc.get('keyhere') == None: print "OK" else: print "FAIL"; failures = failures + 1 print "Testing set_multi() with no memcacheds running", mc.disconnect_all() errors = mc.set_multi({'keyhere' : 'a', 'keythere' : 'b'}) if errors != []: print "FAIL"; failures = failures + 1 else: print "OK" print "Testing delete_multi() with no memcacheds running", mc.disconnect_all() ret = mc.delete_multi({'keyhere' : 'a', 'keythere' : 'b'}) if ret != 1: print "FAIL"; failures = failures + 1 else: print "OK" if failures > 0: print '*** THERE WERE FAILED TESTS' sys.exit(1) sys.exit(0) # vim: ts=4 sw=4 et : Chula-0.7.0/chula/__init__.py0000644000175000017500000000050011412545636016747 0ustar jmcfarlanejmcfarlane"""Chula is a lightweight toolkit for writing web applications in Python It's designed to work with WSGI, FastCGI, or mod_python. Chula only handles the nuts and bolts - meaning there isn't any ORM layer, and no code generation. """ __VERSION__ = '0.7.0' version = __VERSION__ package_dir = 'chula' data_dir = 'sql' Chula-0.7.0/chula/regex.py0000644000175000017500000000124111370740256016323 0ustar jmcfarlanejmcfarlane""" Common regular expressions """ import re def match(regex, test): if re.search(regex, test) is None: return False else: return True IPV4 = (r'^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.)' r'{3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$') PASSWD = r'^[-a-zA-Z0-9!@#$%^&?.*]{6,}$' EMAIL = (r'^[a-z0-9_\-]+(\.[_a-z0-9\-]+)*' r'@([_a-z0-9\-]+\.)+([a-z]{2}' r'|aero|arpa|biz|com|coop|edu|gov|info|int|jobs|mil' r'|museum|name|nato|net|org|pro|travel)$') TAG_CHARS = r'a-zA-Z0-9-_' TAG_MATCH = r'[%s]+' % TAG_CHARS TAG = r'^%s$' % TAG_MATCH TAGS = r'^%s((, ?| )+%s)*$' % (TAG_MATCH, TAG_MATCH) Chula-0.7.0/tests/0000755000175000017500000000000011412546122014700 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/bat/0000755000175000017500000000000011412546122015446 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/bat/test_sample_controller.py0000644000175000017500000000143711370740256022617 0ustar jmcfarlanejmcfarlanefrom chula.test import bat HTML = 'Sample controller' class Test_sample_controller(bat.Bat): def test_root(self): retval = self.request('/sample') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 200) def test_with_slash(self): retval = self.request('/sample/') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 200) def test_method_specified(self): retval = self.request('/sample/page') self.assertEquals(retval.data, HTML + ':page') self.assertEquals(retval.status, 200) def test_method_specified_with_slash(self): retval = self.request('/sample/page/') self.assertEquals(retval.data, HTML + ':page') self.assertEquals(retval.status, 200) Chula-0.7.0/tests/bat/errors/0000755000175000017500000000000011412546122016762 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/bat/errors/test_global_exception.py0000644000175000017500000000052111370740256023716 0ustar jmcfarlanejmcfarlanefrom chula.test import bat class Test_global_exception(bat.Bat): def test_root(self): retval = self.request('/imports/global_exception/index') self.assertTrue(retval.data.find('Trapped Error') >= 0) self.assertTrue(retval.data.find('variable_not_defined') >= 0) self.assertEquals(retval.status, 500) Chula-0.7.0/tests/bat/errors/test_syntax_exception.py0000644000175000017500000000063111370740256024006 0ustar jmcfarlanejmcfarlanefrom chula.test import bat class Test_syntax_exception(bat.Bat): def test_root(self): retval = self.request('/imports/syntax_exception/index') self.assertTrue(retval.data.find('Trapped Error') >= 0) self.assertTrue(retval.data.find('invalid syntax') >= 0) self.assertTrue(retval.data.find('syntax_exception.py, line 5') >= 0) self.assertEquals(retval.status, 500) Chula-0.7.0/tests/bat/errors/test_missing_controller.py0000644000175000017500000000203211370740256024313 0ustar jmcfarlanejmcfarlanefrom chula.test import bat HTML = 'Page not found' class Test_missing_controller(bat.Bat): def test_root(self): retval = self.request('/missing_controller') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 404) def test_root_with_slash(self): retval = self.request('/missing_controller/') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 404) def test_method_specified(self): retval = self.request('/missing_controller/foobar') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 404) def test_package_method_specified(self): retval = self.request('/foopackage/foocontroller/foomethod') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 404) def test_deep_package_method_specified(self): retval = self.request('/foo/bar/black/red/blue/green/white') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 404) Chula-0.7.0/tests/bat/errors/test_bad_import.py0000644000175000017500000000051311370740256022521 0ustar jmcfarlanejmcfarlanefrom chula.test import bat class Test_bad_import(bat.Bat): def test_root(self): retval = self.request('/imports/bad_import/index') self.assertTrue(retval.data.find('Trapped Error') >= 0) self.assertTrue(retval.data.find('intentionally') >= 0, retval.data) self.assertEquals(retval.status, 500) Chula-0.7.0/tests/bat/test_homepage.py0000644000175000017500000000216511370740256020657 0ustar jmcfarlanejmcfarlanefrom chula.test import bat HTML = 'Hello world' class Test_homepage(bat.Bat): def test_root(self): retval = self.request('') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 200) def test_with_slash(self): retval = self.request('/') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 200) def test_controller_specified(self): retval = self.request('/home') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 200) def test_controller_specified_with_slash(self): retval = self.request('/home/') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 200) def test_controller_fq_specified(self): retval = self.request('/home/index') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 200) def test_controller_fq_specified_with_slash(self): retval = self.request('/home/index/') self.assertEquals(retval.data, HTML) self.assertEquals(retval.status, 200) Chula-0.7.0/tests/unit/0000755000175000017500000000000011412546122015657 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/db/0000755000175000017500000000000011412546122016244 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/db/test_sqlite.py0000644000175000017500000000156111370740256021170 0ustar jmcfarlanejmcfarlaneimport unittest from chula.error import * from chula.db import datastore from chula.db.engines import sqlite class Test_sqlite(unittest.TestCase): doctest = sqlite def setUp(self): self.db = datastore.DataStoreFactory('sqlite:memory') self.cursor = self.db.cursor() def tearDown(self): self.cursor.close() self.db.close() def test_default_isolation_level(self): self.assertEquals(None, self.db.conn.isolation_level) def test_invalid_isolation_level(self): self.assertEquals(3, 3) self.assertRaises(InvalidAttributeError, self.db.set_isolation, 'awesome') def test_specified_isolation_level(self): isolation = 'DEFERRED' db = datastore.DataStoreFactory('sqlite:memory', isolation=isolation) self.assertEquals(isolation, db.conn.isolation_level) Chula-0.7.0/tests/unit/db/test_couch.py0000644000175000017500000000406311370740256020770 0ustar jmcfarlanejmcfarlaneimport os import time import unittest from couchdb import Database, Server, ResourceNotFound from chula.error import * from chula.db import datastore from chula.db.engines import couch from chula.nosql import couch as couch_document SANDBOX = 'sandbox' SERVER = 'http://localhost:5984' class SampleDocument(couch_document.Document): DB = SANDBOX class SampleDocuments(couch_document.Documents): DB = SANDBOX class Test_couchdb(unittest.TestCase): doctest = couch def setUp(self): self.futon = datastore.DataStoreFactory('couchdb:%s' % SERVER) def tearDown(self): pass def connect(self, db=SANDBOX): self.db = self.futon.db(db) def test_connection_type(self): self.assertTrue(isinstance(self.futon.conn, Server)) def test_connection_with_existing_db(self): self.connect() self.assertTrue(isinstance(self.db, Database)) def test_connection_with_new_db(self): TEST = 'chula_datastore_test_%s' % str(time.time()).split('.')[0] self.connect(TEST) self.assertTrue(isinstance(self.db, Database)) self.futon.delete(TEST) class Test_couchdb_document(unittest.TestCase): doctest = couch_document def setUp(self): self.futon = datastore.DataStoreFactory('couchdb:%s' % SERVER) def tearDown(self): pass def connect(self, db=SANDBOX): self.db = self.futon.db(db) def test_document(self): NAME = 'foobar' doc = SampleDocument(NAME, server=SERVER) doc['test'] = 'testing' doc.persist() doc = SampleDocument(NAME, server=SERVER) doc['test'] = 'testing again' doc.persist() def test_document_using_env_var(self): os.environ[couch_document.ENV] = SERVER doc = SampleDocument('foobar') doc.persist() def test_documents(self): func = """ function (doc) { emit(doc._id, doc) } """ for doc in SampleDocuments(server=SERVER).query(func): self.assertTrue(isinstance(doc, dict)) Chula-0.7.0/tests/unit/db/test_functions.py0000644000175000017500000002127211370740256021700 0ustar jmcfarlanejmcfarlane""" db functions unit tests """ #test_functions.py - Class to test generic python functions # #Copyright (C) 2005 John McFarlane # #This program is free software; you can redistribute it and/or modify #it under the terms of the GNU General Public License as published by #the Free Software Foundation; either version 2 of the License, or #(at your option) any later version. # #This program is distributed in the hope that it will be useful, #but WITHOUT ANY WARRANTY; without even the implied warranty of #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #GNU General Public License for more details. # #You should have received a copy of the GNU General Public License #along with this program; if not, write to the Free Software #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # import unittest import datetime from chula.db import functions as fcn from chula.db.engines import postgresql, sqlite from chula.db.datastore import DataStoreFactory from chula.error import * class Test_functions(unittest.TestCase): """A test class for the db module""" doctest = fcn def _DS(self, conn): return DataStoreFactory(conn) def setUp(self): self.int = 7 self.str = "foobar" def test_cbool(self): self.assertEqual(fcn.cbool(True), 'TRUE') self.assertEqual(fcn.cbool(1), 'TRUE') self.assertEqual(fcn.cbool('1'), 'TRUE') self.assertEqual(fcn.cbool('true'), 'TRUE') self.assertEqual(fcn.cbool('t'), 'TRUE') self.assertEqual(fcn.cbool('y'), 'TRUE') self.assertEqual(fcn.cbool('On'), 'TRUE') self.assertEqual(fcn.cbool(False), 'FALSE') self.assertEqual(fcn.cbool(0), 'FALSE') self.assertEqual(fcn.cbool('0'), 'FALSE') self.assertEqual(fcn.cbool('false'), 'FALSE') self.assertEqual(fcn.cbool('f'), 'FALSE') self.assertEqual(fcn.cbool('n'), 'FALSE') self.assertEqual(fcn.cbool('Off'), 'FALSE') self.assertEqual(fcn.cbool(None), 'NULL') self.assertEqual(fcn.cbool(''), 'NULL') self.assertRaises(TypeConversionError, fcn.cbool, datetime.datetime.now()) def test_cdate(self): self.assertEqual(fcn.cdate(None), 'NULL') self.assertEqual(fcn.cdate(''), 'NULL') self.assertEqual(fcn.cdate('1/1/2005'), "'1/1/2005'") self.assertEqual(fcn.cdate('now()', isfunction=True), 'now()') # Date with time self.assertEqual(fcn.cdate('1/1/2005 10:00:00'), "'1/1/2005 10:00:00'") self.assertEqual(fcn.cdate('1-1-2005 10:00:00'), "'1-1-2005 10:00:00'") self.assertEqual(fcn.cdate('2005-1-1 10:00:00'), "'2005-1-1 10:00:00'") self.assertEqual(fcn.cdate('2008-7-16 5:50:20'), "'2008-7-16 5:50:20'") # Not leap year self.assertEqual(fcn.cdate('2/29/2008'), "'2/29/2008'") self.assertRaises(TypeConversionError, fcn.cdate, 2) self.assertRaises(TypeConversionError, fcn.cdate, '1/41/2005') # Leap year self.assertRaises(TypeConversionError, fcn.cdate, '2/29/2006') def test_cfloat(self): self.assertEqual(fcn.cfloat(None), 'NULL') self.assertEqual(fcn.cfloat(''), 'NULL') self.assertEqual(fcn.cfloat(35), 35) self.assertEqual(fcn.cfloat('35'), 35) self.assertEqual(fcn.cfloat(35.00), 35.00) self.assertEqual(fcn.cfloat(35.000000001), 35.000000001) self.assertEqual(fcn.cfloat('35.000000001'), 35.000000001) self.assertEqual(fcn.cfloat(True), 1.0) self.assertEqual(fcn.cfloat(False), 0.0) self.assertRaises(TypeConversionError, fcn.cfloat, '35a') def test_cint(self): self.assertEqual(fcn.cint(None), 'NULL') self.assertEqual(fcn.cint(''), 'NULL') self.assertEqual(fcn.cint(35), 35) self.assertEqual(fcn.cint(True), 1) self.assertEqual(fcn.cint(False), 0) self.assertEqual(fcn.cint('35'), 35) self.assertEqual(fcn.cint('Null'), 'NULL') self.assertRaises(TypeConversionError, fcn.cint, '35a') def test_connection_factory_postgresql(self): conn = DataStoreFactory('pg:chula@localhost/chula_test', 'chula') self.assertEquals(True, isinstance(conn, postgresql.DataStore)) def test_connection_factory_sqlite(self): conn = self._DS('sqlite:/tmp/chula_sqlite_test.db') self.assertEquals(True, isinstance(conn, sqlite.DataStore)) def test_connection_string_invalid_type(self): conn = 'oracle:localhost/chula_test' self.assertRaises(UnsupportedDatabaseEngineError, self._DS, conn) def test_connection_string_missing_user(self): conn = 'pg:localhost/chula_test' self.assertRaises(MalformedConnectionStringError, self._DS, conn) def test_connection_string_missing_host(self): conn = 'pg:chula/chula_test' self.assertRaises(MalformedConnectionStringError, self._DS, conn) def test_connection_string_missing_db(self): conn = 'pg:chula@localhost' self.assertRaises(MalformedConnectionStringError, self._DS, conn) def test_cstr(self): clean = fcn.cstr self.assertEqual(clean(None), 'NULL') self.assertEqual(clean('Null'), "'Null'") self.assertEqual(clean('NULL'), "'NULL'") self.assertEqual(clean(''), "''") self.assertEqual(clean(""), "''") self.assertEqual(clean("''''"), "''''''''''") self.assertEqual(clean("b'a"), "'b''a'") self.assertEqual(clean("b''a"), "'b''''a'") self.assertEqual(clean(r"abc\defg"), r"'abc\\defg'") self.assertEqual(clean(r"b\a"), r"'b\\a'") self.assertEqual(clean(r"b\'a"), r"'b\\''a'") self.assertEqual(clean("don't"), "'don''t'") self.assertEqual(clean("don''t"), "'don''''t'") self.assertEqual(clean('a'), "'a'") self.assertEqual(clean("a'"), "'a'''") self.assertEqual(clean("a'", doquote=False), "a''") self.assertEqual(clean("a'", doquote=False, doescape=False), "a'") self.assertEqual(clean(5), "'5'") self.assertEqual(clean(True), "'True'") self.assertEqual(clean(False), "'False'") def test_cregex(self): self.assertEqual(fcn.cregex(r'.*'), "'.*'") self.assertEqual(fcn.cregex(r''), "''") self.assertEqual(fcn.cregex(r'.*', doquote=False), ".*") self.assertRaises(TypeConversionError, fcn.cregex, '[') self.assertRaises(TypeConversionError, fcn.cregex, None) def test_ctags(self): self.assertEqual(fcn.ctags(''), "NULL") self.assertEqual(fcn.ctags(None), "NULL") self.assertEqual(fcn.ctags('abc'), "'abc'") self.assertEqual(fcn.ctags('Abc'), "'abc'") self.assertEqual(fcn.ctags('a b c'), "'a b c'") self.assertEqual(fcn.ctags(['c', 'b', 'a']), "'a b c'") self.assertRaises(TypeConversionError, fcn.ctags, 'abc!') self.assertRaises(TypeConversionError, fcn.ctags, 4) def test_datatore_basic(self): conn = DataStoreFactory('pg:chula@localhost/chula_test', 'chula') cursor = conn.cursor() try: cursor.execute('SELECT * FROM cars LIMIT 1;') keys = cursor.fetchone().keys() keys.sort() self.assertEquals(keys, ['make', 'model', 'uid']) finally: conn.rollback() conn.close() def test_datatore_tuple(self): conn = DataStoreFactory('pg:chula@localhost/chula_test', 'chula') cursor = conn.cursor(type='tuple') try: cursor.execute('SELECT * FROM cars LIMIT 1;') values = cursor.fetchone() self.assertEquals(values, (1, 'Honda', 'Civic')) finally: conn.rollback() conn.close() def test_empty2null(self): self.assertEqual(fcn.empty2null(''), 'NULL') self.assertEqual(fcn.empty2null(""), 'NULL') self.assertEqual(fcn.empty2null('a'), 'a') self.assertEqual(fcn.empty2null(2), 2) def test_invalid_cursor(self): conn = DataStoreFactory('pg:chula@localhost/chula_test', 'chula') self.assertRaises(UnsupportedUsageError, conn.cursor, type='list') conn.close() def test_unquote(self): self.assertEqual(fcn.unquote("'abc'"), "abc") self.assertEqual(fcn.unquote('"abc"'), '"abc"') self.assertEqual(fcn.unquote("'ABC'"), "ABC") self.assertEqual(fcn.unquote("'a'bc'"), "a'bc") self.assertEqual(fcn.unquote("'a\"bc'"), "a\"bc") self.assertEqual(fcn.unquote("'x'"), "x") self.assertEqual(fcn.unquote("abc"), "abc") self.assertEqual(fcn.unquote(None), None) self.assertEqual(fcn.unquote(5), 5) self.assertEqual(fcn.unquote('5'), '5') Chula-0.7.0/tests/unit/db/__init__.py0000644000175000017500000000000011370740256020352 0ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/www/0000755000175000017500000000000011412546122016503 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/www/mapper/0000755000175000017500000000000011412546122017767 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/www/mapper/__init__.py0000644000175000017500000000000011370740256022075 0ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/www/mapper/test_classpath.py0000644000175000017500000000402611370740256023373 0ustar jmcfarlanejmcfarlaneimport unittest from chula import config from chula.www.adapters.mod_python import fakerequest from chula.www.mapper import classpath class Test_classpath(unittest.TestCase): doctest = classpath def setUp(self): req = fakerequest.FakeRequest() cfg = config.Config() cfg.classpath = 'package' cfg.error_controller = 'error' self.mapper = classpath.ClassPathMapper(cfg, req) def tearDown(self): pass def test_homepage(self): self.mapper.uri = '/' self.mapper.default_route() self.mapper.parse() self.assertEquals(self.mapper.route.package, 'package') self.assertEquals(self.mapper.route.module, 'home') self.assertEquals(self.mapper.route.class_name, 'Home') self.assertEquals(self.mapper.route.method, 'index') def test_module_with_named_method(self): self.mapper.uri = '/module/method/' self.mapper.default_route() self.mapper.parse() self.assertEquals(self.mapper.route.package, 'package') self.assertEquals(self.mapper.route.module, 'module') self.assertEquals(self.mapper.route.class_name, 'Module') self.assertEquals(self.mapper.route.method, 'method') def test_module_with_implied_method(self): self.mapper.uri = '/module/' self.mapper.default_route() self.mapper.parse() self.assertEquals(self.mapper.route.package, 'package') self.assertEquals(self.mapper.route.module, 'module') self.assertEquals(self.mapper.route.class_name, 'Module') self.assertEquals(self.mapper.route.method, 'index') def test_package_with_named_method(self): self.mapper.uri = '/pkg/module/method/' self.mapper.default_route() self.mapper.parse() self.assertEquals(self.mapper.route.package, 'package.pkg') self.assertEquals(self.mapper.route.module, 'module') self.assertEquals(self.mapper.route.class_name, 'Module') self.assertEquals(self.mapper.route.method, 'method') Chula-0.7.0/tests/unit/www/mapper/test_regex.py0000644000175000017500000000425411370740256022526 0ustar jmcfarlanejmcfarlaneimport unittest from chula import config from chula.www.adapters.mod_python import fakerequest from chula.www.mapper import regex mapper = ( # Home controller (r'^/$', 'home.index'), (r'^/home/?$', 'home.index'), (r'^/home/index/?$', 'home.index'), # Sample controller (r'^/sample/?$', 'sample.index'), (r'^/sample/page/?$', 'sample.page'), ) class Test_regex(unittest.TestCase): doctest = regex def setUp(self): req = fakerequest.FakeRequest() cfg = config.Config() cfg.classpath = 'package' cfg.error_controller = 'error' self.mapper = regex.RegexMapper(cfg, req, mapper) def test_homepage(self): self.mapper.uri = '/' self.mapper.default_route() self.mapper.parse() self.assertEquals(self.mapper.route.package, 'package') self.assertEquals(self.mapper.route.module, 'home') self.assertEquals(self.mapper.route.class_name, 'Home') self.assertEquals(self.mapper.route.method, 'index') def test_regex_match(self): self.mapper.uri = '/sample' self.mapper.default_route() self.mapper.parse() self.assertEquals(self.mapper.route.package, 'package') self.assertEquals(self.mapper.route.module, 'sample') self.assertEquals(self.mapper.route.class_name, 'Sample') self.assertEquals(self.mapper.route.method, 'index') def test_regex_match_with_optional_slash(self): self.mapper.uri = '/sample/' self.mapper.default_route() self.mapper.parse() self.assertEquals(self.mapper.route.package, 'package') self.assertEquals(self.mapper.route.module, 'sample') self.assertEquals(self.mapper.route.class_name, 'Sample') self.assertEquals(self.mapper.route.method, 'index') def test_uri_without_any_match(self): self.mapper.uri = '/foo/bar/bla/' self.mapper.default_route() self.mapper.parse() self.assertEquals(self.mapper.route.package, 'package') self.assertEquals(self.mapper.route.module, 'error') self.assertEquals(self.mapper.route.class_name, 'Error') self.assertEquals(self.mapper.route.method, 'e404') Chula-0.7.0/tests/unit/www/__init__.py0000644000175000017500000000000011370740256020611 0ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/collection/0000755000175000017500000000000011412546122020012 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/collection/test_ubound.py0000644000175000017500000000230411370740256022725 0ustar jmcfarlanejmcfarlaneimport unittest from chula import collection from chula.collection import ubound from chula.error import * class Test_ubound_collection(unittest.TestCase): doctest = ubound def _fetch_by_key(self, key): return self.collection[key] def setUp(self): self.collection = collection.UboundCollection(5) self.collection.a = 1 self.collection.b = 2 self.collection.c = 3 self.collection.d = 4 self.collection.e = 5 def tearDown(self): pass def test_initial_length(self): self.collection = collection.UboundCollection(5) self.assertEquals(0, len(self.collection)) def test_allows_specified_max_records(self): self.assertEquals(5, len(self.collection)) self.assertEquals(1, self.collection.a) def test_purging_does_purge_FIFO_by_attr(self): self.collection.f = 6 self.assertEquals(5, len(self.collection)) self.assertRaises(KeyError, self._fetch_by_key, 'a') def test_purging_does_purge_FIFO_by_key(self): self.collection['f'] = 6 self.assertEquals(5, len(self.collection)) self.assertRaises(KeyError, self._fetch_by_key, 'a') Chula-0.7.0/tests/unit/collection/test_restricted.py0000644000175000017500000000602211370740256023602 0ustar jmcfarlanejmcfarlaneimport copy import cPickle import unittest from chula import collection, json from chula.collection import restricted from chula.error import * # Example usage of RestrictedCollection class to test it class Human(collection.RestrictedCollection): def __validkeys__(self): return ('head', 'leg', 'arm', 'foot') # TODO: Fix this huge mess, why can't the base class do it!! def __deepcopy__(self, memo={}): """ Return a fresh copy of a Collection object or subclass object """ fresh = Human() for key, value in self.iteritems(): fresh[key] = copy.deepcopy(value, memo) return fresh def __defaults__(self): self.head = 'wears hat' self.leg = 'two to walk with' self.arm = 'hold coffee with' self.foot = 'smell' class Test_restricted_collection(unittest.TestCase): doctest = restricted def setUp(self): self.human = Human() def test_key_with_defalt(self): self.assertEquals(self.human.head, 'wears hat') self.assertEquals(self.human.foot, 'smell') def test_key_no_default(self): def simulate(): class Human(collection.RestrictedCollection): def __validkeys__(self): return ('head', 'stomach') def __defaults__(self): self.head = 'wears hat' person = Human() self.assertRaises(RestrictecCollectionMissingDefaultAttrError, simulate) def test_get_invalid_attr(self): def simulate(): return self.human.missing self.assertRaises(InvalidCollectionKeyError, simulate) def test_get_invalid_dict(self): def simulate(): return self.human['missing'] self.assertRaises(InvalidCollectionKeyError, simulate) def test_set_invalid_attr(self): def simulate(): self.human.back = 'important' self.assertRaises(InvalidCollectionKeyError, simulate) def test_set_invalid_dict(self): def simulate(): self.human['back'] = 'important' self.assertRaises(InvalidCollectionKeyError, simulate) def test_deepcopy(self): person = copy.deepcopy(self.human) self.assertEquals(person.head, 'wears hat') self.assertEquals(person.foot, 'smell') self.assertEquals(True, isinstance(person, collection.Collection)) self.assertEquals(True, isinstance(person, collection.RestrictedCollection)) def test_json_encoding(self): encoded = json.encode(self.human) decoded = json.decode(encoded) self.assertEquals(decoded['foot'], self.human.foot) self.assertEquals(decoded['head'], self.human.head) def test_cpickle_encoding(self): encoded = cPickle.dumps(self.human) decoded = cPickle.loads(encoded) self.assertEquals(decoded['foot'], self.human.foot) self.assertEquals(decoded['head'], self.human.head) Chula-0.7.0/tests/unit/collection/test_base.py0000644000175000017500000000730011370740256022344 0ustar jmcfarlanejmcfarlaneimport copy import cPickle import unittest from chula import collection, json from chula.collection import base class Test_base_collection(unittest.TestCase): doctest = base def _get(self, key): return self.col[key] def setUp(self): self.col = collection.Collection() self.col.name = 'foo' self.col.location = 'bar' self.col.age = 25 def test_access_by_get(self): self.assertEquals(self.col.get('name'), 'foo') def test_access_by_attribute(self): self.assertEquals(self.col.name, 'foo') def test_access_by_dict(self): self.assertEquals(self.col['name'], 'foo') def test_access_by_iteritems(self): data = [] for key, value in self.col.iteritems(): data.append(key + ':' + str(value)) data.sort() expected = ['age:25', 'location:bar', 'name:foo'] self.assertEquals(data, expected) def test_encode_by_cpickle(self): encoded = cPickle.dumps(self.col) self.assertEquals(type(encoded), type('string')) def test_decode_by_cpickle(self): encoded = cPickle.dumps(self.col) decoded = cPickle.loads(encoded) self.assertEquals(decoded.age, self.col.age) self.assertEquals(decoded.location, self.col.location) self.assertEquals(decoded.name, self.col.name) self.assertEquals(type(decoded), type(collection.Collection())) def test_encode_by_json(self): encoded = json.encode(self.col) self.assertEquals(type(encoded), type('string')) def test_decode_by_json(self): encoded = json.encode(self.col) decoded = json.decode(encoded) self.assertEquals(decoded['age'], self.col.age) self.assertEquals(decoded['location'], self.col.location) self.assertEquals(decoded['name'], self.col.name) self.assertEquals(type(decoded), type({})) def test_is_iterable_by_keys(self): i = 0 keys = list(self.col.keys()) keys.sort() for thing in keys: if i == 0: self.assertEquals(thing, 'age') elif i == 1: self.assertEquals(thing, 'location') elif i == 2: self.assertEquals(thing, 'name') i += 1 def test_is_iterable_by_values(self): i = 0 values = list(self.col.values()) values.sort() for thing in values: if i == 0: self.assertEquals(thing, 25) elif i == 1: self.assertEquals(thing, 'bar') elif i == 2: self.assertEquals(thing, 'foo') i += 1 def test_set_by_add(self): self.col.add('foo', 'bar') self.assertEquals(self.col.foo, 'bar') def test_set_by_attr(self): self.col.foo = None self.assertEquals(self.col.foo, None) def test_set_by_dict(self): self.col['foo'] = None self.assertEquals(self.col.foo, None) def test_missing_key_raises_key_error(self): self.assertRaises(KeyError, self._get, 'missing') def test_can_delete_by_attr(self): self.col.foo = None del self.col.foo def test_can_delete_by_dict(self): del self.col['name'] self.assertRaises(KeyError, self._get, 'name') def test_can_delete_by_remove_method(self): self.col.remove('name') self.assertRaises(KeyError, self._get, 'name') def test_deepcopy(self): freshcopy = copy.deepcopy(self.col) self.assertEquals(freshcopy.name, 'foo') self.assertEquals(freshcopy.location, 'bar') self.assertEquals(freshcopy.age, 25) self.assertEquals(True, isinstance(freshcopy, collection.Collection)) Chula-0.7.0/tests/unit/collection/__init__.py0000644000175000017500000000000011370740256022120 0ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/test_config.py0000644000175000017500000000301711370740256020545 0ustar jmcfarlanejmcfarlaneimport unittest from chula import config from chula.error import * class Test_config(unittest.TestCase): doctest = config def d_set(self, key, value): self.config[key] = value def a_set(self, key, value): setattr(self.config, key, value) def d_get(self, key): foo = self.config[key] def a_get(self, key): foo = self.config.key def setUp(self): self.config = config.Config() def test_valid_key_set(self): self.config.session_memcache = ('') self.config['classpath'] = 'foo' def test_keys_method_available_and_working(self): self.assertEquals(len(self.config.keys()), len(config.Config.__validkeys__())) def test_printing_not_result_in_empty_dict(self): self.assertTrue(isinstance(self.config, dict)) self.assertNotEquals(str(self.config), '{}') def test_invalid_key_set_by_dict(self): self.assertRaises(InvalidCollectionKeyError, self.d_set, 'foo', 'bar') def test_invalid_key_set_by_attr(self): self.assertRaises(InvalidCollectionKeyError, self.a_set, 'foo', 'bar') def test_invalid_key_get_by_dict(self): self.assertRaises(InvalidCollectionKeyError, self.d_get, 'foo') def test_invalid_key_get_by_attr(self): self.assertRaises(InvalidCollectionKeyError, self.a_get, 'foo') def test_default_value_inforced_when_UNSET(self): error = RestrictecCollectionMissingDefaultAttrError self.assertRaises(error, self.d_get, 'classpath') Chula-0.7.0/tests/unit/queue/0000755000175000017500000000000011412546122017003 5ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/queue/test_message.py0000644000175000017500000000531711370740256022055 0ustar jmcfarlanejmcfarlaneimport unittest from chula import config from chula.queue import mqueue from chula.queue.messages import echo, mail, message config = config.Config() class Test_mqueue(unittest.TestCase): doctest = message def _add(self, module=echo): msg = module.Message() msg.message = 'payload' self.mqueue.add(msg) def setUp(self): self.mqueue = mqueue.MessageQueue(config) def tearDown(self): pass #self.mqueue.close() #def test_schema(self): # self.assertEquals(True, self.mqueue.schema_exists()) def test_add(self): self._add() #def test_add_empty_email(self): # mtype = 'email' # msg = message.MessageFactory(mtype) # self.assertEquals(mtype, msg.type) # self.failIf(isinstance(msg, email.Message) is False) #def test_add_dict(self): # mtype = 'email' # msg = {} # msg['id'] = 0 # msg['message'] = 'foobar' # msg['name'] = 'foobar' # msg['inprocess'] = True # msg['processed'] = False # msg['type'] = mtype # msg['created'] = '5/4/1971 14:00:00' # msg['updated'] = '5/4/1971 14:00:00' # # msg = message.MessageFactory(msg) # self.assertEquals(mtype, msg.type) # self.failIf(isinstance(msg, email.Message) is False) #def test_pop_returns_message(self): # self._add() # msg = self.mqueue.pop() # self.failIf(msg is None) # #def test_pop_return_msg_with_correct_fields(self): # keys_expected = ['id', # 'message', # 'name', # 'inprocess', # 'processed', # 'type', # 'created', # 'updated'] # keys_expected.sort() # self._add() # msg = self.mqueue.pop() # keys_found = msg.keys() # keys_found.sort() # self.assertEquals(keys_expected, keys_found) # self.assertEquals(True, isinstance(msg, message.Message)) #def test_pop_setting_inprocess(self): # self._add() # msg = self.mqueue.pop() # self.assertEquals(True, msg.inprocess) #def test_cannot_purge_unprocessed(self): # self._add() # msg = self.mqueue.pop() # # Force the processed flag to False # msg.inprocess = False # self.mqueue.persist(msg) # self.assertRaises(message.CannotPurgeUnprocessedError, # self.mqueue.purge, msg) #def test_successfull_purge(self): # self._add() # msg = self.mqueue.pop() # msg = self.mqueue.purge(msg) # msg = self.mqueue.pop() # self.assertEquals(None, msg) Chula-0.7.0/tests/unit/queue/__init__.py0000644000175000017500000000000011370740256021111 0ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/test_passwd.py0000644000175000017500000000205311370740256020600 0ustar jmcfarlanejmcfarlaneimport unittest from chula import passwd from chula.error import * password = 'gitisbetterthancvs' sha1 = 'NcA_puae9ca8739ddfd7db27c153015d39bc5d8c47b345' salt = sha1[:passwd.SALT_LENGTH] class Test_passwd(unittest.TestCase): doctest = passwd def test_new_password_with_known_hash(self): self.assertEquals(sha1, passwd.hash(password, salt=salt)) def test_similar_passwords_are_unique_with_salt(self): self.assertNotEqual(passwd.hash(password, salt=salt), passwd.hash('cookiemonseterMM', salt=salt)) def test_similar_passwords_are_unique_without_salt(self): self.assertNotEqual(passwd.hash(password), passwd.hash('cookiemonseterMM')) def test_malformed_password(self): self.assertRaises(MalformedPasswordError, passwd.hash, 'a') def test_new_matches_with_positive_match(self): self.assertTrue(passwd.matches(password, sha1)) def test_new_matches_without_positive_match(self): self.assertFalse(passwd.matches('badpasswd', sha1)) Chula-0.7.0/tests/unit/test_pager.py0000644000175000017500000000700011370740256020372 0ustar jmcfarlanejmcfarlaneimport unittest from chula import pager class Test_pager(unittest.TestCase): doctest = pager def callable(self, a, b, c, d): page = pager.Pager(a, b, c, d) def is_well_formed(self, pager, pagecount): selectedcount = 0 for page in pager: # Keep track of selected pages, we test this below... if page['isselected']: selectedcount += 1 # Test that the right keys come back if ['isselected', 'number', 'offset'] != page.keys(): raise KeyError, page # Test that exactly one page is selected if selectedcount != 1: msg = 'There can only be one selected page, not:%s' raise ValueError, msg % selectedcount # Test that there are the right number of pages if len(pager) != pagecount: msg = 'Invalid number of pages (%s), should have been: %s' raise IndexError, msg % (len(pager), pagecount) return True def test_visiblepages_must_be_odd(self): self.assertRaises(ValueError, self.callable, 0, 10, 10, 20) def test_offset_cannot_be_less_than_zero(self): self.assertRaises(ValueError, self.callable, -1, 10, 10, 20) def test_recordcount_is_zero(self): p = pager.Pager(0, 0, 5, 5) self.assertEquals([], p) def test_start_must_be_less_than_recordcount(self): self.assertRaises(ValueError, self.callable, 10, 10, 10, 20) def test_1st_page_should_be_selected(self): p = pager.Pager(0, 50, 5, 5) self.assertEquals(True, self.is_well_formed(p, 5)) self.assertEquals(True, p[0]['isselected']) self.assertEquals(20, p[-1]['offset']) def test_2nd_page_should_be_selected(self): p = pager.Pager(6, 50, 5, 5) self.assertEquals(True, self.is_well_formed(p, 5)) self.assertEquals(True, p[1]['isselected']) self.assertEquals(20, p[-1]['offset']) def test_middle_page_should_be_selected(self): p = pager.Pager(20, 50, 5, 5) self.assertEquals(True, self.is_well_formed(p, 5)) self.assertEquals(True, p[2]['isselected']) def test_shift_left_by_01_pages(self): p = pager.Pager(100, 300, 10, 19) self.assertEquals(True, self.is_well_formed(p, 19)) self.assertEquals(10, p[0]['offset']) def test_shift_left_by_02_pages(self): p = pager.Pager(110, 300, 10, 19) self.assertEquals(True, self.is_well_formed(p, 19)) self.assertEquals(20, p[0]['offset']) def test_stop_shifting(self): p = pager.Pager(0, 100, 5, 5) self.assertEquals(True, self.is_well_formed(p, 5)) self.assertEquals(0, p[0]['offset']) for start in xrange(85, 96, 5): p = pager.Pager(start, 100, 5, 5) self.assertEquals(True, self.is_well_formed(p, 5)) self.assertEquals(75, p[0]['offset']) self.assertEquals(80, p[1]['offset']) self.assertEquals(85, p[2]['offset']) self.assertEquals(90, p[3]['offset']) self.assertEquals(95, p[4]['offset']) def test_total_can_be_less_than_visiblepages(self): p = pager.Pager(0, 3, 10, 19) self.assertEquals(True, self.is_well_formed(p, 1)) self.assertEquals(0, p[0]['offset']) def test_total_less_than_02_pages(self): p = pager.Pager(0, 11, 10, 19) self.assertEquals(True, self.is_well_formed(p, 2)) self.assertEquals(0, p[0]['offset']) self.assertEquals(10, p[-1]['offset']) Chula-0.7.0/tests/unit/test_system.py0000644000175000017500000000057011370740256020625 0ustar jmcfarlanejmcfarlaneimport unittest from chula import system class Test_system(unittest.TestCase): doctest = system def setUp(self): self.system = system.System() def test_os_type_was_able_to_be_determined(self): self.failIf(self.system.type == 'UNKNOWN') def test_number_of_processors_able_to_be_determined(self): self.failIf(self.system.procs <= 0) Chula-0.7.0/tests/unit/test_webservice.py0000644000175000017500000001000611370740256021432 0ustar jmcfarlanejmcfarlaneimport cPickle import unittest from chula import collection, error, json, webservice from chula.www.adapters import env class Test_webservice(unittest.TestCase): doctest = webservice def setUp(self): # Sample environment self.env = env.BaseEnv() self.env.form = {} self.env.form_get = {} self.env.form_post = {} # Sample controller self.controller = collection.Collection() self.controller.env = self.env # Sample webservice payload self.payload = {'name':'Test User', 'cars':['honda', 'audi']} # Sample webservice instance holding the sample payload self.transport = webservice.Transport(self.controller) self.transport.data = self.payload def test_expose_default(self): @webservice.expose() def helper(foo): return self.payload response = json.decode(helper(self.controller)) self.assertEquals(response['data'], self.payload) self.assertEquals(response['success'], True) def test_expose_ascii_via_get(self): # Simulate a GET arg of 'transport' self.controller.env.form['transport'] = 'ascii' # Make the payload simpler self.payload = 'foo,bar,bla' self.transport.data = self.payload @webservice.expose() def helper(foo): return self.payload response = helper(self.controller) self.assertEquals(response, self.payload) def test_expose_ascii_via_kwargs(self): # Make the payload simpler self.payload = 'foo,bar,bla' self.transport.data = self.payload @webservice.expose(transport='ascii') def helper(foo): return self.payload response = helper(self.controller) self.assertEquals(response, self.payload) def test_expose_json_via_get(self): # Simulate a GET arg of 'transport' self.controller.env.form['transport'] = 'json' @webservice.expose() def helper(foo): return self.payload response = json.decode(helper(self.controller)) self.assertEquals(response['data'], self.payload) self.assertEquals(response['success'], True) def test_expose_json_via_kwargs(self): @webservice.expose(transport='json') def helper(foo): return self.payload response = json.decode(helper(self.controller)) self.assertEquals(response['data'], self.payload) self.assertEquals(response['success'], True) def test_expose_json_via_x_header(self): @webservice.expose(x_header=True) def helper(foo): return self.payload HEADER_FOUND = False html_body = helper(self.controller) for header in self.controller.env.headers: if header[0] == 'X-JSON': HEADER_FOUND = True response = json.decode(header[1]) break self.assertEquals(HEADER_FOUND, True) self.assertEquals(response['data'], self.payload) self.assertEquals(response['success'], True) def test_expose_pickle_via_get(self): # Simulate a GET arg of 'transport' self.controller.env.form['transport'] = 'pickle' @webservice.expose() def helper(foo): return self.payload response = cPickle.loads(helper(self.controller)) self.assertEquals(response['data'], self.payload) self.assertEquals(response['success'], True) def test_expose_pickle(self): @webservice.expose(transport='pickle') def helper(foo): return self.payload response = cPickle.loads(helper(self.controller)) self.assertEquals(response['data'], self.payload) self.assertEquals(response['success'], True) def test_unkown_transport(self): @webservice.expose(transport='xml') def helper(foo): return self.payload exception = error.WebserviceUnknownTransportError self.assertRaises(exception, helper, (self.controller)) Chula-0.7.0/tests/unit/test_example.py0000644000175000017500000000125711370740256020737 0ustar jmcfarlanejmcfarlaneimport unittest from chula import example class Test_example(unittest.TestCase): doctest = example def setUp(self): self.something = 'This gets reset at the start of every test' self.db = 'Sometimes you will set a db and cursor here' self.example = example.Example() def tearDown(self): self.something = 'This resets it after each test' def test_something(self): self.assertEquals([], example.something()) def test_sum(self): self.assertEquals(3, self.example.sum(1, 2)) self.assertRaises(TypeError, self.example.sum, (1, '2')) def test_awesome(self): self.failIf(not self.example.awesome()) Chula-0.7.0/tests/unit/test_guid.py0000644000175000017500000000235311370740256020232 0ustar jmcfarlanejmcfarlaneimport time import unittest from chula import guid class Test_guid(unittest.TestCase): doctest = guid def msg(self): msg = 'Guid generation was too slow: %s ms > %s ms' return msg % (round(self.speed / self.tests, 5), self.max) def fast_enough(self): return self.speed / self.tests < self.max def unique(self, max=50): unique = set() for x in xrange(max): unique.add(guid.guid()) return len(unique) def setUp(self): self.start = time.time() self.max = 0.0005 # Unit of measure is second per guid generation self.uv = 'Unique violation: guid() generated a non unique guid!' def test_guid_length_is_64_characters(self): self.assertEquals(len(guid.guid()), 64) def test_500(self): self.tests = 500 self.assertEqual(self.tests, self.unique(self.tests), self.uv) self.speed = time.time() - self.start self.assertTrue(self.fast_enough(), self.msg()) def test_5000(self): self.tests = 5000 self.assertEqual(self.tests, self.unique(self.tests), self.uv) self.speed = time.time() - self.start self.assertTrue(self.fast_enough(), self.msg()) Chula-0.7.0/tests/unit/test_singleton.py0000644000175000017500000000015711370740256021304 0ustar jmcfarlanejmcfarlaneimport unittest from chula import singleton class Test_singleton(unittest.TestCase): doctest = singleton Chula-0.7.0/tests/unit/__init__.py0000644000175000017500000000000011370740256017765 0ustar jmcfarlanejmcfarlaneChula-0.7.0/tests/unit/test_data.py0000644000175000017500000003362511370740256020221 0ustar jmcfarlanejmcfarlane"""data.py unit tests """ #test_data.py - Class to test generic python functions # #Copyright (C) 2005 John McFarlane # #This program is free software; you can redistribute it and/or modify #it under the terms of the GNU General Public License as published by #the Free Software Foundation; either version 2 of the License, or #(at your option) any later version. # #This program is distributed in the hope that it will be useful, #but WITHOUT ANY WARRANTY; without even the implied warranty of #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #GNU General Public License for more details. # #You should have received a copy of the GNU General Public License #along with this program; if not, write to the Free Software #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # import unittest import datetime from chula import data from chula.error import * class Test_data(unittest.TestCase): """A test class for the data module""" doctest = data def setUp(self): self.int = 7 self.str = "foobar" def test_commaify(self): self.assertEqual(data.commaify(""), "") self.assertEqual(data.commaify(" "), " ") self.assertEqual(data.commaify("abcdef"), "abcdef") self.assertEqual(data.commaify("1"), "1") self.assertEqual(data.commaify("1.20"), "1.20") self.assertEqual(data.commaify("10"), "10") self.assertEqual(data.commaify("10.1"), "10.10") self.assertEqual(data.commaify("10.10"), "10.10") self.assertEqual(data.commaify("100"), "100") self.assertEqual(data.commaify("100.00"), "100.00") self.assertEqual(data.commaify("1000"), "1,000") self.assertEqual(data.commaify("1000.45"), "1,000.45") self.assertEqual(data.commaify("1000.450"), "1,000.450") self.assertEqual(data.commaify("-1000.45"), "-1,000.45") self.assertEqual(data.commaify("-10000.45"), "-10,000.45") self.assertEqual(data.commaify("-100000.45"), "-100,000.45") self.assertEqual(data.commaify("-1000000.45"), "-1,000,000.45") def test_date_add(self): t = datetime.datetime self.assertEqual(data.date_add("s", 5, data.str2date("1/1/2005 1:00")), t(2005, 1, 1, 1, 0, 5)) self.assertEqual(data.date_add("m", 5, data.str2date("1/1/2005 1:00")), t(2005, 1, 1, 1, 5, 0)) self.assertEqual(data.date_add("s", 5, data.str2date("1/1/2005 1:00:05")), t(2005, 1, 1, 1, 0, 10)) # A few negative tests: self.assertEqual(data.date_add("m", -5, data.str2date("1/1/2005 1:05")), t(2005, 1, 1, 1, 0, 0)) def test_date_diff(self): d = datetime.datetime # Equal dates a = d(2005, 01, 27, 22, 15, 00) b = d(2005, 01, 27, 22, 15, 00) self.assertEqual(data.date_diff(a, b), 0) # Off by seconds a = d(2005, 01, 27, 22, 15, 00) b = d(2005, 01, 27, 22, 45, 00) self.assertEqual(data.date_diff(a, b), 1800) # Off by 15 seconds which rounds to 0 minutes a = d(2005, 01, 27, 22, 15, 00) b = d(2005, 01, 27, 22, 15, 15) self.assertEqual(data.date_diff(a, b, 'm'), 0) # Off by minutes a = d(2005, 01, 27, 22, 15, 00) b = d(2005, 01, 27, 22, 20, 00) self.assertEqual(data.date_diff(a, b, 'm'), 5) # Off by hours a = d(2005, 01, 27, 22, 15, 00) b = d(2005, 01, 27, 23, 15, 00) self.assertEqual(data.date_diff(a, b, 'h'), 1) # Off by hours (spanning midnight) a = d(2005, 01, 27, 22, 15, 00) b = d(2005, 01, 28, 02, 15, 00) self.assertEqual(data.date_diff(a, b, 'h'), 4) # Off by days a = d(2005, 01, 27, 22, 15, 00) b = d(2005, 01, 28, 22, 15, 00) self.assertEqual(data.date_diff(a, b, 'd'), 1) # Off by negative hours a = d(2005, 01, 27, 22, 15, 00) b = d(2005, 01, 27, 20, 15, 00) self.assertEqual(data.date_diff(a, b, 'h'), -2) # Off by days involving Feb when NOT a leap year a = d(2005, 01, 27, 22, 15, 0) b = d(2006, 01, 27, 22, 15, 0) self.assertEqual(data.date_diff(a, b, 'd'), 365) # Off by days involving Feb when IS a leap year a = d(2008, 01, 27, 22, 15, 00) b = d(2009, 01, 27, 22, 15, 00) self.assertEqual(data.date_diff(a, b, 'd'), 366) def test_date_within_range(self): d = datetime.datetime fmt = '%H:%M' # Now is always 30 minutes from now() now = d.now().strftime(fmt) self.assertEqual(data.date_within_range(now, 30), True) # 20 minutes is within range of 30 now = d(2005, 10, 4, 21, 35, 45).strftime(fmt) then = d(2005, 10, 4, 21, 55, 45) self.assertEqual(data.date_within_range(now, 30, then), True) # 31 minutes is NOT within range of 30 now = d(2005, 10, 4, 21, 35, 45).strftime(fmt) then = d(2005, 10, 4, 22, 6, 45) self.assertEqual(data.date_within_range(now, 30, then), False) # Anything in the past cannot be in range now = d(2005, 10, 4, 21, 35, 45).strftime(fmt) then = d(2005, 10, 4, 21, 34, 45) self.assertEqual(data.date_within_range(now, 30, then), False) def test_format_phone(self): self.assertEqual(data.format_phone("+44-(0)1224-XXXX-XXXX"), "+44-(0)1224-XXXX-XXXX") self.assertEqual(data.format_phone("5551234"), "555-1234") self.assertEqual(data.format_phone("555-1234"), "555-1234") self.assertEqual(data.format_phone("555-555-1234"), "(555) 555-1234") self.assertEqual(data.format_phone("5135551234"), "(513) 555-1234") def test_format_money(self): self.assertEqual(data.format_money(0), "0.00") self.assertEqual(data.format_money(.45), "0.45") self.assertEqual(data.format_money(-.45), "-0.45") self.assertEqual(data.format_money(0.45), "0.45") self.assertEqual(data.format_money(10), "10.00") self.assertEqual(data.format_money(1000), "1,000.00") self.assertEqual(data.format_money(1000000), "1,000,000.00") self.assertEqual(data.format_money(1000000.45), "1,000,000.45") self.assertEqual(data.format_money(-1000000.45), "-1,000,000.45") self.assertEqual(data.format_money(10.000), "10.00") self.assertEqual(data.format_money(10.00045), "10.00") self.assertRaises(TypeConversionError, data.format_money, 'abc') def test_isdate(self): self.assertEqual(data.isdate('1/1/2005'), True) self.assertEqual(data.isdate('1-1-2005'), True) self.assertEqual(data.isdate('2005-01-01'), True) self.assertEqual(data.isdate('1/1/2005 10:45'), True) self.assertEqual(data.isdate('1/1/2005 10:45:00'), True) self.assertEqual(data.isdate('1/1/20050'), False) self.assertEqual(data.isdate(None), False) self.assertEqual(data.isdate('a'), False) self.assertEqual(data.isdate(1), False) self.assertEqual(data.isdate(''), False) self.assertEqual(data.isdate("'"), False) def test_isregex(self): self.assertEqual(data.isregex(r"abc[a-z]"), True) self.assertEqual(data.isregex(r"("), False) def test_istag(self): self.assertEqual(data.istag("'"), False) self.assertEqual(data.istag("''"), False) self.assertEqual(data.istag('"'), False) self.assertEqual(data.istag('""'), False) self.assertEqual(data.istag(''), False) self.assertEqual(data.istag(' '), False) self.assertEqual(data.istag(' '), False) self.assertEqual(data.istag(None), False) self.assertEqual(data.istag('abc'), True) self.assertEqual(data.istag('a'), True) self.assertEqual(data.istag('B'), True) self.assertEqual(data.istag('4'), True) self.assertEqual(data.istag(4), False) self.assertEqual(data.istag('a,b'), False) self.assertEqual(data.istag(r'abc'), True) self.assertEqual(data.istag(u'abc'), True) def test_istags(self): self.assertEqual(data.istags("a b"), True) self.assertEqual(data.istags(u"a b"), True) self.assertEqual(data.istags("a$a b"), False) def test_none2empty(self): self.assertEqual(data.none2empty(self.int), self.int) self.assertEqual(data.none2empty(self.str), self.str) self.assertEqual(data.none2empty(""), "") self.assertEqual(data.none2empty(None), '') def test_replace_all(self): rall = data.replace_all self.assertEqual(rall({'o':'0', 't':'7'},"out"), "0u7") self.assertEqual(rall({'aaa':'a', ' b':'b'}, "aaa b"), "ab") self.assertEqual(rall({'aaa':'a', ' b':'b'}, "aaa b"), "ab") self.assertEqual(rall({'a':'A'}, "aaaaaa"), "AAAAAA") self.assertRaises(TypeError, rall, {2:5}, "12345") self.assertRaises(TypeError, rall, {'a':'A'}, 12345) self.assertRaises(TypeError, rall, {'a':'A'}, ['a', 'A']) def test_str2bool(self): self.assertEqual(data.str2bool(True), True) self.assertEqual(data.str2bool(False), False) self.assertEqual(data.str2bool('true'), True) self.assertEqual(data.str2bool('on'), True) self.assertEqual(data.str2bool('1'), True) self.assertEqual(data.str2bool('y'), True) self.assertEqual(data.str2bool(1), True) self.assertEqual(data.str2bool('false'), False) self.assertEqual(data.str2bool('off'), False) self.assertEqual(data.str2bool('0'), False) self.assertEqual(data.str2bool('n'), False) self.assertEqual(data.str2bool(0), False) self.assertRaises(TypeConversionError, data.str2bool, 'abc') def test_str2date(self): d = datetime.datetime cv = data.str2date self.assertEqual(cv('10/4/2005'), d(2005, 10, 4, 0, 0)) self.assertEqual(cv('10-4-2005'), d(2005, 10, 4, 0, 0)) self.assertEqual(cv('10-04-2005'), d(2005, 10, 4, 0, 0)) self.assertEqual(cv('2005-10-4'), d(2005, 10, 4, 0, 0)) self.assertEqual(cv('2005-10-04'), d(2005, 10, 4, 0, 0)) self.assertEqual(cv('10/4/2005 21:35'), d(2005, 10, 4, 21, 35)) self.assertEqual(cv('10/4/2005 21:35:45'), d(2005, 10, 4, 21, 35, 45)) self.assertEqual(cv('10/4/2005 21:35:00'), d(2005, 10, 4, 21, 35, 00)) self.assertEqual(cv('10/4/2005 21:01:00'), d(2005, 10, 4, 21, 01, 00)) self.assertEqual(cv('2005-10-4 21:01'), d(2005, 10, 4, 21, 01, 00)) self.assertEqual(cv('2005-10-4 21:01:00.970532-04:00'), d(2005, 10, 4, 21, 01, 00)) self.assertEqual(cv('2009-04-16 23:16:34.953368+00:00'), d(2009, 4, 16, 23, 16, 34)) self.assertEqual(cv('2005-10-4 21:01:00-04:00'), d(2005, 10, 4, 21, 01, 00)) self.assertEqual(cv('2005-10-4 21:01:00+04:00'), d(2005, 10, 4, 21, 01, 00)) self.assertEqual(cv('2005-10-4 21:01:00'), d(2005, 10, 4, 21, 01, 00)) self.assertEqual(cv('20051004'), d(2005, 10, 4, 0, 00, 00)) self.assertEqual(cv('20051004'), d(2005, 10, 4, 0, 00, 00)) self.assertEqual(cv('10042005'), d(2005, 10, 4, 0, 00, 00)) self.assertEqual(cv('10.04.2005'), d(2005, 10, 4, 0, 00, 00)) self.assertEqual(cv('1241579419'), d(2009, 5, 5, 20, 10, 19)) self.assertEqual(cv('1241579419.'), d(2009, 5, 5, 20, 10, 19)) self.assertEqual(cv('1241579419.0'), d(2009, 5, 5, 20, 10, 19)) self.assertEqual(cv('1241579419.00'), d(2009, 5, 5, 20, 10, 19)) self.assertRaises(TypeConversionError, cv, '2005-10') self.assertRaises(TypeConversionError, cv, '2005/21/5') self.assertRaises(TypeConversionError, cv, '2005/10/40') self.assertRaises(TypeConversionError, cv, '2005/10/21 90:10:00') self.assertRaises(TypeConversionError, cv, '2005/10/21 10:75:00') self.assertRaises(TypeConversionError, cv, '2005/10/21 10:20:61') self.assertRaises(TypeConversionError, cv, '2005/10/21 10:00:00:00') self.assertRaises(TypeConversionError, cv, 'abc') self.assertRaises(TypeConversionError, cv, '124157941') def test_str2tags(self): self.assertEqual(data.str2tags(''), []) self.assertEqual(data.str2tags('Abc'), ['abc']) self.assertEqual(data.str2tags('abc'), ['abc']) self.assertEqual(data.str2tags("abc4"), ['abc4']) self.assertEqual(data.str2tags('a,b'), ['a','b']) self.assertEqual(data.str2tags('a, b'), ['a','b']) self.assertEqual(data.str2tags('a, b'), ['a','b']) self.assertEqual(data.str2tags('a, b,c d a'), ['a','b','c','d']) self.assertEqual(data.str2tags('a b c a'), ['a','b','c']) self.assertRaises(TypeConversionError, data.str2tags, 'a;b') self.assertRaises(TypeConversionError, data.str2tags, 'a+b') self.assertRaises(TypeConversionError, data.str2tags, 'a!b') self.assertRaises(TypeConversionError, data.str2tags, "I'd") self.assertRaises(TypeConversionError, data.str2tags, 4) self.assertRaises(TypeConversionError, data.str2tags, None) def test_tags2str(self): self.assertEqual(data.tags2str(['a']), 'a') self.assertEqual(data.tags2str(['a','b']), 'a b') self.assertEqual(data.tags2str(['b','a']), 'a b') self.assertRaises(ValueError, data.tags2str, '') self.assertRaises(ValueError, data.tags2str, None) self.assertRaises(ValueError, data.tags2str, 4) self.assertRaises(ValueError, data.tags2str, ('a','b')) self.assertRaises(TypeConversionError, data.tags2str, ['a','!']) self.assertRaises(TypeConversionError, data.tags2str, ['-','*']) Chula-0.7.0/tests/unit/test_regex.py0000644000175000017500000000142611370740256020414 0ustar jmcfarlanejmcfarlaneimport unittest from chula import regex match = regex.match class Test_regex(unittest.TestCase): doctest = regex def test_ipv4(self): self.assertEqual(match(regex.IPV4, '127.0.0.1'), True) self.assertEqual(match(regex.IPV4, '12x.0.0.1'), False) def test_passwd_special_chars(self): self.assertEqual(match(regex.PASSWD, 'abcdefg'), True) self.assertEqual(match(regex.PASSWD, '123456'), True) self.assertEqual(match(regex.PASSWD, '!@#$%^&*?.'), True) def test_passwd_illegal_chars(self): self.assertEqual(match(regex.PASSWD, r'~`()[]{}\/<>,|'), False) def test_passwd_at_least_six_chars(self): self.assertEqual(match(regex.PASSWD, '12345'), False) self.assertEqual(match(regex.PASSWD, '123456'), True) Chula-0.7.0/README0000644000175000017500000000014211412545601014414 0ustar jmcfarlanejmcfarlaneChula is a lightweight toolkit for writing web applications in Python http://chula.rockfloat.com Chula-0.7.0/PKG-INFO0000644000175000017500000000173411412546122014640 0ustar jmcfarlanejmcfarlaneMetadata-Version: 1.0 Name: Chula Version: 0.7.0 Summary: Chula is a lightweight toolkit for writing web applications in Python Home-page: http://chula.rockfloat.com Author: John McFarlane Author-email: john.mcfarlane@rockfloat.com License: GPL Download-URL: ('http://chula.rockfloat.com/downloads/Chula-0.7.0.tar.gz',) Description: It's designed to work with WSGI, FastCGI, or mod_python. Chula only handles the nuts and bolts - meaning there isn't any ORM layer, and no code generation. Platform: UNKNOWN Classifier: Development Status :: 3 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Database Classifier: Topic :: Internet :: WWW/HTTP :: Site Management Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries :: Python Modules Chula-0.7.0/scripts/0000755000175000017500000000000011412546122015225 5ustar jmcfarlanejmcfarlaneChula-0.7.0/scripts/docs0000755000175000017500000000130211370740256016106 0ustar jmcfarlanejmcfarlane#! /bin/bash # Change directories to the root of the project BASEDIR=${1:-"/tmp/chula-docs-build"} SRC=$(dirname $0) cd $SRC/.. # Validate the base dir exists mkdir -p "$BASEDIR" # Fetch the current version for which docs are being generated version=$(grep -o -E '[0-9]+\.[0-9]+\.[0-9]+[a-zA-Z._-]*' chula/__init__.py) # Specify the output location (this will include a version number) dest=$BASEDIR/$version mkdir -p $dest # Truncate any existing docs so the generation is pristine if [ -f "$dest/searchindex.js" ]; then rm -rf $dest/* echo "Previous docs purged..." fi # Generate documentation sphinx-build -E -b html docs $dest # Print browsable path echo echo "file://$dest/index.html" Chula-0.7.0/scripts/run_tests0000755000175000017500000000070611370740256017213 0ustar jmcfarlanejmcfarlane#!/usr/bin/env python # # Run all Chula unit tests (including doctests) import os import sys # For those running tests without Chula actually installed sys.path.insert(0, os.path.dirname(sys.path[0])) from chula import testutils if __name__ == "__main__": # Search the passed paths, or the current working directory path = sys.argv[1:] if len(path) == 0: path = os.getcwd() tests = testutils.TestFinder(path) tests.run() Chula-0.7.0/setup.cfg0000644000175000017500000000007311412546122015357 0ustar jmcfarlanejmcfarlane[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 Chula-0.7.0/MANIFEST.in0000644000175000017500000000027511370740256015307 0ustar jmcfarlanejmcfarlaneinclude INSTALL include LICENSE recursive-include apps * recursive-include chula *.py recursive-include docs * recursive-include scripts * recursive-include sql * recursive-include tests * Chula-0.7.0/Chula.egg-info/0000755000175000017500000000000011412546122016264 5ustar jmcfarlanejmcfarlaneChula-0.7.0/Chula.egg-info/PKG-INFO0000644000175000017500000000173411412546122017366 0ustar jmcfarlanejmcfarlaneMetadata-Version: 1.0 Name: Chula Version: 0.7.0 Summary: Chula is a lightweight toolkit for writing web applications in Python Home-page: http://chula.rockfloat.com Author: John McFarlane Author-email: john.mcfarlane@rockfloat.com License: GPL Download-URL: ('http://chula.rockfloat.com/downloads/Chula-0.7.0.tar.gz',) Description: It's designed to work with WSGI, FastCGI, or mod_python. Chula only handles the nuts and bolts - meaning there isn't any ORM layer, and no code generation. Platform: UNKNOWN Classifier: Development Status :: 3 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: GNU General Public License (GPL) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Topic :: Database Classifier: Topic :: Internet :: WWW/HTTP :: Site Management Classifier: Topic :: Software Development Classifier: Topic :: Software Development :: Libraries :: Python Modules Chula-0.7.0/Chula.egg-info/dependency_links.txt0000644000175000017500000000000111412546122022332 0ustar jmcfarlanejmcfarlane Chula-0.7.0/Chula.egg-info/zip-safe0000644000175000017500000000000111412546122017714 0ustar jmcfarlanejmcfarlane Chula-0.7.0/Chula.egg-info/SOURCES.txt0000644000175000017500000000776011412546122020162 0ustar jmcfarlanejmcfarlaneINSTALL LICENSE MANIFEST.in README setup.py Chula.egg-info/PKG-INFO Chula.egg-info/SOURCES.txt Chula.egg-info/dependency_links.txt Chula.egg-info/requires.txt Chula.egg-info/top_level.txt Chula.egg-info/zip-safe apps/basic/webserver apps/basic/example/__init__.py apps/basic/example/configuration.py apps/basic/example/www/__init__.py apps/basic/example/www/controllers/__init__.py apps/basic/example/www/controllers/error.py apps/basic/example/www/controllers/home.py apps/basic/example/www/controllers/sample.py apps/basic/example/www/controllers/imports/__init__.py apps/basic/example/www/controllers/imports/bad_import.py apps/basic/example/www/controllers/imports/global_exception.py apps/basic/example/www/controllers/imports/syntax_exception.py apps/basic/example/www/controllers/imports/test.py chula/__init__.py chula/cache.py chula/config.py chula/data.py chula/ecalendar.py chula/error.py chula/example.py chula/guid.py chula/json.py chula/logger.py chula/mail.py chula/pager.py chula/passwd.py chula/regex.py chula/singleton.py chula/system.py chula/testutils.py chula/webservice.py chula/collection/__init__.py chula/collection/base.py chula/collection/restricted.py chula/collection/ubound.py chula/db/__init__.py chula/db/datastore.py chula/db/functions.py chula/db/engines/__init__.py chula/db/engines/couch.py chula/db/engines/engine.py chula/db/engines/postgresql.py chula/db/engines/sqlite.py chula/nosql/__init__.py chula/nosql/couch.py chula/queue/__init__.py chula/queue/client.py chula/queue/mqueue.py chula/queue/server.py chula/queue/tester.py chula/queue/messages/__init__.py chula/queue/messages/echo.py chula/queue/messages/mail.py chula/queue/messages/message.py chula/session/__init__.py chula/session/session.py chula/session/backends/__init__.py chula/session/backends/base.py chula/session/backends/couchdb.py chula/session/backends/memcached.py chula/session/backends/postgresql.py chula/test/__init__.py chula/test/bat.py chula/test/selenium.py chula/vendor/__init__.py chula/vendor/fcgi.py chula/vendor/memcache.py chula/vendor/selenium.py chula/www/__init__.py chula/www/controller.py chula/www/cookie.py chula/www/http.py chula/www/adapters/__init__.py chula/www/adapters/base.py chula/www/adapters/env.py chula/www/adapters/fcgi/__init__.py chula/www/adapters/fcgi/adapter.py chula/www/adapters/fcgi/env.py chula/www/adapters/mod_python/__init__.py chula/www/adapters/mod_python/adapter.py chula/www/adapters/mod_python/env.py chula/www/adapters/mod_python/fakerequest.py chula/www/adapters/wsgi/__init__.py chula/www/adapters/wsgi/adapter.py chula/www/adapters/wsgi/env.py chula/www/mapper/__init__.py chula/www/mapper/base.py chula/www/mapper/classpath.py chula/www/mapper/regex.py docs/about.rst docs/changelog.rst docs/conf.py docs/getting_started.rst docs/index.rst docs/install.rst docs/library.rst docs/session.rst docs/template.txt docs/_static/flow.dot docs/library/cache.rst docs/library/collection.rst docs/library/config.rst docs/library/error.rst scripts/docs scripts/run_tests sql/session/reload sql/session/schema.sql sql/test/reload sql/test/schema.sql tests/bat/test_homepage.py tests/bat/test_sample_controller.py tests/bat/errors/test_bad_import.py tests/bat/errors/test_global_exception.py tests/bat/errors/test_missing_controller.py tests/bat/errors/test_syntax_exception.py tests/unit/__init__.py tests/unit/test_config.py tests/unit/test_data.py tests/unit/test_example.py tests/unit/test_guid.py tests/unit/test_pager.py tests/unit/test_passwd.py tests/unit/test_regex.py tests/unit/test_singleton.py tests/unit/test_system.py tests/unit/test_webservice.py tests/unit/collection/__init__.py tests/unit/collection/test_base.py tests/unit/collection/test_restricted.py tests/unit/collection/test_ubound.py tests/unit/db/__init__.py tests/unit/db/test_couch.py tests/unit/db/test_functions.py tests/unit/db/test_sqlite.py tests/unit/queue/__init__.py tests/unit/queue/test_message.py tests/unit/www/__init__.py tests/unit/www/mapper/__init__.py tests/unit/www/mapper/test_classpath.py tests/unit/www/mapper/test_regex.pyChula-0.7.0/Chula.egg-info/top_level.txt0000644000175000017500000000000611412546122021012 0ustar jmcfarlanejmcfarlanechula Chula-0.7.0/Chula.egg-info/requires.txt0000644000175000017500000000001211412546122020655 0ustar jmcfarlanejmcfarlanesimplejsonChula-0.7.0/setup.py0000644000175000017500000000413111370740256015256 0ustar jmcfarlanejmcfarlane#setup.py - Chula # #Copyright (C) 2010 John McFarlane # #This program is free software; you can redistribute it and/or modify #it under the terms of the GNU General Public License as published by #the Free Software Foundation; either version 2 of the License, or #(at your option) any later version. # #This program is distributed in the hope that it will be useful, #but WITHOUT ANY WARRANTY; without even the implied warranty of #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #GNU General Public License for more details. # #You should have received a copy of the GNU General Public License #along with this program; if not, write to the Free Software #Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # from setuptools import setup, find_packages import os import sys import chula from chula import error # Check for dependencies if 'install' in sys.argv: if sys.version_info < (2, 6): raise error.MissingDependencyError('Python-2.6 or higher') # Attributes AUTHOR = 'John McFarlane' CLASSIFIERS = """ Development Status :: 3 - Beta Intended Audience :: Developers License :: OSI Approved :: GNU General Public License (GPL) Operating System :: OS Independent Programming Language :: Python Topic :: Database Topic :: Internet :: WWW/HTTP :: Site Management Topic :: Software Development Topic :: Software Development :: Libraries :: Python Modules """ EMAIL = 'john.mcfarlane@rockfloat.com' INSTALL_REQUIRES = ['simplejson'] LICENSE = 'GPL' NAME = 'Chula' TESTS = 'tests' URL = 'http://chula.rockfloat.com' URL_ = URL + '/downloads/Chula-%s.tar.gz' % chula.version, ZIP_SAFE = True setup( author = AUTHOR, author_email = EMAIL, classifiers = [c for c in CLASSIFIERS.split('\n') if c], description = chula.__doc__.split('\n')[0], download_url = URL_, install_requires = INSTALL_REQUIRES, license = LICENSE, long_description = '\n'.join(chula.__doc__.split('\n')[2:]), name = NAME, packages = find_packages(), test_suite = TESTS, url = URL, version = chula.version, zip_safe = ZIP_SAFE, ) Chula-0.7.0/INSTALL0000644000175000017500000000047211370740256014601 0ustar jmcfarlanejmcfarlaneTo install on linux/unix/mac: root# python setup.py To install on windows (replace 1.2.3 with the actual version): Download http://rockfloat.com/chula/Chula-1.2.3.win32.exe Double click on Chula-1.2.3.win32.exe and follow the prompts See http://www.rockfloat.com/projects/chula/ for more information