From 89dde9dfd63988054e35f900bc09d311e8d94552 Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Sun, 1 Sep 2013 16:44:23 -0400 Subject: [PATCH 1/9] fix for encoding problem --- brubeck/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brubeck/auth.py b/brubeck/auth.py index 606611e..f869150 100644 --- a/brubeck/auth.py +++ b/brubeck/auth.py @@ -32,7 +32,7 @@ def gen_hexdigest(raw_password, algorithm=BCRYPT, salt=None): # bcrypt has a special salt if salt is None: salt = bcrypt.gensalt() - return (algorithm, salt, bcrypt.hashpw(raw_password, salt)) + return (algorithm, salt, bcrypt.hashpw(raw_password.encode('utf-8'), salt.encode('utf-8'))) raise ValueError('Unknown password algorithm') From 64a4756b3f5a18288ad8c85f7a3b8022d88a253e Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Sun, 15 Sep 2013 13:25:26 -0400 Subject: [PATCH 2/9] added my own requirements to support my custom fields --- envs/brubeck.reqs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/envs/brubeck.reqs b/envs/brubeck.reqs index a1d663c..1c161d4 100644 --- a/envs/brubeck.reqs +++ b/envs/brubeck.reqs @@ -1,11 +1,9 @@ Cython==0.15.1 Jinja2==2.6 -#Mako==0.7.0 -#tornado==2.2 -#pystache==0.5.1 dictshield==0.4.3 py-bcrypt==0.2 pymongo==2.1.1 python-dateutil==2.1 pyzmq==2.1.11 ujson==1.18 +-e git+git://github.com/reduxdj/schematics/tree/v0.9.2 \ No newline at end of file From 2a6e77b4083c5e849279d5d80763929670fed84e Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Wed, 18 Sep 2013 22:38:41 -0400 Subject: [PATCH 3/9] changed env requirements back to original repo --- envs/brubeck.reqs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/envs/brubeck.reqs b/envs/brubeck.reqs index 1c161d4..8a78851 100644 --- a/envs/brubeck.reqs +++ b/envs/brubeck.reqs @@ -6,4 +6,4 @@ pymongo==2.1.1 python-dateutil==2.1 pyzmq==2.1.11 ujson==1.18 --e git+git://github.com/reduxdj/schematics/tree/v0.9.2 \ No newline at end of file +-e git+git://github.com/j2labs/schematics/tree/v0.9.2 \ No newline at end of file From 118278c241fd89c285690232b60aae795af9166d Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Thu, 19 Sep 2013 09:04:44 -0400 Subject: [PATCH 4/9] added import fix for brubeck --- brubeck/request_handling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brubeck/request_handling.py b/brubeck/request_handling.py index 68f6556..ec01b02 100755 --- a/brubeck/request_handling.py +++ b/brubeck/request_handling.py @@ -55,7 +55,7 @@ def coro_spawn(function, app, message, *a, **kw): import os, sys from request import Request, to_bytes, to_unicode -from schematics.serialize import for_jsonschema, from_jsonschema +from schematics.types.serializable import for_jsonschema, from_jsonschema import ujson as json From 5083382d22ebb73947e3150fdd35237a8924e947 Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Fri, 20 Sep 2013 08:32:22 -0400 Subject: [PATCH 5/9] updates --- brubeck/.gitignore | 15 + brubeck/.travis.yml | 14 + brubeck/README.md | 81 + brubeck/brubeck/__init__.py | 16 + brubeck/brubeck/auth.py | 134 + brubeck/brubeck/autoapi.py | 386 + brubeck/brubeck/caching.py | 120 + brubeck/brubeck/connections.py | 269 + brubeck/brubeck/datamosh.py | 92 + brubeck/brubeck/models.py | 126 + brubeck/brubeck/queryset/__init__.py | 4 + brubeck/brubeck/queryset/base.py | 119 + brubeck/brubeck/queryset/dict.py | 70 + brubeck/brubeck/queryset/redis.py | 133 + brubeck/brubeck/request.py | 296 + brubeck/brubeck/request_handling.py | 866 ++ brubeck/brubeck/templating.py | 164 + brubeck/brubeck/timekeeping.py | 90 + brubeck/demos/README.md | 236 + brubeck/demos/demo_auth.py | 48 + brubeck/demos/demo_autoapi.py | 94 + brubeck/demos/demo_jinja2.py | 19 + brubeck/demos/demo_jinja2_noclasses.py | 24 + brubeck/demos/demo_login.py | 103 + brubeck/demos/demo_longpolling.py | 43 + brubeck/demos/demo_mako.py | 20 + brubeck/demos/demo_minimal.py | 19 + brubeck/demos/demo_multipart.py | 48 + brubeck/demos/demo_mustache.py | 18 + brubeck/demos/demo_noclasses.py | 15 + brubeck/demos/demo_tornado.py | 19 + brubeck/demos/demo_urlargs.py | 40 + brubeck/demos/demo_wsgi.py | 20 + brubeck/demos/log/.gitignore | 0 brubeck/demos/m2reader.py | 11 + brubeck/demos/media/index.html | 1 + brubeck/demos/media/jquery-1.5.js | 8176 +++++++++++++++++ brubeck/demos/mongrel2.conf | 32 + brubeck/demos/run/.gitignore | 0 brubeck/demos/templates/autoapi/base.html | 8 + brubeck/demos/templates/autoapi/errors.html | 4 + brubeck/demos/templates/autoapi/todos.html | 144 + brubeck/demos/templates/jinja2/base.html | 8 + brubeck/demos/templates/jinja2/errors.html | 4 + brubeck/demos/templates/jinja2/success.html | 4 + brubeck/demos/templates/login/base.html | 8 + brubeck/demos/templates/login/errors.html | 4 + brubeck/demos/templates/login/landing.html | 6 + brubeck/demos/templates/login/login.html | 10 + brubeck/demos/templates/longpolling/base.html | 55 + brubeck/demos/templates/mako/base.html | 8 + brubeck/demos/templates/mako/errors.html | 1 + brubeck/demos/templates/mako/success.html | 5 + brubeck/demos/templates/multipart/base.html | 19 + brubeck/demos/templates/multipart/errors.html | 4 + .../demos/templates/multipart/landing.html | 10 + .../demos/templates/mustache/errors.mustache | 8 + .../mustache/nested_partial.mustache | 1 + .../demos/templates/mustache/success.mustache | 8 + .../templates/mustache/take_five.mustache | 2 + brubeck/demos/templates/tornado/base.html | 8 + brubeck/demos/templates/tornado/errors.html | 4 + brubeck/demos/templates/tornado/success.html | 4 + brubeck/docs/AUTHENTICATION.md | 33 + brubeck/docs/AUTOAPI.md | 140 + brubeck/docs/CONCURRENCY.md | 63 + brubeck/docs/DATAMODELING.md | 155 + brubeck/docs/DEMOS.md | 1 + brubeck/docs/DEPENDENCIES.md | 75 + brubeck/docs/DEPLOYING.md | 155 + brubeck/docs/FILEUPLOADING.md | 43 + brubeck/docs/HANDLERS.md | 79 + brubeck/docs/INDEX.md | 1 + brubeck/docs/INSTALLING.md | 148 + brubeck/docs/LICENSE.md | 26 + brubeck/docs/QUERYSETS.md | 18 + brubeck/docs/TEMPLATING.md | 68 + brubeck/docs/ZEROMQ.md | 69 + brubeck/docs/upupdowndown.py | 5 + brubeck/envs/brubeck.reqs | 9 + brubeck/envs/eventlet.reqs | 1 + brubeck/envs/gevent.reqs | 2 + brubeck/setup.py | 12 + brubeck/tests/__init__.py | 6 + brubeck/tests/fixtures/__init__.py | 6 + .../tests/fixtures/http_request_brubeck.txt | 1 + brubeck/tests/fixtures/http_request_root.txt | 1 + .../http_request_root_with_cookie.txt | 1 + .../fixtures/request_handler_fixtures.py | 27 + .../fixtures/test_body_method_handler.txt | 1 + .../fixtures/test_body_object_handler.txt | 1 + brubeck/tests/handlers/__init__.py | 0 brubeck/tests/handlers/method_handlers.py | 6 + brubeck/tests/handlers/object_handlers.py | 46 + brubeck/tests/test_queryset.py | 367 + brubeck/tests/test_request_handling.py | 219 + brubeck/tox.ini | 17 + brubeck/wsgi.py | 36 + 98 files changed, 14156 insertions(+) create mode 100644 brubeck/.gitignore create mode 100644 brubeck/.travis.yml create mode 100644 brubeck/README.md create mode 100644 brubeck/brubeck/__init__.py create mode 100644 brubeck/brubeck/auth.py create mode 100644 brubeck/brubeck/autoapi.py create mode 100644 brubeck/brubeck/caching.py create mode 100644 brubeck/brubeck/connections.py create mode 100644 brubeck/brubeck/datamosh.py create mode 100644 brubeck/brubeck/models.py create mode 100644 brubeck/brubeck/queryset/__init__.py create mode 100644 brubeck/brubeck/queryset/base.py create mode 100644 brubeck/brubeck/queryset/dict.py create mode 100644 brubeck/brubeck/queryset/redis.py create mode 100644 brubeck/brubeck/request.py create mode 100755 brubeck/brubeck/request_handling.py create mode 100644 brubeck/brubeck/templating.py create mode 100755 brubeck/brubeck/timekeeping.py create mode 100644 brubeck/demos/README.md create mode 100755 brubeck/demos/demo_auth.py create mode 100755 brubeck/demos/demo_autoapi.py create mode 100755 brubeck/demos/demo_jinja2.py create mode 100755 brubeck/demos/demo_jinja2_noclasses.py create mode 100755 brubeck/demos/demo_login.py create mode 100755 brubeck/demos/demo_longpolling.py create mode 100755 brubeck/demos/demo_mako.py create mode 100755 brubeck/demos/demo_minimal.py create mode 100755 brubeck/demos/demo_multipart.py create mode 100755 brubeck/demos/demo_mustache.py create mode 100755 brubeck/demos/demo_noclasses.py create mode 100755 brubeck/demos/demo_tornado.py create mode 100755 brubeck/demos/demo_urlargs.py create mode 100755 brubeck/demos/demo_wsgi.py create mode 100644 brubeck/demos/log/.gitignore create mode 100755 brubeck/demos/m2reader.py create mode 100644 brubeck/demos/media/index.html create mode 100644 brubeck/demos/media/jquery-1.5.js create mode 100644 brubeck/demos/mongrel2.conf create mode 100644 brubeck/demos/run/.gitignore create mode 100644 brubeck/demos/templates/autoapi/base.html create mode 100644 brubeck/demos/templates/autoapi/errors.html create mode 100644 brubeck/demos/templates/autoapi/todos.html create mode 100644 brubeck/demos/templates/jinja2/base.html create mode 100644 brubeck/demos/templates/jinja2/errors.html create mode 100644 brubeck/demos/templates/jinja2/success.html create mode 100644 brubeck/demos/templates/login/base.html create mode 100644 brubeck/demos/templates/login/errors.html create mode 100644 brubeck/demos/templates/login/landing.html create mode 100644 brubeck/demos/templates/login/login.html create mode 100644 brubeck/demos/templates/longpolling/base.html create mode 100644 brubeck/demos/templates/mako/base.html create mode 100644 brubeck/demos/templates/mako/errors.html create mode 100644 brubeck/demos/templates/mako/success.html create mode 100644 brubeck/demos/templates/multipart/base.html create mode 100644 brubeck/demos/templates/multipart/errors.html create mode 100644 brubeck/demos/templates/multipart/landing.html create mode 100644 brubeck/demos/templates/mustache/errors.mustache create mode 100644 brubeck/demos/templates/mustache/nested_partial.mustache create mode 100644 brubeck/demos/templates/mustache/success.mustache create mode 100644 brubeck/demos/templates/mustache/take_five.mustache create mode 100644 brubeck/demos/templates/tornado/base.html create mode 100644 brubeck/demos/templates/tornado/errors.html create mode 100644 brubeck/demos/templates/tornado/success.html create mode 100644 brubeck/docs/AUTHENTICATION.md create mode 100644 brubeck/docs/AUTOAPI.md create mode 100644 brubeck/docs/CONCURRENCY.md create mode 100644 brubeck/docs/DATAMODELING.md create mode 120000 brubeck/docs/DEMOS.md create mode 100644 brubeck/docs/DEPENDENCIES.md create mode 100644 brubeck/docs/DEPLOYING.md create mode 100644 brubeck/docs/FILEUPLOADING.md create mode 100644 brubeck/docs/HANDLERS.md create mode 120000 brubeck/docs/INDEX.md create mode 100644 brubeck/docs/INSTALLING.md create mode 100644 brubeck/docs/LICENSE.md create mode 100644 brubeck/docs/QUERYSETS.md create mode 100644 brubeck/docs/TEMPLATING.md create mode 100644 brubeck/docs/ZEROMQ.md create mode 100755 brubeck/docs/upupdowndown.py create mode 100644 brubeck/envs/brubeck.reqs create mode 100644 brubeck/envs/eventlet.reqs create mode 100644 brubeck/envs/gevent.reqs create mode 100644 brubeck/setup.py create mode 100644 brubeck/tests/__init__.py create mode 100644 brubeck/tests/fixtures/__init__.py create mode 100644 brubeck/tests/fixtures/http_request_brubeck.txt create mode 100644 brubeck/tests/fixtures/http_request_root.txt create mode 100644 brubeck/tests/fixtures/http_request_root_with_cookie.txt create mode 100644 brubeck/tests/fixtures/request_handler_fixtures.py create mode 100644 brubeck/tests/fixtures/test_body_method_handler.txt create mode 100644 brubeck/tests/fixtures/test_body_object_handler.txt create mode 100644 brubeck/tests/handlers/__init__.py create mode 100644 brubeck/tests/handlers/method_handlers.py create mode 100644 brubeck/tests/handlers/object_handlers.py create mode 100755 brubeck/tests/test_queryset.py create mode 100755 brubeck/tests/test_request_handling.py create mode 100644 brubeck/tox.ini create mode 100644 brubeck/wsgi.py diff --git a/brubeck/.gitignore b/brubeck/.gitignore new file mode 100644 index 0000000..1f5d5f0 --- /dev/null +++ b/brubeck/.gitignore @@ -0,0 +1,15 @@ +.DS_Store +*.db +*.pyc +*.bak +*.log +*.*~ +MANIFEST +build +dist +.rope* +*.pid +.*.swp +.*.swo +*.egg-info +.tox diff --git a/brubeck/.travis.yml b/brubeck/.travis.yml new file mode 100644 index 0000000..e8a6be2 --- /dev/null +++ b/brubeck/.travis.yml @@ -0,0 +1,14 @@ +language: python + +python: + - 2.6 + - 2.7 + +before_install: + - sudo apt-get install libevent-dev + - pip install --use-mirrors gevent nose redis testtools + +install: + - python setup.py install + +script: nosetests --exe -w tests diff --git a/brubeck/README.md b/brubeck/README.md new file mode 100644 index 0000000..f43f5a3 --- /dev/null +++ b/brubeck/README.md @@ -0,0 +1,81 @@ +# What Is Brubeck? + +__Brubeck__ is a flexible Python web framework that aims to make the process of building scalable web services easy. + +Brubeck's design is discussed in depth in the provided documentation. There, you will find lots of code samples for building request handlers, authentication, rendering templates, managing databases and more. + + +## Goals + +* __Be Fast__: Brubeck is currently very fast. We intend to keep it that way. + +* __Scalable__: Massive scaling capabilities should be available out of the box. + +* __Friendly__: Should be easy for Python hackers of any skill level to use. + +* __Pluggable__: Brubeck can speak to any language and any database. + + +# Example: Hello World + +This is a whole Brubeck application. + + class DemoHandler(WebMessageHandler): + def get(self): + self.set_body('Hello world') + return self.render() + + urls = [(r'^/', DemoHandler)] + msg_conn = Mongrel2Connection('ipc://127.0.0.1:9999', + 'ipc://127.0.0.1:9998') + + app = Brubeck(msg_conn=msg_conn, + handler_tuples=urls) + app.run() + + +# Complete Examples + +__Listsurf__ is a simple to way to save links. Yeah... another delicious clone! + +It serves as a basic demonstration of what a complete site looks like when you build with Brubeck. It has authentication with secure cookies, offers a JSON API, uses [Jinja2](http://jinja.pocoo.org/) for templating and stores data in [MongoDB](http://mongodb.org). + +* [Listsurf Code](https://github.com/j2labs/listsurf) + +__Readify__ is a more elaborate form of Listsurf. + +User's have profiles, you can mark things as liked, archived (out of your stream, kept) or you can delete them. The links can also be tagged for easy finding. This project also splits the API out from the Web system into two separate processes, each reading from a single Mongrel2. + +You could actually run four Web processes and four API processes as easily as just turning each of them on four times. + +This project roughly represents a typical organization of Brubeck's components. Most notably is the separation of handlers, models and queries into isolated python files. + +* [Readify Code](https://github.com/j2labs/readify) + +__SpotiChat__ is a chat app for spotify user. + +SpotiChat provides chat for users listening to the same song with Spotify. The chat is handled via request handlers that go to sleep until incoming messages need to be distributed to connect clients. The messages are backed by [Redis](http://redis.io) too. + +* [SpotiChat Code](https://github.com/sethmurphy/SpotiChat-Server) + +__no.js__ is a javascript-free chat system. + +It works by using the old META Refresh trick, combined with long-polling. It even works in IE4! + +* [No.js Code](https://github.com/talos/no.js) + + +## Contributors + +Brubeck wouldn't be what it is without help from: + +[James Dennis](https://github.com/j2labs), [Andrew Gwozdziewycz](https://github.com/apgwoz), [Malcolm Matalka](https://github.com/orbitz/), [Dion Paragas](https://github.com/d1on/), [Duane Griffin](https://github.com/duaneg), [Faruk Akgul](https://github.com/faruken), [Seth Murphy](https://github.com/sethmurphy), [John Krauss](https://github.com/talos), [Ben Beecher](https://github.com/gone), [Jordan Orelli](https://github.com/jordanorelli), [Michael Larsen](https://github.com/mghlarsen), [Moritz](https://github.com/m2w), [Dmitrijs Milajevs](https://github.com/dimazest), [Paul Winkler](https://github.com/slinkp), [Chris McCulloh](https://github.com/st0w), [Nico Mandery](https://github.com/nmandery), [Victor Trac](https://github.com/victortrac) + + +# Contact Us + +If you discover bugs or want to suggest features, please use our [issue tracker](https://github.com/j2labs/brubeck/issues). + +Also consider joining our mailing list: [brubeck-dev](http://groups.google.com/group/brubeck-dev). + +You can find some of us in #brubeck on freenode too. diff --git a/brubeck/brubeck/__init__.py b/brubeck/brubeck/__init__.py new file mode 100644 index 0000000..72d264a --- /dev/null +++ b/brubeck/brubeck/__init__.py @@ -0,0 +1,16 @@ +#!/usr/bin/env python + +"""The Brubeck Message Handling system.""" + +version = "0.4.0" +version_info = (0, 4, 0) +__all__ = ['auth', + 'autoapi', + 'caching', + 'datamosh', + 'models', + 'mongrel2', + 'queryset', + 'request_handling', + 'templating', + 'timekeeping'] diff --git a/brubeck/brubeck/auth.py b/brubeck/brubeck/auth.py new file mode 100644 index 0000000..f869150 --- /dev/null +++ b/brubeck/brubeck/auth.py @@ -0,0 +1,134 @@ +"""Authentication functions are offered here in three groups. + + 1. The mechanics of auth, like generating a hex digest or assembling the + data. + + 2. Tools for applying auth requirements to functions, eg. decorators. + + 3. Mixins for adding authenticaiton handling to MessageHandler's and + Document classes +""" + +import bcrypt +import functools +import logging + + +### +### Password Helpers +### + +BCRYPT = 'bcrypt' +PASSWD_DELIM = '|||' + + +def gen_hexdigest(raw_password, algorithm=BCRYPT, salt=None): + """Takes the algorithm, salt and password and uses Python's + hashlib to produce the hash. Currently only supports bcrypt. + """ + if raw_password is None: + raise ValueError('No empty passwords, fool') + if algorithm == BCRYPT: + # bcrypt has a special salt + if salt is None: + salt = bcrypt.gensalt() + return (algorithm, salt, bcrypt.hashpw(raw_password.encode('utf-8'), salt.encode('utf-8'))) + raise ValueError('Unknown password algorithm') + + +def build_passwd_line(algorithm, salt, digest): + """Simply takes the inputs for a passwd entry and puts them + into the convention for storage + """ + return PASSWD_DELIM.join([algorithm, salt, digest]) + + +def split_passwd_line(password_line): + """Takes a password line and returns the line split by PASSWD_DELIM + """ + return password_line.split(PASSWD_DELIM) + + +### +### Authentication decorators +### + +def authenticated(method): + """Decorate request handler methods with this to require that the user be + logged in. Works by checking for the existence of self.current_user as set + by a RequestHandler's prepare() function. + """ + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + if not self.current_user: + return self.render_error(self._AUTH_FAILURE, self.auth_error) + return method(self, *args, **kwargs) + return wrapper + + +def web_authenticated(method): + """Same as `authenticated` except it redirects a user to the login page + specified by self.application.login_url + """ + @functools.wraps(method) + def wrapper(self, *args, **kwargs): + if not self.current_user: + has_login_url = hasattr(self.application, 'login_url') + if has_login_url and self.application.login_url is not None: + return self.redirect(self.application.login_url) + else: + error = 'web_authentication called with undefined ' + logging.error(error) + return self.render_error(self._AUTH_FAILURE) + return method(self, *args, **kwargs) + return wrapper + + +### +### Mixins to extend MessageHandlers with auth funcitons +### + +class UserHandlingMixin(object): + """A request handler that uses this mixin can also use the decorators + above. This mixin is intended to make the interaction with authentication + generic without insisting on a particular strategy. + """ + + @property + def current_user(self): + """The authenticated user for this message. + + Determined by either get_current_user, which you can override to + set the user based on, e.g., a cookie. If that method is not + + overridden, this method always returns None. + + We lazy-load the current user the first time this method is called + and cache the result after that. + """ + if not hasattr(self, "_current_user"): + self._current_user = self.get_current_user() + return self._current_user + + def get_current_user(self): + """Override to determine the current user from, e.g., a cookie. + """ + return None + + @property + def current_userprofile(self): + """Same idea for the user's profile + """ + if not hasattr(self, "_current_userprofile"): + self._current_userprofile = self.get_current_userprofile() + return self._current_userprofile + + def get_current_userprofile(self): + """Override to determine the current user + """ + return None + + def auth_error(self): + """Override with actions to perform before rendering the error output. + """ + pass diff --git a/brubeck/brubeck/autoapi.py b/brubeck/brubeck/autoapi.py new file mode 100644 index 0000000..1820b68 --- /dev/null +++ b/brubeck/brubeck/autoapi.py @@ -0,0 +1,386 @@ +from request_handling import JSONMessageHandler, FourOhFourException +from schematics.serialize import to_json, make_safe_json + +import ujson as json + + +class AutoAPIBase(JSONMessageHandler): + """AutoAPIBase generates a JSON REST API for you. *high five!* + I also read this link for help in propertly defining the behavior of HTTP + PUT and POST: http://stackoverflow.com/questions/630453/put-vs-post-in-rest + """ + + model = None + queries = None + + _PAYLOAD_DATA = 'data' + + ### + ### Input Handling + ### + + def _get_body_as_data(self): + """Returns the body data based on the content_type requested by the + client. + """ + ### Locate body data by content type + if self.message.content_type == 'application/json': + body = self.message.body + else: + body = self.get_argument('data') + + ### Load found JSON into Python structure + if body: + body = json.loads(body) + + return body + + def _convert_to_id(self, datum): + """`datum` in this function is an id that needs to be validated and + converted to it's native type. + """ + try: + converted = self.model.id.validate(datum) # interface might change + return (True, converted) + except Exception, e: + return (False, e) + + def _convert_to_model(self, datum): + """Handles the details of converting some data into a model or + information about why data was invalid. + """ + try: + converted = self.model(**datum) + converted.validate() + return (True, converted) + except Exception, e: + return (False, e) + + def _convert_item_or_list(self, body_data, is_list, converter): + """This function takes the output of a _get_body* function and checks + it against the model for inputs. + + In some cases this is a list of IDs or in others it's a complete + document. The details of this are controlled by the `converter` + function, provided as the last argument. + + If a list is provided, the output is a boolean and a list of + two-tuples, each containing a boolean and the converted datum, as + provided by the `converter` function. + + If a single item is provided as input the converter function is called + and the output is returned. + """ + if not body_data: + return (True, None) + + if is_list: + results = list() + all_valid = True + for idd in body_data: + (is_valid, data) = converter(idd) + if not is_valid: + all_valid = False + results.append((is_valid, data)) + return (all_valid, results) + else: + (is_valid, data) = converter(body_data) + return (is_valid, data) + + ### + ### Output Processing + ### + + def _crud_to_http(self, crud_status): + """Translates the crud status returned by a `QuerySet` into the status + used for HTTP. + """ + if self.queries.MSG_FAILED == crud_status: + return self._FAILED_CODE + + elif self.queries.MSG_CREATED == crud_status: + return self._CREATED_CODE + + elif self.queries.MSG_UPDATED == crud_status: + return self._UPDATED_CODE + + elif self.queries.MSG_OK == crud_status: + return self._SUCCESS_CODE + + elif len(crud_status) == 0: + return self._SUCCESS_CODE + + else: + return self._SERVER_ERROR + + def _make_presentable(self, datum): + """This function takes either a model instance or a dictionary + representation of some model and returns a dictionary one safe for + transmitting as payload. + """ + if isinstance(datum, dict): + iid = str(datum.get('id')) + model_instance = self.model(**datum) + instance = to_json(model_instance, encode=False) + else: + iid = str(datum.id) + instance = to_json(datum, encode=False) + + data = make_safe_json(self.model, instance, 'owner', encode=False) + + return data + + def _add_status(self, datum, status_code): + """Passed a status tuples of the form (status code, processed model), + it generates the status structure to carry info about the processing. + """ + datum[self._STATUS_CODE] = status_code + status_msg = self._response_codes.get(status_code, + str(status_code)) + datum[self._STATUS_MSG] = status_msg + return datum + + def _parse_crud_datum(self, crud_datum): + """Parses the result of some crud operation into an HTTP-ready + datum instead. + """ + (crud_status, datum) = crud_datum + data = self._make_presentable(datum) + http_status_code = self._crud_to_http(crud_status) + data = self._add_status(data, http_status_code) + return (http_status_code, data) + + def _generate_response(self, status_data): + """Parses crud data and generates the full HTTP response. The idea here + is to translate the results of some crud operation into something + appropriate for HTTP. + + `status_data` is ambiguously named because it might be a list and it + might be a single item. This will likely be altered when the crud + interface's ambiguous functions go away too. + """ + ### Case 1: `status_data` is a list + if isinstance(status_data, list): + ### Aggregate all the statuses and collect the data items in a list + statuses = set() + data_list = list() + for status_datum in status_data: + (http_status_code, data) = self._parse_crud_datum(status_datum) + data_list.append(data) + statuses.add(http_status_code) + + ### If no statuses are found, just use 200 + if len(statuses) == 0: + http_status_code = self._SUCCESS_CODE + ### If more than one status, use HTTP 207 + elif len(statuses) > 1: + http_status_code = self._MULTI_CODE + ### If only one status is there, use it for the HTTP status + else: + http_status_code = statuses.pop() + + ### Store accumulated data to payload + self.add_to_payload(self._PAYLOAD_DATA, data_list) + + ### Return full HTTP response + return self.render(status_code=http_status_code) + + ### Case 2: `status_data` is a single item + else: + ### Extract datum + (http_status_code, data) = self._parse_crud_datum(status_data) + + ### Store datum as data on payload + self.add_to_payload(self._PAYLOAD_DATA, data) + + ### Return full HTTP response + return self.render(status_code=http_status_code) + + ### + ### Validation + ### + + def url_matches_body(self, ids, shields): + """ We want to make sure that if the request asks for a specific few + resources, those resources and only those resources are in the body + """ + if not ids: + return True + + if isinstance(shields, list): + for item_id, shield in zip(ids, shields): + if item_id != str(shield.id): # enforce a good request + return False + else: + return ids != str(shields) + + return True + + ### + ### HTTP methods + ### + + ### There is a pattern that is used in each of the calls. The basic idea is + ### to handle three cases in an appropriate way. These three cases apply to + ### input provided in the URL, such as document ids, or data provided via + ### an HTTP method, like POST. + ### + ### For URLs we handle 0 IDs, 1 ID, and N IDs. Zero, One, Infinity. + ### For data we handle 0 datums, 1 datum and N datums. ZOI, again. + ### + ### Paging and authentication will be offered soon. + + def get(self, ids=""): + """HTTP GET implementation. + + IDs: + * 0 IDs: produces a list of items presented. Paging will be available + soon. + * 1 ID: This produces the corresponding document. + * N IDs: This produces a list of corresponding documents. + + Data: N/A + """ + + try: + ### Setup environment + is_list = isinstance(ids, list) + + # Convert arguments + (valid, data) = self._convert_item_or_list(ids, is_list, + self._convert_to_id) + + # CRUD stuff + if is_list: + valid_ids = list() + errors_ids = list() + for status in data: + (is_valid, idd) = status + if is_valid: + valid_ids.append(idd) + else: + error_ids.append(idd) + models = self.queries.read(valid_ids) + response_data = models + else: + datum_tuple = self.queries.read(data) + response_data = datum_tuple + # Handle status update + return self._generate_response(response_data) + + except FourOhFourException: + return self.render(status_code=self._NOT_FOUND) + + def post(self, ids=""): + """HTTP POST implementation. + + The opinion of this `post()` implementation is that POST is ideally + suited for saving documents for the first time. Using POST triggers the + ID generation system and the document is saved with an ID. The ID is + then returned as part of the generated response. + + We are aware there is sometimes some controversy over what POST and PUT + mean. You can please some of the people, some of the time... + + Data: + * 0 Data: This case isn't useful so it throws an error. + * 1 Data: Writes a single document to queryset. + * N Datas: Attempts to write each document to queryset. + """ + body_data = self._get_body_as_data() + is_list = isinstance(body_data, list) + + # Convert arguments + (valid, data) = self._convert_item_or_list(body_data, is_list, + self._convert_to_model) + + if not valid: + return self.render(status_code=self._FAILED_CODE) + + ### If no ids, we attempt to create the data + if ids == "": + statuses = self.queries.create(data) + return self._generate_response(statuses) + else: + if isinstance(ids, list): + items = ids + else: + items = ids.split(self.application.MULTIPLE_ITEM_SEP) + + ### TODO: add informative error message + if not self.url_matches_body(items, data): + return self.render(status_code=self._FAILED_CODE) + + statuses = self.queries.update(data) + return self._generate_response(statuses) + + def put(self, ids=""): + """HTTP PUT implementation. + + The opinion of this `put()` implementation is that PUT is ideally + suited for saving documents that have been saved at least once before, + signaled by the presence of an id. This call will write the entire + input on top of any data previously there, rendering it idempotent, but + also destructive. + + IDs: + * 0 IDs: Generates IDs for each item of input and saves to QuerySet. + * 1 ID: Attempts to map one document from input to the provided ID. + * N IDs: Attempts to one-to-one map documents from input to IDs. + + Data: + * 0 Data: This case isn't useful so it throws an error. + * 1 Data: Writes a single document to queryset. + * N Datas: Attempts to write each document to queryset. + """ + body_data = self._get_body_as_data() + is_list = isinstance(body_data, list) + + # Convert arguments + (valid, data) = self._convert_item_or_list(body_data, is_list, + self._convert_to_model) + + if not valid: + return self.render(status_code=self._FAILED_CODE) + + ### TODO: add informative error message + items = ids.split(self.application.MULTIPLE_ITEM_SEP) + + if not self.url_matches_body(items, data): + return self.render(status_code=self._FAILED_CODE) + + crud_statuses = self.queries.update(data) + return self._generate_response(crud_statuses) + + def delete(self, ids=""): + """HTTP DELETE implementation. + + Basically just attempts to delete documents by the provided ID. + + IDs: + * 0 IDs: Returns a 400 error + * 1 ID: Attempts to delete a single document by ID + * N IDs: Attempts to delete many documents by ID. + + Data: N/A + """ + body_data = self._get_body_as_data() + is_list = isinstance(body_data, list) + crud_statuses = list() + + # Convert arguments + (valid, data) = self._convert_item_or_list(body_data, is_list, + self._convert_to_model) + + if not valid: + return self.render(status_code=400) + + if ids: + item_ids = ids.split(self.application.MULTIPLE_ITEM_SEP) + try: + crud_statuses = self.queries.destroy(item_ids) + except FourOhFourException: + return self.render(status_code=self._NOT_FOUND) + + return self._generate_response(crud_statuses) + diff --git a/brubeck/brubeck/caching.py b/brubeck/brubeck/caching.py new file mode 100644 index 0000000..5dd073b --- /dev/null +++ b/brubeck/brubeck/caching.py @@ -0,0 +1,120 @@ +import os +import time +from exceptions import NotImplementedError + + +### +### Sessions are basically caches +### + +def generate_session_id(): + """Returns random 32 bit string for cache id + """ + return os.urandom(32).encode('hex') + + +### +### Cache storage +### + +class BaseCacheStore(object): + """Ram based cache storage. Essentially uses a dictionary stored in + the app to store cache id => serialized cache data + """ + def __init__(self, **kwargs): + super(BaseCacheStore, self).__init__(**kwargs) + self._cache_store = dict() + + def save(self, key, data, expire=None): + """Save the cache data and metadata to the backend storage + if necessary, as defined by self.dirty == True. On successful + save set dirty to False. + """ + cache_item = { + 'data': data, + 'expire': expire, + } + self._cache_store[key] = cache_item + + def load(self, key): + """Load the stored data from storage backend or return None if the + session was not found. Stale cookies are treated as empty. + """ + try: + if key in self._cache_store: + data = self._cache_store[key] + + # It's an in memory cache, so we must manage + if not data.get('expire', None) or data['expire'] > time.time(): + return data['data'] + return None + except: + return None + + def delete(self, key): + """Remove all data for the `key` from storage. + """ + if key in self._cache_store: + del self._cache_store[key] + + def delete_expired(self): + """Deletes sessions with timestamps in the past from storage. + """ + del_keys = list() + for key, data in self._cache_store.items(): + if data.get('expire', None) and data['expire'] < time.time(): + del_keys.append(key) + map(self.delete, del_keys) + +### +### Redis Cache Store +### + +class RedisCacheStore(BaseCacheStore): + """Redis cache using Redis' EXPIRE command to set + expiration time. `delete_expired` raises NotImplementedError. + Pass the Redis connection instance as `db_conn`. + + ################## + IMPORTANT NOTE: + + This caching store uses a flat namespace for storing keys since + we cannot set an EXPIRE for a hash `field`. Use different + Redis databases to keep applications from overwriting + keys of other applications. + + ################## + + The Redis connection uses the redis-py api located here: + https://github.com/andymccurdy/redis-py + """ + + def __init__(self, redis_connection=None, **kwargs): + super(RedisCacheStore, self).__init__(**kwargs) + self._cache_store = redis_connection + + def save(self, key, data, expire=None): + """expire will be a Unix timestamp + from time.time() + which is + a value in seconds.""" + + pipe = self._cache_store.pipeline() + pipe.set(key, data) + if expire: + expire_seconds = expire - time.time() + assert(expire_seconds > 0) + pipe.expire(key, int(expire_seconds)) + pipe.execute() + + def load(self, key): + """return the value of `key`. If key + does not exist or has expired, `hget` will + return None""" + + return self._cache_store.get(key) + + def delete(self, key): + self._cache_store.delete(key) + + def delete_expired(self): + raise NotImplementedError diff --git a/brubeck/brubeck/connections.py b/brubeck/brubeck/connections.py new file mode 100644 index 0000000..921c0a7 --- /dev/null +++ b/brubeck/brubeck/connections.py @@ -0,0 +1,269 @@ +import ujson as json +from uuid import uuid4 +import cgi +import re +import logging +import Cookie + +from request import to_bytes, to_unicode, parse_netstring, Request +from request_handling import http_response, coro_spawn + + +### +### Connection Classes +### + +class Connection(object): + """This class is an abstraction for how Brubeck sends and receives + messages. The idea is that Brubeck waits to receive messages for some work + and then it responds. Therefore each connection should essentially be a + mechanism for reading a message and a mechanism for responding, if a + response is necessary. + """ + + def __init__(self, incoming=None, outgoing=None): + """The base `__init__()` function configures a unique ID and assigns + the incoming and outgoing mechanisms to a name. + + `in_sock` and `out_sock` feel like misnomers at this time but they are + preserved for a transition period. + """ + self.sender_id = uuid4().hex + self.in_sock = incoming + self.out_sock = outgoing + + def _unsupported(self, name): + """Simple function that raises an exception. + """ + error_msg = 'Subclass of Connection has not implemented `%s()`' % name + raise NotImplementedError(error_msg) + + + def recv(self): + """Receives a raw mongrel2.handler.Request object that you + can then work with. + """ + self._unsupported('recv') + + def _recv_forever_ever(self, fun_forever): + """Calls a handler function that runs forever. The handler can be + interrupted with a ctrl-c, though. + """ + try: + fun_forever() + except KeyboardInterrupt, ki: + # Put a newline after ^C + print '\nBrubeck going down...' + + def send(self, uuid, conn_id, msg): + """Function for sending a single message. + """ + self._unsupported('send') + + def reply(self, req, msg): + """Does a reply based on the given Request object and message. + """ + self.send(req.sender, req.conn_id, msg) + + def reply_bulk(self, uuid, idents, data): + """This lets you send a single message to many currently + connected clients. There's a MAX_IDENTS that you should + not exceed, so chunk your targets as needed. Each target + will receive the message once by Mongrel2, but you don't have + to loop which cuts down on reply volume. + """ + self._unsupported('reply_bulk') + self.send(uuid, ' '.join(idents), data) + + def close(self): + """Close the connection. + """ + self._unsupported('close') + + def close_bulk(self, uuid, idents): + """Same as close but does it to a whole bunch of idents at a time. + """ + self._unsupported('close_bulk') + self.reply_bulk(uuid, idents, "") + + +### +### ZeroMQ +### + +def load_zmq(): + """This function exists to determine where zmq should come from and then + cache that decision at the module level. + """ + if not hasattr(load_zmq, '_zmq'): + from request_handling import CORO_LIBRARY + if CORO_LIBRARY == 'gevent': + from zmq import green as zmq + elif CORO_LIBRARY == 'eventlet': + from eventlet.green import zmq + load_zmq._zmq = zmq + + return load_zmq._zmq + + +def load_zmq_ctx(): + """This function exists to contain the namespace requirements of generating + a zeromq context, while keeping the context at the module level. If other + parts of the system need zeromq, they should use this function for access + to the existing context. + """ + if not hasattr(load_zmq_ctx, '_zmq_ctx'): + zmq = load_zmq() + zmq_ctx = zmq.Context() + load_zmq_ctx._zmq_ctx = zmq_ctx + + return load_zmq_ctx._zmq_ctx + + +### +### Mongrel2 +### + +class Mongrel2Connection(Connection): + """This class is an abstraction for how Brubeck sends and receives + messages. This abstraction makes it possible for something other than + Mongrel2 to be used easily. + """ + MAX_IDENTS = 100 + + def __init__(self, pull_addr, pub_addr): + """sender_id = uuid.uuid4() or anything unique + pull_addr = pull socket used for incoming messages + pub_addr = publish socket used for outgoing messages + + The class encapsulates socket type by referring to it's pull socket + as in_sock and it's publish socket as out_sock. + """ + zmq = load_zmq() + ctx = load_zmq_ctx() + + in_sock = ctx.socket(zmq.PULL) + out_sock = ctx.socket(zmq.PUB) + + super(Mongrel2Connection, self).__init__(in_sock, out_sock) + self.in_addr = pull_addr + self.out_addr = pub_addr + + in_sock.connect(pull_addr) + out_sock.setsockopt(zmq.IDENTITY, self.sender_id) + out_sock.connect(pub_addr) + + def process_message(self, application, message): + """This coroutine looks at the message, determines which handler will + be used to process it, and then begins processing. + + The application is responsible for handling misconfigured routes. + """ + request = Request.parse_msg(message) + if request.is_disconnect(): + return # Ignore disconnect msgs. Dont have areason to do otherwise + handler = application.route_message(request) + result = handler() + + if result: + http_content = http_response(result['body'], result['status_code'], + result['status_msg'], result['headers']) + + application.msg_conn.reply(request, http_content) + + def recv(self): + """Receives a raw mongrel2.handler.Request object that you from the + zeromq socket and return whatever is found. + """ + zmq_msg = self.in_sock.recv() + return zmq_msg + + def recv_forever_ever(self, application): + """Defines a function that will run the primary connection Brubeck uses + for incoming jobs. This function should then call super which runs the + function in a try-except that can be ctrl-c'd. + """ + def fun_forever(): + while True: + request = self.recv() + coro_spawn(self.process_message, application, request) + self._recv_forever_ever(fun_forever) + + def send(self, uuid, conn_id, msg): + """Raw send to the given connection ID at the given uuid, mostly used + internally. + """ + header = "%s %d:%s," % (uuid, len(str(conn_id)), str(conn_id)) + self.out_sock.send(header + ' ' + to_bytes(msg)) + + def reply(self, req, msg): + """Does a reply based on the given Request object and message. + """ + self.send(req.sender, req.conn_id, msg) + + def reply_bulk(self, uuid, idents, data): + """This lets you send a single message to many currently + connected clients. There's a MAX_IDENTS that you should + not exceed, so chunk your targets as needed. Each target + will receive the message once by Mongrel2, but you don't have + to loop which cuts down on reply volume. + """ + self.send(uuid, ' '.join(idents), data) + + def close(self): + """Tells mongrel2 to explicitly close the HTTP connection. + """ + pass + + def close_bulk(self, uuid, idents): + """Same as close but does it to a whole bunch of idents at a time. + """ + self.reply_bulk(uuid, idents, "") + + +### +### WSGI +### + +class WSGIConnection(Connection): + """ + """ + + def __init__(self, port=6767): + super(WSGIConnection, self).__init__() + self.port = port + + def process_message(self, application, environ, callback): + request = Request.parse_wsgi_request(environ) + handler = application.route_message(request) + result = handler() + + wsgi_status = ' '.join([str(result['status_code']), result['status_msg']]) + headers = [(k, v) for k,v in result['headers'].items()] + callback(str(wsgi_status), headers) + + return [to_bytes(result['body'])] + + def recv_forever_ever(self, application): + """Defines a function that will run the primary connection Brubeck uses + for incoming jobs. This function should then call super which runs the + function in a try-except that can be ctrl-c'd. + """ + def fun_forever(): + from brubeck.request_handling import CORO_LIBRARY + print "Serving on port %s..." % (self.port) + + def proc_msg(environ, callback): + return self.process_message(application, environ, callback) + + if CORO_LIBRARY == 'gevent': + from gevent import wsgi + server = wsgi.WSGIServer(('', self.port), proc_msg) + server.serve_forever() + + elif CORO_LIBRARY == 'eventlet': + import eventlet.wsgi + server = eventlet.wsgi.server(eventlet.listen(('', self.port)), + proc_msg) + + self._recv_forever_ever(fun_forever) diff --git a/brubeck/brubeck/datamosh.py b/brubeck/brubeck/datamosh.py new file mode 100644 index 0000000..7cef467 --- /dev/null +++ b/brubeck/brubeck/datamosh.py @@ -0,0 +1,92 @@ +from schematics.models import Model +from schematics.types.base import UUIDType, StringType + + +from brubeck.timekeeping import MillisecondType + + +"""The purpose of the datamosh model is to provide Mixins for building data +models and data handlers. In it's current state, it provides some helpers +for handling HTTP request arguments that map members of a data model. + +I wanted the name of this module to indicate that it's a place to put request +handling code alongside the models they're intended for handling. It's a mosh +pit of data handling logic. +""" + + +### +### Helper Functions +### + +def get_typed_argument(arg_name, default, handler, type_fun): + """Simple short hand for handling type detection of arguments. + """ + value = handler.get_argument(arg_name, default) + try: + value = type_fun(value) + except: + value = default + return value + + +### +### Ownable Data Mixins +### + +class OwnedModelMixin(Model): + """This class standardizes the approach to expressing ownership of data + """ + owner_id = UUIDType(required=True) + owner_username = StringType(max_length=30, required=True) + + +class OwnedHandlerMixin: + """This mixin supports receiving an argument called `owner`, intended to + map to the `owner_username` field in the Model above. + """ + def get_owner_username(self, default_usernam=None): + owner_username = get_typed_argument('owner', default_username, self, + str) + return owner_username + + +### +### Streamable Data Handling +### + +class StreamedModelMixin(Model): + """This class standardizes the way streaming data is handled by adding two + fields that can be used to sort the list. + """ + created_at = MillisecondType(default=0) + updated_at = MillisecondType(default=0) + + +class StreamedHandlerMixin: + """Provides standard definitions for paging arguments + """ + def get_stream_offset(self, default_since=0): + """This function returns some offset for use with either `created_at` + or `updated_at` as provided by `StreamModelMixin`. + """ + since = get_typed_argument('since', default_since, self, long) + return since + + def get_paging_arguments(self, default_page=0, default_count=25, + max_count=25): + """This function checks for arguments called `page` and `count`. It + returns a tuple either with their value or default values. + + `max_count` may be used to put a limit on the number of items in each + page. It defaults to 25, but you can use `max_count=None` for no limit. + """ + page = get_typed_argument('page', default_page, self, int) + count = get_typed_argument('count', default_count, self, int) + if max_count and count > max_count: + count = max_count + + default_skip = page * count + skip = get_typed_argument('skip', default_skip, self, int) + + return (page, count, skip) diff --git a/brubeck/brubeck/models.py b/brubeck/brubeck/models.py new file mode 100644 index 0000000..74dc8e6 --- /dev/null +++ b/brubeck/brubeck/models.py @@ -0,0 +1,126 @@ +### +### DictShield documents +### + +from schematics.models import Model +from schematics.types import (StringType, + BooleanType, + URLType, + EmailType, + LongType) + + +import auth +from timekeeping import curtime +from datamosh import OwnedModelMixin, StreamedModelMixin + +import re + + +### +### User Document +### + +class User(Model): + """Bare minimum to have the concept of a User. + """ + username = StringType(max_length=30, required=True) + password = StringType(max_length=128) + + is_active = BooleanType(default=False) + last_login = LongType(default=curtime) + date_joined = LongType(default=curtime) + + username_regex = re.compile('^[A-Za-z0-9._]+$') + username_min_length = 2 + + class Options: + roles = { + 'owner': blacklist('password', 'is_active'), + } + + def __unicode__(self): + return u'%s' % (self.username) + + def set_password(self, raw_passwd): + """Generates bcrypt hash and salt for storing a user's password. With + bcrypt, the salt is kind of redundant, but this format stays friendly + to other algorithms. + """ + (algorithm, salt, digest) = auth.gen_hexdigest(raw_passwd) + self.password = auth.build_passwd_line(algorithm, salt, digest) + + def check_password(self, raw_password): + """Compares raw_password to password stored for user. Updates + self.last_login on success. + """ + algorithm, salt, hash = auth.split_passwd_line(self.password) + (_, _, user_hash) = auth.gen_hexdigest(raw_password, + algorithm=algorithm, salt=salt) + if hash == user_hash: + self.last_login = curtime() + return True + else: + return False + + @classmethod + def create_user(cls, username, password, email=str()): + """Creates a user document with given username and password + and saves it. + + Validation occurs only for email argument. It makes no assumptions + about password format. + """ + now = curtime() + + username = username.lower() + email = email.strip() + email = email.lower() + + # Username must pass valid character range check. + if not cls.username_regex.match(username): + warning = 'Username failed character validation - username_regex' + raise ValueError(warning) + + # Caller should handle validation exceptions + cls.validate_class_partial(dict(email=email)) + + user = cls(username=username, email=email, date_joined=now) + user.set_password(password) + return user + + +### +### UserProfile +### + +class UserProfile(Model, OwnedModelMixin, StreamedModelMixin): + """The basic things a user profile tends to carry. Isolated in separate + class to keep separate from private data. + """ + # Provided by OwnedModelMixin + #owner_id = ObjectIdField(required=True) + #owner_username = StringField(max_length=30, required=True) + + # streamable # provided by StreamedModelMixin now + #created_at = MillisecondField() + #updated_at = MillisecondField() + + # identity info + name = StringType(max_length=255) + email = EmailType(max_length=100) + website = URLType(max_length=255) + bio = StringType(max_length=100) + location_text = StringType(max_length=100) + avatar_url = URLType(max_length=255) + + class Options: + roles = { + 'owner': blacklist('owner_id'), + } + + def __init__(self, *args, **kwargs): + super(UserProfile, self).__init__(*args, **kwargs) + + def __unicode__(self): + return u'%s' % (self.name) diff --git a/brubeck/brubeck/queryset/__init__.py b/brubeck/brubeck/queryset/__init__.py new file mode 100644 index 0000000..34f0b56 --- /dev/null +++ b/brubeck/brubeck/queryset/__init__.py @@ -0,0 +1,4 @@ +from brubeck.queryset.base import AbstractQueryset +from brubeck.queryset.dict import DictQueryset +from brubeck.queryset.redis import RedisQueryset + diff --git a/brubeck/brubeck/queryset/base.py b/brubeck/brubeck/queryset/base.py new file mode 100644 index 0000000..186df2e --- /dev/null +++ b/brubeck/brubeck/queryset/base.py @@ -0,0 +1,119 @@ +from brubeck.request_handling import FourOhFourException + +class AbstractQueryset(object): + """The design of the `AbstractQueryset` attempts to map RESTful calls + directly to CRUD calls. It also attempts to be compatible with a single + item of a list of items, handling multiple statuses gracefully if + necessary. + + The querysets then must allow for calls to perform typical CRUD operations + on individual items or a list of items. + + By nature of being dependent on complete data models or ids, the system + suggests users follow a key-value methodology. Brubeck believes this is + what we scale into over time and should just build to this model from the + start. + + Implementing the details of particular databases is then to implement the + `create_one`, `create_many`, ..., for all the CRUD operations. MySQL, + Mongo, Redis, etc should be easy to implement while providing everything + necessary for a proper REST API. + """ + + MSG_OK = 'OK' + MSG_UPDATED = 'Updated' + MSG_CREATED = 'Created' + MSG_NOTFOUND = 'Not Found' + MSG_FAILED = 'Failed' + + def __init__(self, db_conn=None, api_id='id'): + self.db_conn = db_conn + self.api_id = api_id + + ### + ### CRUD Operations + ### + + ### Section TODO: + ### * Pagination + ### * Hook in authentication + ### * Key filtering (owner / public) + ### * Make model instantiation an option + + def create(self, shields): + """Commits a list of new shields to the database + """ + if isinstance(shields, list): + return self.create_many(shields) + else: + return self.create_one(shields) + + def read(self, ids): + """Returns a list of items that match ids + """ + if not ids: + return self.read_all() + elif isinstance(ids, list): + return self.read_many(ids) + else: + return self.read_one(ids) + + def update(self, shields): + if isinstance(shields, list): + return self.update_many(shields) + else: + return self.update_one(shields) + + def destroy(self, item_ids): + """ Removes items from the datastore + """ + if isinstance(item_ids, list): + return self.destroy_many(item_ids) + else: + return self.destroy_one(item_ids) + + ### + ### CRUD Implementations + ### + + ### Create Functions + + def create_one(self, shield): + raise NotImplementedError + + def create_many(self, shields): + raise NotImplementedError + + ### Read Functions + + def read_all(self): + """Returns a list of objects in the db + """ + raise NotImplementedError + + def read_one(self, iid): + """Returns a single item from the db + """ + raise NotImplementedError + + def read_many(self, ids): + """Returns a list of objects matching ids from the db + """ + raise NotImplementedError + + ### Update Functions + + def update_one(self, shield): + raise NotImplementedError + + def update_many(self, shields): + raise NotImplementedError + + ### Destroy Functions + + def destroy_one(self, iid): + raise NotImplementedError + + def destroy_many(self, ids): + raise NotImplementedError + diff --git a/brubeck/brubeck/queryset/dict.py b/brubeck/brubeck/queryset/dict.py new file mode 100644 index 0000000..6775682 --- /dev/null +++ b/brubeck/brubeck/queryset/dict.py @@ -0,0 +1,70 @@ +from brubeck.queryset.base import AbstractQueryset +from schematics.serialize import to_python + +class DictQueryset(AbstractQueryset): + """This class exists as an example of how one could implement a Queryset. + This model is an in-memory dictionary and uses the model's id as the key. + + The data stored is the result of calling `to_python()` on the model. + """ + def __init__(self, **kw): + """Set the db_conn to a dictionary. + """ + super(DictQueryset, self).__init__(db_conn=dict(), **kw) + + ### Create Functions + + def create_one(self, shield): + if shield.id in self.db_conn: + status = self.MSG_UPDATED + else: + status = self.MSG_CREATED + + shield_key = str(getattr(shield, self.api_id)) + self.db_conn[shield_key] = to_python(shield) + return (status, shield) + + def create_many(self, shields): + statuses = [self.create_one(shield) for shield in shields] + return statuses + + ### Read Functions + + def read_all(self): + return [(self.MSG_OK, datum) for datum in self.db_conn.values()] + + + def read_one(self, iid): + iid = str(iid) # TODO Should be cleaner + if iid in self.db_conn: + return (self.MSG_OK, self.db_conn[iid]) + else: + return (self.MSG_FAILED, iid) + + def read_many(self, ids): + return [self.read_one(iid) for iid in ids] + + ### Update Functions + def update_one(self, shield): + shield_key = str(getattr(shield, self.api_id)) + self.db_conn[shield_key] = to_python(shield) + return (self.MSG_UPDATED, shield) + + def update_many(self, shields): + statuses = [self.update_one(shield) for shield in shields] + return statuses + + ### Destroy Functions + + def destroy_one(self, item_id): + try: + datum = self.db_conn[item_id] + del self.db_conn[item_id] + except KeyError: + raise FourOhFourException + return (self.MSG_UPDATED, datum) + + def destroy_many(self, ids): + statuses = [self.destroy_one(iid) for iid in ids] + return statuses + diff --git a/brubeck/brubeck/queryset/redis.py b/brubeck/brubeck/queryset/redis.py new file mode 100644 index 0000000..3f6b559 --- /dev/null +++ b/brubeck/brubeck/queryset/redis.py @@ -0,0 +1,133 @@ +from brubeck.queryset.base import AbstractQueryset +from itertools import imap +import ujson as json +import zlib +try: + import redis +except ImportError: + pass + +class RedisQueryset(AbstractQueryset): + """This class uses redis to store the DictShield after + calling it's `to_json()` method. Upon reading from the Redis + store, the object is deserialized using json.loads(). + + Redis connection uses the redis-py api located here: + https://github.com/andymccurdy/redis-py + """ + # TODO: - catch connection exceptions? + # - set Redis EXPIRE and self.expires + # - confirm that the correct status is being returned in + # each circumstance + def __init__(self, compress=False, compress_level=1, **kw): + """The Redis connection wiil be passed in **kw and is used below + as self.db_conn. + """ + super(RedisQueryset, self).__init__(**kw) + self.compress = compress + self.compress_level = compress_level + + def _setvalue(self, shield): + if self.compress: + return zlib.compress(shield.to_json(), self.compress_level) + return shield.to_json() + + def _readvalue(self, value): + if self.compress: + try: + compressed_value = zlib.decompress(value) + return json.loads(zlib.decompress(value)) + except Exception as e: + # value is 0 or None from a Redis return value + return value + if value: + return json.loads(value) + return None + + def _message_factory(self, fail_status, success_status): + """A Redis command often returns some value or 0 after the + operation has returned. + """ + return lambda x: success_status if x else fail_status + + ### Create Functions + + def create_one(self, shield): + shield_value = self._setvalue(shield) + shield_key = str(getattr(shield, self.api_id)) + result = self.db_conn.hset(self.api_id, shield_key, shield_value) + if result: + return (self.MSG_CREATED, shield) + return (self.MSG_UPDATED, shield) + + def create_many(self, shields): + message_handler = self._message_factory(self.MSG_UPDATED, self.MSG_CREATED) + pipe = self.db_conn.pipeline() + for shield in shields: + pipe.hset(self.api_id, str(getattr(shield, self.api_id)), self._setvalue(shield)) + results = zip(imap(message_handler, pipe.execute()), shields) + pipe.reset() + return results + + ### Read Functions + + def read_all(self): + return [(self.MSG_OK, self._readvalue(datum)) for datum in self.db_conn.hvals(self.api_id)] + + def read_one(self, shield_id): + result = self.db_conn.hget(self.api_id, shield_id) + if result: + return (self.MSG_OK, self._readvalue(result)) + return (self.MSG_FAILED, shield_id) + + def read_many(self, shield_ids): + message_handler = self._message_factory(self.MSG_FAILED, self.MSG_OK) + pipe = self.db_conn.pipeline() + for shield_id in shield_ids: + pipe.hget(self.api_id, str(shield_id)) + results = pipe.execute() + pipe.reset() + return zip(imap(message_handler, results), map(self._readvalue, results)) + + ### Update Functions + + def update_one(self, shield): + shield_key = str(getattr(shield, self.api_id)) + message_handler = self._message_factory(self.MSG_UPDATED, self.MSG_CREATED) + status = message_handler(self.db_conn.hset(self.api_id, shield_key, self._setvalue(shield))) + return (status, shield) + + def update_many(self, shields): + message_handler = self._message_factory(self.MSG_UPDATED, self.MSG_CREATED) + pipe = self.db_conn.pipeline() + for shield in shields: + pipe.hset(self.api_id, str(getattr(shield, self.api_id)), self._setvalue(shield)) + results = pipe.execute() + pipe.reset() + return zip(imap(message_handler, results), shields) + + ### Destroy Functions + + def destroy_one(self, shield_id): + pipe = self.db_conn.pipeline() + pipe.hget(self.api_id, shield_id) + pipe.hdel(self.api_id, shield_id) + result = pipe.execute() + pipe.reset() + if result[1]: + return (self.MSG_UPDATED, self._readvalue(result[0])) + return self.MSG_NOTFOUND + + def destroy_many(self, ids): + # TODO: how to handle missing fields, currently returning self.MSG_FAILED + message_handler = self._message_factory(self.MSG_FAILED, self.MSG_UPDATED) + pipe = self.db_conn.pipeline() + for _id in ids: + pipe.hget(self.api_id, _id) + values_results = pipe.execute() + for _id in ids: + pipe.hdel(self.api_id, _id) + delete_results = pipe.execute() + pipe.reset() + return zip(imap(message_handler, delete_results), map(self._readvalue, values_results)) + diff --git a/brubeck/brubeck/request.py b/brubeck/brubeck/request.py new file mode 100644 index 0000000..fc73d1a --- /dev/null +++ b/brubeck/brubeck/request.py @@ -0,0 +1,296 @@ +import cgi +import json +import Cookie +import logging +import urlparse +import re + +def parse_netstring(ns): + length, rest = ns.split(':', 1) + length = int(length) + assert rest[length] == ',', "Netstring did not end in ','" + return rest[:length], rest[length + 1:] + +def to_bytes(data, enc='utf8'): + """Convert anything to bytes + """ + return data.encode(enc) if isinstance(data, unicode) else bytes(data) + + +def to_unicode(s, enc='utf8'): + """Convert anything to unicode + """ + return s if isinstance(s, unicode) else unicode(str(s), encoding=enc) + + +class Request(object): + """Word. + """ + def __init__(self, sender, conn_id, path, headers, body, url, *args, **kwargs): + self.sender = sender + self.path = path + self.conn_id = conn_id + self.headers = headers + self.body = body + self.url_parts = urlparse.urlsplit(url) if isinstance(url, basestring) else url + + if self.method == 'JSON': + self.data = json.loads(body) + else: + self.data = {} + + ### populate arguments with QUERY string + self.arguments = {} + if 'QUERY' in self.headers: + query = self.headers['QUERY'] + arguments = cgi.parse_qs(query.encode("utf-8")) + for name, values in arguments.iteritems(): + values = [v for v in values if v] + if values: + self.arguments[name] = values + + ### handle data, multipart or not + if self.method in ("POST", "PUT") and self.content_type: + form_encoding = "application/x-www-form-urlencoded" + if self.content_type.startswith(form_encoding): + arguments = cgi.parse_qs(self.body) + for name, values in arguments.iteritems(): + values = [v for v in values if v] + if values: + self.arguments.setdefault(name, []).extend(values) + # Not ready for this, but soon + elif self.content_type.startswith("multipart/form-data"): + fields = self.content_type.split(";") + for field in fields: + k, sep, v = field.strip().partition("=") + if k == "boundary" and v: + self.arguments = {} + self.files = {} + self._parse_mime_body(v, self.body, self.arguments, + self.files) + break + else: + logging.warning("Invalid multipart/form-data") + + def _parse_mime_body(self, boundary, data, arguments, files): + if boundary.startswith('"') and boundary.endswith('"'): + boundary = boundary[1:-1] + if data.endswith("\r\n"): + footer_length = len(boundary) + 6 + else: + footer_length = len(boundary) + 4 + data = str(data) + parts = data[:-footer_length].split("--" + str(boundary) + "\r\n") + for part in parts: + if not part: + continue + eoh = part.find("\r\n\r\n") + if eoh == -1: + logging.warning("multipart/form-data missing headers") + continue + #headers = HTTPHeaders.parse(part[:eoh].decode("utf-8")) + header_string = part[:eoh].decode("utf-8") + headers = dict() + last_key = '' + for line in header_string.splitlines(): + if line[0].isspace(): + # continuation of a multi-line header + new_part = ' ' + line.lstrip() + headers[last_key] += new_part + else: + name, value = line.split(":", 1) + last_key = "-".join([w.capitalize() for w in name.split("-")]) + headers[name] = value.strip() + + disp_header = headers.get("Content-Disposition", "") + disposition, disp_params = self._parse_header(disp_header) + if disposition != "form-data" or not part.endswith("\r\n"): + logging.warning("Invalid multipart/form-data") + continue + value = part[eoh + 4:-2] + if not disp_params.get("name"): + logging.warning("multipart/form-data value missing name") + continue + name = disp_params["name"] + if disp_params.get("filename"): + ctype = headers.get("Content-Type", "application/unknown") + files.setdefault(name, []).append(dict( + filename=disp_params["filename"], body=value, + content_type=ctype)) + else: + arguments.setdefault(name, []).append(value) + + def _parseparam(self, s): + while s[:1] == ';': + s = s[1:] + end = s.find(';') + while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2: + end = s.find(';', end + 1) + if end < 0: + end = len(s) + f = s[:end] + yield f.strip() + s = s[end:] + + def _parse_header(self, line): + """Parse a Content-type like header. + + Return the main content-type and a dictionary of options. + """ + parts = self._parseparam(';' + line) + key = parts.next() + pdict = {} + for p in parts: + i = p.find('=') + if i >= 0: + name = p[:i].strip().lower() + value = p[i + 1:].strip() + if len(value) >= 2 and value[0] == value[-1] == '"': + value = value[1:-1] + value = value.replace('\\\\', '\\').replace('\\"', '"') + pdict[name] = value + return key, pdict + + @property + def method(self): + return self.headers.get('METHOD') + + @property + def content_type(self): + return self.headers.get("content-type") + + @property + def version(self): + return self.headers.get('VERSION') + + @property + def remote_addr(self): + return self.headers.get('x-forwarded-for') + + @property + def cookies(self): + """Lazy generation of cookies from request headers.""" + if not hasattr(self, "_cookies"): + self._cookies = Cookie.SimpleCookie() + if "cookie" in self.headers: + try: + cookies = self.headers['cookie'] + self._cookies.load(to_bytes(cookies)) + except Exception, e: + logging.error('Failed to load cookies') + self.clear_all_cookies() + return self._cookies + + @property + def url(self): + return self.url_parts.geturl() + + @staticmethod + def parse_msg(msg): + """Static method for constructing a Request instance out of a + message read straight off a zmq socket. + """ + sender, conn_id, path, rest = msg.split(' ', 3) + headers, rest = parse_netstring(rest) + body, _ = parse_netstring(rest) + headers = json.loads(headers) + # construct url from request + scheme = headers.get('URL_SCHEME', 'http') + netloc = headers.get('host') + path = headers.get('PATH') + query = headers.get('QUERY') + url = urlparse.SplitResult(scheme, netloc, path, query, None) + r = Request(sender, conn_id, path, headers, body, url) + r.is_wsgi = False + return r + + @staticmethod + def parse_wsgi_request(environ): + """Static method for constructing Request instance out of environ + dict from wsgi server.""" + conn_id = None + sender = "WSGI_server" + path = environ['PATH_INFO'] + body = "" + if "CONTENT_LENGTH" in environ and environ["CONTENT_LENGTH"]: + body = environ["wsgi.input"].read(int(environ['CONTENT_LENGTH'])) + del environ["CONTENT_LENGTH"] + del environ["wsgi.input"] + #setting headers to environ dict with no manipulation + headers = environ + # normalize request dict + if 'REQUEST_METHOD' in headers: + headers['METHOD'] = headers['REQUEST_METHOD'] + if 'QUERY_STRING' in headers: + headers['QUERY'] = headers['QUERY_STRING'] + if 'CONTENT_TYPE' in headers: + headers['content-type'] = headers['CONTENT_TYPE'] + headers['version'] = 1.1 #TODO: hardcoded! + if 'HTTP_COOKIE' in headers: + headers['cookie'] = headers['HTTP_COOKIE'] + if 'HTTP_CONNECTION' in headers: + headers['connection'] = headers['HTTP_CONNECTION'] + # construct url from request + scheme = headers['wsgi.url_scheme'] + netloc = headers.get('HTTP_HOST') + if not netloc: + netloc = headers['SERVER_NAME'] + port = headers['SERVER_PORT'] + if ((scheme == 'https' and port != '443') or + (scheme == 'http' and port != '80')): + netloc += ':' + port + path = headers.get('SCRIPT_NAME', '') + path += headers.get('PATH_INFO', '') + query = headers.get('QUERY_STRING', None) + url = urlparse.SplitResult(scheme, netloc, path, query, None) + r = Request(sender, conn_id, path, headers, body, url) + r.is_wsgi = True + return r + + def is_disconnect(self): + if self.headers.get('METHOD') == 'JSON': + logging.error('DISCONNECT') + return self.data.get('type') == 'disconnect' + + def should_close(self): + """Determines if Request data matches criteria for closing request""" + if self.headers.get('connection') == 'close': + return True + elif self.headers.get('VERSION') == 'HTTP/1.0': + return True + else: + return False + + def get_arguments(self, name, strip=True): + """Returns a list of the arguments with the given name. If the argument + is not present, returns a None. The returned values are always unicode. + """ + values = self.arguments.get(name, None) + if values is None: + return None + + # Get the stripper ready + if strip: + stripper = lambda v: v.strip() + else: + stripper = lambda v: v + + def clean_value(v): + v = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", v) + v = to_unicode(v) + v = stripper(v) + return v + + values = [clean_value(v) for v in values] + return values + + def get_argument(self, name, default=None, strip=True): + """Returns the value of the argument with the given name. + + If the argument appears in the url more than once, we return the + last value. + """ + args = self.get_arguments(name, strip=strip) + if not args: + return default + return args[-1] diff --git a/brubeck/brubeck/request_handling.py b/brubeck/brubeck/request_handling.py new file mode 100755 index 0000000..ec01b02 --- /dev/null +++ b/brubeck/brubeck/request_handling.py @@ -0,0 +1,866 @@ +#!/usr/bin/env python + + +"""Brubeck is a coroutine oriented zmq message handling framework. I learn by +doing and this code base represents where my mind has wandered with regard to +concurrency. + +If you are building a message handling system you should import this class +before anything else to guarantee the eventlet code is run first. + +See github.com/j2labs/brubeck for more information. +""" + +### Attempt to setup gevent +try: + from gevent import monkey + monkey.patch_all() + from gevent import pool + + coro_pool = pool.Pool + + def coro_spawn(function, app, message, *a, **kw): + app.pool.spawn(function, app, message, *a, **kw) + + CORO_LIBRARY = 'gevent' + +### Fallback to eventlet +except ImportError: + try: + import eventlet + eventlet.patcher.monkey_patch(all=True) + + coro_pool = eventlet.GreenPool + + def coro_spawn(function, app, message, *a, **kw): + app.pool.spawn_n(function, app, message, *a, **kw) + + CORO_LIBRARY = 'eventlet' + + except ImportError: + raise EnvironmentError('You need to install eventlet or gevent') + + +from . import version + +import re +import time +import logging +import inspect +import Cookie +import base64 +import hmac +import cPickle as pickle +from itertools import chain +import os, sys +from request import Request, to_bytes, to_unicode + +from schematics.types.serializable import for_jsonschema, from_jsonschema + +import ujson as json + +### +### Common helpers +### + +HTTP_METHODS = ['get', 'post', 'put', 'delete', + 'head', 'options', 'trace', 'connect'] + +HTTP_FORMAT = "HTTP/1.1 %(code)s %(status)s\r\n%(headers)s\r\n\r\n%(body)s" + + +class FourOhFourException(Exception): + pass + + +### +### Result Processing +### + +def render(body, status_code, status_msg, headers): + payload = { + 'body': body, + 'status_code': status_code, + 'status_msg': status_msg, + 'headers': headers, + } + return payload + + +def http_response(body, code, status, headers): + """Renders arguments into an HTTP response. + """ + payload = {'code': code, 'status': status, 'body': body} + content_length = 0 + if body is not None: + content_length = len(to_bytes(body)) + + headers['Content-Length'] = content_length + payload['headers'] = "\r\n".join('%s: %s' % (k, v) + for k, v in headers.items()) + + return HTTP_FORMAT % payload + +def _lscmp(a, b): + """Compares two strings in a cryptographically safe way + """ + return not sum(0 if x == y else 1 + for x, y in zip(a, b)) and len(a) == len(b) + + +### +### Me not *take* cookies, me *eat* the cookies. +### + +def cookie_encode(data, key): + """Encode and sign a pickle-able object. Return a (byte) string + """ + msg = base64.b64encode(pickle.dumps(data, -1)) + sig = base64.b64encode(hmac.new(key, msg).digest()) + return to_bytes('!') + sig + to_bytes('?') + msg + + +def cookie_decode(data, key): + ''' Verify and decode an encoded string. Return an object or None.''' + data = to_bytes(data) + if cookie_is_encoded(data): + sig, msg = data.split(to_bytes('?'), 1) + if _lscmp(sig[1:], base64.b64encode(hmac.new(key, msg).digest())): + return pickle.loads(base64.b64decode(msg)) + return None + + +def cookie_is_encoded(data): + ''' Return True if the argument looks like a encoded cookie.''' + return bool(data.startswith(to_bytes('!')) and to_bytes('?') in data) + + +### +### Message handling +### + +class MessageHandler(object): + """The base class for request handling. It's functionality consists + primarily of a payload system and a way to store some state for + the duration of processing the message. + + Mixins are provided in Brubeck's modules for extending these handlers. + Mixins provide a simple way to add functions to a MessageHandler that are + unique to the message our handler is designed for. Mix in logic as you + realize you need it. Or rip it out. Keep your handlers lean. + + Two callbacks are offered for state preparation. + + The `initialize` function allows users to add steps to object + initialization. A mixin, however, should never use this. You could hook + the request handler up to a database connection pool, for example. + + The `prepare` function is called just before any decorators are called. + The idea here is to give Mixin creators a chance to build decorators that + depend on post-initialization processing to have taken place. You could use + that database connection we created in `initialize` to check the username + and password from a user. + """ + _STATUS_CODE = 'status_code' + _STATUS_MSG = 'status_msg' + _TIMESTAMP = 'timestamp' + _DEFAULT_STATUS = -1 # default to error, earn success + _SUCCESS_CODE = 0 + _AUTH_FAILURE = -2 + _SERVER_ERROR = -5 + + _response_codes = { + 0: 'OK', + -1: 'Bad request', + -2: 'Authentication failed', + -3: 'Not found', + -4: 'Method not allowed', + -5: 'Server error', + } + + def __init__(self, application, message, *args, **kwargs): + """A MessageHandler is called at two major points, with regard to the + eventlet scheduler. __init__ is the first point, which is responsible + for bootstrapping the state of a single handler. + + __call__ is the second major point. + """ + self.application = application + self.message = message + self._payload = dict() + self._finished = False + self.set_status(self._DEFAULT_STATUS) + self.set_timestamp(int(time.time() * 1000)) + self.initialize() + + def initialize(self): + """Hook for subclass. Implementers should be aware that this class's + __init__ calls initialize. + """ + pass + + def prepare(self): + """Called before the message handling method. Code here runs prior to + decorators, so any setup required for decorators to work should happen + here. + """ + pass + + def on_finish(self): + """Called after the message handling method. Counterpart to prepare + """ + pass + + @property + def db_conn(self): + """Short hand to put database connection in easy reach of handlers + """ + return self.application.db_conn + + @property + def supported_methods(self): + """List all the HTTP methods you have defined. + """ + supported_methods = [] + for mef in HTTP_METHODS: + if callable(getattr(self, mef, False)): + supported_methods.append(mef) + return supported_methods + + def unsupported(self): + """Called anytime an unsupported request is made. + """ + return self.render_error(-1) + + def error(self, err): + return self.unsupported() + + def add_to_payload(self, key, value): + """Upserts key-value pair into payload. + """ + self._payload[key] = value + + def clear_payload(self): + """Resets the payload but preserves the current status_code. + """ + status_code = self.status_code + self._payload = dict() + self.set_status(status_code) + self.initialize() + + def set_status(self, status_code, status_msg=None, extra_txt=None): + """Sets the status code of the payload to and sets + status msg to the the relevant msg as defined in _response_codes. + """ + if status_msg is None: + status_msg = self._response_codes.get(status_code, + str(status_code)) + if extra_txt: + status_msg = '%s - %s' % (status_msg, extra_txt) + self.add_to_payload(self._STATUS_CODE, status_code) + self.add_to_payload(self._STATUS_MSG, status_msg) + + @property + def status_code(self): + return self._payload[self._STATUS_CODE] + + @property + def status_msg(self): + return self._payload[self._STATUS_MSG] + + @property + def current_time(self): + return self._payload[self._TIMESTAMP] + + def set_timestamp(self, timestamp): + """Sets the timestamp to given timestamp. + """ + self.add_to_payload(self._TIMESTAMP, timestamp) + self.timestamp = timestamp + + def render(self, status_code=None, hide_status=False, **kwargs): + """Renders entire payload as json dump. Subclass and overwrite this + function if a different output format is needed. See WebMessageHandler + as an example. + """ + if not status_code: + status_code = self.status_code + self.set_status(status_code) + rendered = json.dumps(self._payload) + return rendered + + def render_error(self, status_code, error_handler=None, **kwargs): + """Clears the payload before rendering the error status. + Takes a callable to perform customization before rendering the output. + """ + self.clear_payload() + if error_handler: + error_handler() + self._finished = True + return self.render(status_code=status_code) + + def __call__(self): + """This function handles mapping the request type to a function on + the request handler. + + It requires a method attribute to indicate which function on the + handler should be called. If that function is not supported, call the + handlers unsupported function. + + In the event that an error has already occurred, _finished will be + set to true before this function call indicating we should render + the handler and nothing else. + + In all cases, generating a response for mongrel2 is attempted. + """ + try: + self.prepare() + if not self._finished: + mef = self.message.method.lower() # M-E-T-H-O-D man! + + # Find function mapped to method on self + if mef in HTTP_METHODS: + fun = getattr(self, mef, self.unsupported) + else: + fun = self.unsupported + + # Call the function we settled on + try: + if not hasattr(self, '_url_args') or self._url_args is None: + self._url_args = [] + + if isinstance(self._url_args, dict): + ### if the value was optional and not included, filter it + ### out so the functions default takes priority + kwargs = dict((k, v) + for k, v in self._url_args.items() if v) + rendered = fun(**kwargs) + else: + rendered = fun(*self._url_args) + + if rendered is None: + logging.debug('Handler had no return value: %s' % fun) + return '' + except Exception, e: + logging.error(e, exc_info=True) + rendered = self.error(e) + + self._finished = True + return rendered + else: + return self.render() + finally: + self.on_finish() + + +class WebMessageHandler(MessageHandler): + """A base class for common functionality in a request handler. + + Tornado's design inspired this design. + """ + _DEFAULT_STATUS = 500 # default to server error + _SUCCESS_CODE = 200 + _UPDATED_CODE = 200 + _CREATED_CODE = 201 + _MULTI_CODE = 207 + _FAILED_CODE = 400 + _AUTH_FAILURE = 401 + _FORBIDDEN = 403 + _NOT_FOUND = 404 + _NOT_ALLOWED = 405 + _SERVER_ERROR = 500 + + _response_codes = { + 200: 'OK', + 400: 'Bad request', + 401: 'Authentication failed', + 403: 'Forbidden', + 404: 'Not found', + 405: 'Method not allowed', + 500: 'Server error', + } + + ### + ### Payload extension + ### + + _HEADERS = 'headers' + + def initialize(self): + """WebMessageHandler extends the payload for body and headers. It + also provides both fields as properties to mask storage in payload + """ + self.body = '' + self.headers = dict() + + def set_body(self, body, headers=None, status_code=_SUCCESS_CODE): + """ + """ + self.body = body + self.set_status(status_code) + if headers is not None: + self.headers = headers + + ### + ### Supported HTTP request methods are mapped to these functions + ### + + def options(self, *args, **kwargs): + """Default to allowing all of the methods you have defined and public + """ + self.headers["Access-Control-Allow-Methods"] = self.supported_methods + self.set_status(200) + return self.render() + + def unsupported(self, *args, **kwargs): + def allow_header(): + methods = str.join(', ', map(str.upper, self.supported_methods)) + self.headers['Allow'] = methods + return self.render_error(self._NOT_ALLOWED, error_handler=allow_header) + + def error(self, err): + self.render_error(self._SERVER_ERROR) + + def redirect(self, url): + """Clears the payload before rendering the error status + """ + logging.debug('Redirecting to url: %s' % url) + self.clear_payload() + self._finished = True + msg = 'Page has moved to %s' % url + self.set_status(302, status_msg=msg) + self.headers['Location'] = '%s' % url + return self.render() + + ### + ### Helpers for accessing request variables + ### + + def get_argument(self, name, default=None, strip=True): + """Returns the value of the argument with the given name. + + If the argument appears in the url more than once, we return the + last value. + """ + return self.message.get_argument(name, default=default, strip=strip) + + def get_arguments(self, name, strip=True): + """Returns a list of the arguments with the given name. + """ + return self.message.get_arguments(name, strip=strip) + + ### + ### Cookies + ### + + ### Incoming cookie functions + + def get_cookie(self, key, default=None, secret=None): + """Retrieve a cookie from message, if present, else fallback to + `default` keyword. Accepts a secret key to validate signed cookies. + """ + value = default + if key in self.message.cookies: + value = self.message.cookies[key].value + if secret and value: + dec = cookie_decode(value, secret) + return dec[1] if dec and dec[0] == key else None + return value + + ### Outgoing cookie functions + + @property + def cookies(self): + """Lazy creation of response cookies.""" + if not hasattr(self, "_cookies"): + self._cookies = Cookie.SimpleCookie() + return self._cookies + + def set_cookie(self, key, value, secret=None, **kwargs): + """Add a cookie or overwrite an old one. If the `secret` parameter is + set, create a `Signed Cookie` (described below). + + `key`: the name of the cookie. + `value`: the value of the cookie. + `secret`: required for signed cookies. + + params passed to as keywords: + `max_age`: maximum age in seconds. + `expires`: a datetime object or UNIX timestamp. + `domain`: the domain that is allowed to read the cookie. + `path`: limits the cookie to a given path + + If neither `expires` nor `max_age` are set (default), the cookie + lasts only as long as the browser is not closed. + """ + if secret: + value = cookie_encode((key, value), secret) + elif not isinstance(value, basestring): + raise TypeError('Secret missing for non-string Cookie.') + + # Set cookie value + self.cookies[key] = value + + # handle keywords + for k, v in kwargs.iteritems(): + self.cookies[key][k.replace('_', '-')] = v + + def delete_cookie(self, key, **kwargs): + """Delete a cookie. Be sure to use the same `domain` and `path` + parameters as used to create the cookie. + """ + kwargs['max_age'] = -1 + kwargs['expires'] = 0 + self.set_cookie(key, '', **kwargs) + + def delete_cookies(self): + """Deletes every cookie received from the user. + """ + for key in self.message.cookies.iterkeys(): + self.delete_cookie(key) + + ### + ### Output generation + ### + + def convert_cookies(self): + """ Resolves cookies into multiline values. + """ + cookie_vals = [c.OutputString() for c in self.cookies.values()] + if len(cookie_vals) > 0: + cookie_str = '\nSet-Cookie: '.join(cookie_vals) + self.headers['Set-Cookie'] = cookie_str + + def render(self, status_code=None, http_200=False, **kwargs): + """Renders payload and prepares the payload for a successful HTTP + response. + + Allows forcing HTTP status to be 200 regardless of request status + for cases where payload contains status information. + """ + if status_code: + self.set_status(status_code) + + # Some API's send error messages in the payload rather than over + # HTTP. Not necessarily ideal, but supported. + status_code = self.status_code + if http_200: + status_code = 200 + + self.convert_cookies() + + response = render(self.body, status_code, self.status_msg, self.headers) + + logging.info('%s %s %s (%s)' % (status_code, self.message.method, + self.message.path, + self.message.remote_addr)) + return response + + +class JSONMessageHandler(WebMessageHandler): + """This class is virtually the same as the WebMessageHandler with a slight + change to how payloads are handled to make them more appropriate for + representing JSON transmissions. + + The `hide_status` flag is used to reduce the payload down to just the data. + """ + def render(self, status_code=None, hide_status=False, **kwargs): + if status_code: + self.set_status(status_code) + + self.convert_cookies() + + self.headers['Content-Type'] = 'application/json' + + if hide_status and 'data' in self._payload: + body = json.dumps(self._payload['data']) + else: + body = json.dumps(self._payload) + + response = render(body, self.status_code, self.status_msg, + self.headers) + + logging.info('%s %s %s (%s)' % (self.status_code, self.message.method, + self.message.path, + self.message.remote_addr)) + return response + + +class JsonSchemaMessageHandler(WebMessageHandler): + manifest = {} + + @classmethod + def add_model(self, model): + self.manifest[model.__name__.lower()] = for_jsonschema(model) + + def get(self): + self.set_body(json.dumps(self.manifest.values())) + return self.render(status_code=200) + + def render(self, status_code=None, **kwargs): + if status_code: + self.set_status(status_code) + + self.convert_cookies() + self.headers['Content-Type'] = "application/schema+json" + + response = render(self.body, status_code, self.status_msg, + self.headers) + + return response + +### +### Application logic +### + +class Brubeck(object): + + MULTIPLE_ITEM_SEP = ',' + + def __init__(self, msg_conn=None, handler_tuples=None, pool=None, + no_handler=None, base_handler=None, template_loader=None, + log_level=logging.INFO, login_url=None, db_conn=None, + cookie_secret=None, api_base_url=None, + *args, **kwargs): + """Brubeck is a class for managing connections to webservers. It + supports Mongrel2 and WSGI while providing an asynchronous system for + managing message handling. + + `msg_conn` should be a `connections.Connection` instance. + + `handler_tuples` is a list of two-tuples. The first item is a regex + for matching the URL requested. The second is the class instantiated + to handle the message. + + `pool` can be an existing coroutine pool, but one will be generated if + one isn't provided. + + `base_handler` is a class that Brubeck can rely on for implementing + error handling functions. + + `template_loader` is a function that builds the template loading + environment. + + `log_level` is a log level mapping to Python's `logging` module's + levels. + + `login_url` is the default URL for a login screen. + + `db_conn` is a database connection to be shared in this process + + `cookie_secret` is a string to use for signing secure cookies. + """ + # All output is sent via logging + # (while i figure out how to do a good abstraction via zmq) + logging.basicConfig(level=log_level) + + # Log whether we're using eventlet or gevent. + logging.info('Using coroutine library: %s' % CORO_LIBRARY) + + # Attach the web server connection + if msg_conn is not None: + self.msg_conn = msg_conn + else: + raise ValueError('No web server connection provided.') + + # Class based route lists should be handled this way. + # It is also possible to use `add_route`, a decorator provided by a + # brubeck instance, that can extend routing tables. + self.handler_tuples = handler_tuples + if self.handler_tuples is not None: + self.init_routes(handler_tuples) + + # We can accept an existing pool or initialize a new pool + if pool is None: + self.pool = coro_pool() + elif callable(pool): + self.pool = pool() + else: + raise ValueError('Unable to initialize coroutine pool') + + # Set a base_handler for handling errors (eg. 404 handler) + self.base_handler = base_handler + if self.base_handler is None: + self.base_handler = WebMessageHandler + + # A database connection is optional. The var name is now in place + self.db_conn = db_conn + + # Login url is optional + self.login_url = login_url + + # API base url is optional + if api_base_url is None: + self.api_base_url = '/' + else: + self.api_base_url = api_base_url + + # This must be set to use secure cookies + self.cookie_secret = cookie_secret + + # Any template engine can be used. Brubeck just needs a function that + # loads the environment without arguments. + # + # It then creates a function that renders templates with the given + # environment and attaches it to self. + if callable(template_loader): + loaded_env = template_loader() + if loaded_env: + self.template_env = loaded_env + + # Create template rendering function + def render_template(template_file, **context): + """Renders template using provided template environment. + """ + if hasattr(self, 'template_env'): + t_env = self.template_env + template = t_env.get_template(template_file) + body = template.render(**context or {}) + return body + + # Attach it to brubeck app (self) + setattr(self, 'render_template', render_template) + else: + raise ValueError('template_env failed to load.') + + ### + ### Message routing functions + ### + + def init_routes(self, handler_tuples): + """Loops over a list of (pattern, handler) tuples and adds them + to the routing table. + """ + for ht in handler_tuples: + (pattern, kallable) = ht + self.add_route_rule(pattern, kallable) + + def add_route_rule(self, pattern, kallable): + """Takes a string pattern and callable and adds them to URL routing. + The pattern should be compilable as a regular expression with `re`. + The kallable argument should be a handler. + """ + if not hasattr(self, '_routes'): + self._routes = list() + regex = re.compile(pattern, re.UNICODE) + self._routes.append((regex, kallable)) + + def add_route(self, url_pattern, method=None): + """A decorator to facilitate building routes wth callables. Can be + used as alternative method for constructing routing tables. + """ + if method is None: + method = list() + elif not hasattr(method, '__iter__'): + method = [method] + + def decorator(kallable): + """Decorates a function by adding it to the routing table and + adding code to check the HTTP Method used. + """ + def check_method(app, msg, *args): + """Create new method which checks the HTTP request type. + If URL matches, but unsupported request type is used an + unsupported error is thrown. + + def one_more_layer(): + INCEPTION + """ + if msg.method not in method: + return self.base_handler(app, msg).unsupported() + else: + return kallable(app, msg, *args) + + self.add_route_rule(url_pattern, check_method) + return check_method + return decorator + + def route_message(self, message): + """Factory function that instantiates a request handler based on + path requested. + + If a class that implements `__call__` is used, the class should + implement an `__init__` that receives two arguments: a brubeck instance + and the message to be handled. The return value of this call is a + callable class that is ready to be executed in a follow up coroutine. + + If a function is used (eg with the decorating routing pattern) a + closure is created around the two arguments. The return value of this + call is a function ready to be executed in a follow up coroutine. + """ + handler = None + for (regex, kallable) in self._routes: + url_check = regex.match(message.path) + + if url_check: + ### `None` will fail, so we have to use at least an empty list + ### We should try to use named arguments first, and if they're + ### not present fall back to positional arguments + url_args = url_check.groupdict() or url_check.groups() or [] + + if inspect.isclass(kallable): + ### Handler classes must be instantiated + handler = kallable(self, message) + ### Attach url args to handler + handler._url_args = url_args + return handler + else: + ### Can't instantiate a function + if isinstance(url_args, dict): + ### if the value was optional and not included, filter + ### it out so the functions default takes priority + kwargs = dict((k, v) for k, v in url_args.items() if v) + + handler = lambda: kallable(self, message, **kwargs) + else: + handler = lambda: kallable(self, message, *url_args) + return handler + + if handler is None: + handler = self.base_handler(self, message) + + return handler + + def register_api(self, APIClass, prefix=None): + model, model_name = APIClass.model, APIClass.model.__name__.lower() + + if not JsonSchemaMessageHandler.manifest: + manifest_pattern = "/manifest.json" + self.add_route_rule(manifest_pattern, JsonSchemaMessageHandler) + + if prefix is None: + url_prefix = self.api_base_url + model_name + else: + url_prefix = prefix + + # TODO inspect url pattern for holes + pattern = "/((?P[-\w\d%s]+)(/)*|$)" % self.MULTIPLE_ITEM_SEP + api_url = ''.join([url_prefix, pattern]) + + self.add_route_rule(api_url, APIClass) + JsonSchemaMessageHandler.add_model(model) + + + ### + ### Application running functions + ### + + def recv_forever_ever(self): + """Helper function for starting the link between Brubeck and the + message processing provided by `msg_conn`. + """ + mc = self.msg_conn + mc.recv_forever_ever(self) + + def run(self): + """This method turns on the message handling system and puts Brubeck + in a never ending loop waiting for messages. + + The loop is actually the eventlet scheduler. A goal of Brubeck is to + help users avoid thinking about complex things like an event loop while + still getting the goodness of asynchronous and nonblocking I/O. + """ + greeting = 'Brubeck v%s online ]-----------------------------------' + print greeting % version + + self.recv_forever_ever() diff --git a/brubeck/brubeck/templating.py b/brubeck/brubeck/templating.py new file mode 100644 index 0000000..f2efa47 --- /dev/null +++ b/brubeck/brubeck/templating.py @@ -0,0 +1,164 @@ +from request_handling import WebMessageHandler + + +### +### Mako templates +### + +def load_mako_env(template_dir, *args, **kwargs): + """Returns a function which loads a Mako templates environment. + """ + def loader(): + from mako.lookup import TemplateLookup + if template_dir is not None: + return TemplateLookup(directories=[template_dir or '.'], + *args, **kwargs) + else: + return None + return loader + + +class MakoRendering(WebMessageHandler): + def render_template(self, template_file, + _status_code=WebMessageHandler._SUCCESS_CODE, + **context): + body = self.application.render_template(template_file, **context or {}) + self.set_body(body, status_code=_status_code) + return self.render() + + def render_error(self, error_code): + return self.render_template('errors.html', _status_code=error_code, + **{'error_code': error_code}) + + +### +### Jinja2 +### + +def load_jinja2_env(template_dir, *args, **kwargs): + """Returns a function that loads a jinja template environment. Uses a + closure to provide a namespace around module loading without loading + anything until the caller is ready. + """ + def loader(): + from jinja2 import Environment, FileSystemLoader + if template_dir is not None: + return Environment(loader=FileSystemLoader(template_dir or '.'), + *args, **kwargs) + else: + return None + return loader + + +class Jinja2Rendering(WebMessageHandler): + """Jinja2Rendering is a mixin for for loading a Jinja2 rendering + environment. + + Render success is transmitted via http 200. Rendering failures result in + http 500 errors. + """ + def render_template(self, template_file, + _status_code=WebMessageHandler._SUCCESS_CODE, + **context): + """Renders payload as a jinja template + """ + body = self.application.render_template(template_file, **context or {}) + self.set_body(body, status_code=_status_code) + return self.render() + + def render_error(self, error_code): + """Receives error calls and sends them through a templated renderer + call. + """ + return self.render_template('errors.html', _status_code=error_code, + **{'error_code': error_code}) + + +### +### Tornado +### + +def load_tornado_env(template_dir, *args, **kwargs): + """Returns a function that loads the Tornado template environment. + """ + def loader(): + from tornado.template import Loader + if template_dir is not None: + return Loader(template_dir or '.', *args, **kwargs) + else: + return None + return loader + + +class TornadoRendering(WebMessageHandler): + """TornadoRendering is a mixin for for loading a Tornado rendering + environment. + + Follows usual convention: 200 => success and 500 => failure + + The unusual convention of an underscore in front of a variable is used + to avoid conflict with **context. '_status_code', for now, is a reserved + word. + """ + def render_template(self, template_file, + _status_code=WebMessageHandler._SUCCESS_CODE, + **context): + """Renders payload as a tornado template + """ + body = self.application.render_template(template_file, **context or {}) + self.set_body(body, status_code=_status_code) + return self.render() + + def render_error(self, error_code): + """Receives error calls and sends them through a templated renderer + call. + """ + return self.render_template('errors.html', _status_code=error_code, + **{'error_code': error_code}) + +### +### Mustache +### + +def load_mustache_env(template_dir, *args, **kwargs): + """ + Returns a function that loads a mustache template environment. Uses a + closure to provide a namespace around module loading without loading + anything until the caller is ready. + """ + def loader(): + import pystache + + return pystache.Renderer(search_dirs=[template_dir]) + + return loader + + +class MustacheRendering(WebMessageHandler): + """ + MustacheRendering is a mixin for for loading a Mustache rendering + environment. + + Render success is transmitted via http 200. Rendering failures result in + http 500 errors. + """ + def render_template(self, template_file, + _status_code=WebMessageHandler._SUCCESS_CODE, + **context): + """ + Renders payload as a mustache template + """ + mustache_env = self.application.template_env + + template = mustache_env.load_template(template_file) + body = mustache_env.render(template, context or {}) + + self.set_body(body, status_code=_status_code) + return self.render() + + def render_error(self, error_code): + """Receives error calls and sends them through a templated renderer + call. + """ + return self.render_template('errors', _status_code=error_code, + **{'error_code': error_code}) diff --git a/brubeck/brubeck/timekeeping.py b/brubeck/brubeck/timekeeping.py new file mode 100755 index 0000000..e98985c --- /dev/null +++ b/brubeck/brubeck/timekeeping.py @@ -0,0 +1,90 @@ +import time +from datetime import datetime +from dateutil.parser import parse + +from schematics.types import LongType + + +### +### Main Time Function +### + +def curtime(): + """This funciton is the central method for getting the current time. It + represents the time in milliseconds and the timezone is UTC. + """ + return long(time.time() * 1000) + + +### +### Converstion Helpers +### + +def datestring_to_millis(ds): + """Takes a string representing the date and converts it to milliseconds + since epoch. + """ + dt = parse(ds) + return datetime_to_millis(dt) + + +def datetime_to_millis(dt): + """Takes a datetime instances and converts it to milliseconds since epoch. + """ + seconds = dt.timetuple() + seconds_from_epoch = time.mktime(seconds) + return seconds_from_epoch * 1000 # milliseconds + + +def millis_to_datetime(ms): + """Converts milliseconds into it's datetime equivalent + """ + seconds = ms / 1000.0 + return datetime.fromtimestamp(seconds) + + +### +### Neckbeard date parsing (fuzzy!) +### + +def prettydate(d): + """I <3 U, StackOverflow. + + http://stackoverflow.com/questions/410221/natural-relative-days-in-python + """ + diff = datetime.utcnow() - d + s = diff.seconds + if diff.days > 7 or diff.days < 0: + return d.strftime('%d %b %y') + elif diff.days == 1: + return '1 day ago' + elif diff.days > 1: + return '{0} days ago'.format(diff.days) + elif s <= 1: + return 'just now' + elif s < 60: + return '{0} seconds ago'.format(s) + elif s < 120: + return '1 minute ago' + elif s < 3600: + return '{0} minutes ago'.format(s / 60) + elif s < 7200: + return '1 hour ago' + else: + return '{0} hours ago'.format(s / 3600) + + +### +### Custom Schematics Type +### + +class MillisecondType(LongType): + """High precision time field. + """ + def __set__(self, instance, value): + """__set__ is overriden to allow accepting date strings as input. + dateutil is used to parse strings into milliseconds. + """ + if isinstance(value, (str, unicode)): + value = datestring_to_millis(value) + instance._data[self.field_name] = value diff --git a/brubeck/demos/README.md b/brubeck/demos/README.md new file mode 100644 index 0000000..63b5715 --- /dev/null +++ b/brubeck/demos/README.md @@ -0,0 +1,236 @@ +# Learn By Example + +Each demo attempts to explain some of the nuances of Brubeck. + +Each example should be run from inside the [demos](https://github.com/j2labs/brubeck/blob/master/demos/) directory after Brubeck has been installed. + +This document assumes you have already read the README. If you have not, please read that and come back after. + +## Abstract + +We begin by building some knowledge of Mongrel2's internals using `sqlite3` and `m2reader.py`. + +Then there are four sets of demos. The first set contains the two demos from the README that build request handlers using classes or functions. Then we discuss how URL's are mapped to handlers. Template rendering is then shown for [Jinja2](http://jinja.pocoo.org/), [Tornado templates](http://www.tornadoweb.org/documentation/template.html) and [Mako](http://www.makotemplates.org/). This doc is then finished with an explanation of authentication over two final demos. + + +## Kicking Mongrel2's Tires + +Each of these tests can be run underneath the same Mongrel2 instance. You can bring the handlers down and back up without taking Mongrel2 down. + +First, we parse the config file into a sqlite database. Configuring the database this way makes the experience of editing configs as easy as editing text, but the database is stored in a programmatically friendly way too via [SQLite](http://www.sqlite.org/). + +There is no need to edit the config so we can just load the config into a database using `m2sh load`. + + $ m2sh load -config mongrel2.conf -db the.db + +Now we have a sqlite database representing our config. If you have sqlite installed, open the database and take a look. You can start by typing `.tables` at the prompt to get a table list. + + $ sqlite3 the.db + sqlite> .tables + directory host mimetype route setting + handler log proxy server statistic + sqlite> select * from route; + 1|/|0|1|1|handler + 2|/media/|0|1|1|dir + +We can then turn Mongrel2 on with `m2sh start`. + + $ m2sh start -db the.db -host localhost + ... # lots of output + [INFO] (src/handler.c:285) Binding handler PUSH socket ipc://127.0.0.1:9999 with identity: 34f9ceee-cd52-4b7f-b197-88bf2f0ec378 + [INFO] (src/handler.c:311) Binding listener SUB socket ipc://127.0.0.1:9998 subscribed to: + [INFO] (src/control.c:401) Setting up control socket in at ipc://run/control + +OK. Mongrel2 is now listening on port 6767 and sending messages down a ZeroMQ push socket, ipc://127.0.0.1:9999 + + +### m2reader.py + +Wanna see what Mongrel2 is actually saying? Turn on `m2reader.py`. It won't respond with a proper web request, but you can see the entire JSON message passed to Brubeck from Mongrel2. + + $ ./m2reader.py + 34f9ceee-cd52-4b7f-b197-88bf2f0ec378 0 / 571:{"PATH":"/","x-forwarded-for":"127.0.0.1","accept-language":"en-US,en;q=0.8","accept-encoding":"gzip,deflate,sdch","connection":"keep-alive","accept-charset":"ISO-8859-1,utf-8;q=0.7,*;q=0.3","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","user-agent":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.30 (KHTML, like Gecko) Chrome/12.0.742.122 Safari/534.30","host":"localhost:6767","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/","PATTERN":"/"},0:, + +Brubeck's job is to generate a response and send it to Mongrel2, which Mongrel2 then then forwards to our user. + + +# The Demos + +On the agenda: + +* Classes and Functions +* URL design and handling +* Template rendering +* Authentication + + +# Classes And Functions + +* [https://github.com/j2labs/brubeck/blob/master/demos/demo_minimal.py](https://github.com/j2labs/brubeck/blob/master/demos/demo_minimal.py) +* [https://github.com/j2labs/brubeck/blob/master/demos/demo_noclasses.py](https://github.com/j2labs/brubeck/blob/master/demos/demo_noclasses.py) + +As we saw in the README there are two ways of writing message handlers. `demo_minimal.py` implements a class that implements a `get()` function to answer HTTP GET. `demo_noclasses.py` implements a function with it's URL mapping specified with the `add_route` decorator. + + +# URL Design And Handling + +* [https://github.com/j2labs/brubeck/blob/master/demos/demo_urlargs.py](https://github.com/j2labs/brubeck/blob/master/demos/demo_urlargs.py) + +URL's are matched by regular expression. Sometimes parameters we need are part of the URL. Here is a quick glance at the URL's used for this demo. + + urls = [(r'^/class/(\w+)$', NameHandler), + (r'^/fun/(?P\w+)$', name_handler), + (r'^/', IndexHandler)] + +In spite of being the last URL listed above, `IndexHandler` is the first class defined. This class responds to HTTP GET with the string `'Take five!'`. That's it. + + class IndexHandler(WebMessageHandler): + def get(self): + self.set_body('Take five!') + return self.render() + +The next class, `NameHandler`, defines it's `get()` function differently from `IndexHandler`. The new definition includes the parameter `name`. Notice that in the `urls` above we asign `NameHandler` to pattern `'^/class/(\w+)$'`. + +Whatever matches `(\w+)` will be the value of the `name` argument below. + + class NameHandler(WebMessageHandler): + def get(self, name): + self.set_body('Take five, %s!' % (name)) + return self.render() + +The third handler defined is not a class. This handler is defined as a function. And notice that it also has a `name` argument tacked on. + + def name_handler(application, message, name): + return render('Take five, %s!' % (name), 200, 'OK', {}) + +We then map all three URL's to the relevant handlers and instantiate a `Brubeck` instance. + + app = Brubeck(**config) + +But hey, we'll add one more function just because we still can. + +The `add_route` decorator is now available to us on the Brubeck instance (`app`). Wrap any function with this decorator to assign it to a URL pattern and HTTP method. Passing parameters in URL's works fine here too. + + @app.add_route('^/deco/(?P\w+)$', method='GET') + def new_name_handler(application, message, name): + return render('Take five, %s!' % (name), 200, 'OK', {}) + +Then we turn it on by calling `run()` and all four URL's can answer requests. Try this one: [http://localhost:6767/class/james](http://localhost:6767/class/james). Or this one: [http://localhost:6767/fun/james](http://localhost:6767/fun/james). Or this one: [http://localhost:6767/deco/james](http://localhost:6767/deco/james). + +The only URL left is the boring one: [http://localhost:6767/](http://localhost:6767/). + + +# Template Rendering + +* [https://github.com/j2labs/brubeck/blob/master/demos/demo_jinja2.py](https://github.com/j2labs/brubeck/blob/master/demos/demo_jinja2.py) +* [https://github.com/j2labs/brubeck/blob/master/demos/demo_tornado.py](https://github.com/j2labs/brubeck/blob/master/demos/demo_tornado.py) +* [https://github.com/j2labs/brubeck/blob/master/demos/demo_mako.py](https://github.com/j2labs/brubeck/blob/master/demos/demo_mako.py) + +Template rendering is adequately covered as part of the README for now. + + +# Authentication + +Authentication comes in many forms. The first example will cover the basic system for authenticating requests. The second demo will combine cookies, templates and a hard coded user to demonstrate a full login system. + + +## Auth Over POST + +* [https://github.com/j2labs/brubeck/blob/master/demos/demo_auth.py](https://github.com/j2labs/brubeck/blob/master/demos/demo_auth.py) + +To place authentication restrictions on any function you can use the `@authenticated` decorator. The purpose of this decorator is tell the web server to fail with errors sent via the relevant protocol. When using a `WebMessageHandler` errors will be sent as HTTP level errors. We will discuss another decorator `@web_authenticated` in the next section. + +Here is what using it looks like. + + @authenticated + def post(self): + ... + +For the purpose of the demonstration I hardcode a `User` instance with the username 'jd' and the password 'foo'. Brubeck comes with a `User` and `UserProfile` model but we only use the `User` model here. + + demo_user = User.create_user('jd', 'foo') + + All `get_current_user` does is check the request arguments for a username and password and validate them. Brubeck makes the authenticated user available for you as `self.current_user`. + + Let's try it using it curl. + + $ curl -d "username=jd&password=foo" localhost:6767/brubeck + jd logged in successfully! + +Now let's see it fail. We will tell curl to fail silently, meaning it won't print out any returned HTML, so we can see the 401 error Brubeck returns. + + $ curl -f -d "username=jd&password=bar" localhost:6767/brubeck + curl: (22) The requested URL returned error: 401 + +Someone could build the first draft of an API using this example. All errors would be passed via HTTP. + + +## Authenticated Website + +* [https://github.com/j2labs/brubeck/blob/master/demos/demo_login.py](https://github.com/j2labs/brubeck/blob/master/demos/demo_login.py) + +This example is considerably more involved. Let's look at the URL's before we dig in. + + handler_tuples = [ + (r'^/login', LoginHandler), + (r'^/logout', LogoutHandler), + (r'^/', LandingHandler), + ] + +We can probably guess that `LoginHandler` logs a user in and `LogoutHandler` logs a user out. But what happens if we visit [http://localhost:6767/](http://localhost:6767/) before logging in? + +### Redirection + +Try visiting [http://localhost:6767](http://localhost:6767) and you'll be redirected to [http://localhost:6767/login](http://localhost:6767/login). This happens because we wrapped `LoginHandler`'s `get()` method with the `@web_authenticated` decorator. + + class LandingHandler(CustomAuthMixin, Jinja2Rendering): + @web_authenticated + def get(self): + ... + +Failures to pass authentication are redirected to the application's login_url, as specified in Brubeck's config. + + config = { + ... + 'login_url': '/login', + } + +If you need to redirect a user to the login url at any point in your code, you could write the following. + + return self.redirect(self.application.login_url) + +The implementation of `LoginHandler` is straight forward. The `get()` method renders the login template with fields for a username and password. The implementation of `post()` has the `@web_authenticated` decorator on it, meaning it expects auth credentials to be provided. If the credentials pass `post()` then calls `self.redirect('/')` to send a logged-in user to the landing page. + + +### Authentication Tracking + +A cookie was set the first time `@web_authenticated` was called because we provided the correct username and password. This doesn't happen automatically. It happened because of these two lines in `get_current_user`. + + self.set_cookie('username', username) # DEMO: Don't actually put a + self.set_cookie('password', password) # password in a cookie... + +Notice the comment suggesting you shouldn't actually store a password in the cookie. This is done to keep the demo focused. Secure cookies will be covered soon.. + + +### Authenticated Browsing + +Now that we're logged in, `LandingHandler` let's us call `get()` and it renders `landing.html`. It simply says hello and offers a logout button. + +Clicking logout sends us to [http://localhost:6767/logout](http://localhost:6767/logout) and `LogoutHandler` calls `self.delete_cookies()`. We are no longer authenticated so it sends us the login screen when it's finished. + + +### Secure Cookies + +Brubeck also supports secure cookies. This is what it looks like to use them. + +Setting one: + + self.set_cookie('user_id', username, + secret=self.application.cookie_secret) + +Reading one: + + user_id = self.get_cookie('user_id', + secret=self.application.cookie_secret) + +The [List Surf](https://github.com/j2labs/listsurf) project features secure cookies in it's authentication system. diff --git a/brubeck/demos/demo_auth.py b/brubeck/demos/demo_auth.py new file mode 100755 index 0000000..5a4769d --- /dev/null +++ b/brubeck/demos/demo_auth.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +from brubeck.request_handling import Brubeck, WebMessageHandler +from brubeck.connections import Mongrel2Connection +from brubeck.models import User +from brubeck.auth import authenticated, UserHandlingMixin +import sys +import logging + +### Hardcode a user for the demo +demo_user = User.create_user('jd', 'foo') + +class DemoHandler(WebMessageHandler, UserHandlingMixin): + def get_current_user(self): + """Attempts to load authentication credentials from a request and validate + them. Returns an instantiated User if credentials were good. + + `get_current_user` is a callback triggered by decorating a function + with @authenticated. + """ + username = self.get_argument('username') + password = self.get_argument('password') + + if demo_user.username != username: + logging.error('Auth fail: username incorrect') + return + + if not demo_user.check_password(password): + logging.error('Auth fail: password incorrect') + return + + logging.info('Access granted for user: %s' % username) + return demo_user + + @authenticated + def post(self): + """Requires username and password.""" + self.set_body('%s logged in successfully!' % (self.current_user.username)) + return self.render() + + +config = { + 'msg_conn': Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + 'handler_tuples': [(r'^/brubeck', DemoHandler)], +} + +app = Brubeck(**config) +app.run() diff --git a/brubeck/demos/demo_autoapi.py b/brubeck/demos/demo_autoapi.py new file mode 100755 index 0000000..a72b96f --- /dev/null +++ b/brubeck/demos/demo_autoapi.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python + +"""To use this demo, try entering the following commands in a terminal: + + curl http://localhost:6767/todo/ | python -mjson.tool + + curl -H "content-type: application/json" -f -X POST -d '{"id": "111b4bb7-55f5-441b-ba25-c7a4fd99442c", "text": "Watch more bsg", "order": 1}' http://localhost:6767/todo/111b4bb7-55f5-441b-ba25-c7a4fd99442c/ | python -m json.tool + + curl -H "content-type: application/json" -f -X POST -d '{"id": "222b4bb7-55f5-441b-ba25-c7a4fd994421", "text": "Watch Blade Runner", "order": 2}' http://localhost:6767/todo/222b4bb7-55f5-441b-ba25-c7a4fd994421/ | python -m json.tool + + curl http://localhost:6767/todo/ | python -mjson.tool + + curl http://localhost:6767/todo/222b4bb7-55f5-441b-ba25-c7a4fd994421/ | python -mjson.tool + + curl -H "content-type: application/json" -f -X DELETE http://localhost:6767/todo/222b4bb7-55f5-441b-ba25-c7a4fd994421/ + + curl http://localhost:6767/todo/ | python -mjson.tool + + curl -H "content-type: application/json" -f -X POST -d '[{"id": "333b4bb7-55f5-441b-ba25-c7a4fd99442c", "text": "Write more Brubeck code", "order": 3},{"id": "444b4bb7-55f5-441b-ba25-c7a4fd994421", "text": "Drink coffee", "order": 4}]' http://localhost:6767/todo/ | python -m json.tool + + curl http://localhost:6767/todo/ | python -mjson.tool + + curl -H "content-type: application/json" -f -X POST -d '{"id": "b4bb7-55f5-441b-ba25-c7a4fd994421", "text": "Watch Blade Runner", "order": 2}' http://localhost:6767/todo/222b4bb7-55f5-441b-ba25-c7a4fd994421/ | python -m json.tool + + curl -H "content-type: application/json" -f -X POST -d '{"id": "b4bb7-55f5-441b-ba25-c7a4fd994421", "text": "Watch Blade Runner", "order": 2}' http://localhost:6767/todo/b4bb7-55f5-441b-ba25-c7a4fd994421/ | python -m json.tool +""" + +from brubeck.request_handling import Brubeck +from brubeck.autoapi import AutoAPIBase +from brubeck.queryset import DictQueryset +from brubeck.templating import Jinja2Rendering, load_jinja2_env +from brubeck.connections import Mongrel2Connection + +from schematics.models import Model +from schematics.types import (UUIDType, + StringType, + BooleanType) +from schematics.serialize import wholelist + + +### Todo Model +class Todo(Model): + # status fields + id = UUIDType(auto_fill=True) + completed = BooleanType(default=False) + deleted = BooleanType(default=False) + archived = BooleanType(default=False) + title = StringType(required=True) + + class Options: + roles = { + 'owner': wholelist(), + } + + +### Todo API +class TodosAPI(AutoAPIBase): + queries = DictQueryset() + model = Todo + def render(self, **kwargs): + return super(TodosAPI, self).render(hide_status=True, **kwargs) + + +### Flat page handler +class TodosHandler(Jinja2Rendering): + def get(self): + """A list display matching the parameters of a user's dashboard. The + parameters essentially map to the variation in how `load_listitems` is + called. + """ + return self.render_template('todos.html') + + +### +### Configuration +### + +# Routing config +handler_tuples = [ + (r'^/$', TodosHandler), +] + +# Application config +config = { + 'msg_conn': Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + 'handler_tuples': handler_tuples, + 'template_loader': load_jinja2_env('./templates/autoapi'), +} + +# Instantiate app instance +app = Brubeck(**config) +app.register_api(TodosAPI) +app.run() + diff --git a/brubeck/demos/demo_jinja2.py b/brubeck/demos/demo_jinja2.py new file mode 100755 index 0000000..d753338 --- /dev/null +++ b/brubeck/demos/demo_jinja2.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +from brubeck.request_handling import Brubeck +from brubeck.templating import Jinja2Rendering, load_jinja2_env +from brubeck.connections import Mongrel2Connection +import sys + +class DemoHandler(Jinja2Rendering): + def get(self): + name = self.get_argument('name', 'dude') + context = { + 'name': name, + } + return self.render_template('success.html', **context) + +app = Brubeck(msg_conn=Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + handler_tuples=[(r'^/brubeck', DemoHandler)], + template_loader=load_jinja2_env('./templates/jinja2')) +app.run() diff --git a/brubeck/demos/demo_jinja2_noclasses.py b/brubeck/demos/demo_jinja2_noclasses.py new file mode 100755 index 0000000..418762b --- /dev/null +++ b/brubeck/demos/demo_jinja2_noclasses.py @@ -0,0 +1,24 @@ +#! /usr/bin/env python + + +from brubeck.request_handling import Brubeck, render +from brubeck.connections import Mongrel2Connection +from brubeck.templating import load_jinja2_env, Jinja2Rendering + + +app = Brubeck(msg_conn=Mongrel2Connection('tcp://127.0.0.1:9999', + 'tcp://127.0.0.1:9998'), + template_loader=load_jinja2_env('./templates/jinja2')) + + +@app.add_route('^/', method=['GET', 'POST']) +def index(application, message): + name = message.get_argument('name', 'dude') + context = { + 'name': name, + } + body = application.render_template('success.html', **context) + return render(body, 200, 'OK', {}) + + +app.run() diff --git a/brubeck/demos/demo_login.py b/brubeck/demos/demo_login.py new file mode 100755 index 0000000..daecd6c --- /dev/null +++ b/brubeck/demos/demo_login.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +from brubeck.request_handling import Brubeck, WebMessageHandler +from brubeck.models import User +from brubeck.auth import web_authenticated, UserHandlingMixin +from brubeck.templating import Jinja2Rendering, load_jinja2_env +from brubeck.connections import Mongrel2Connection +import sys +import logging + +### +### Hardcoded authentication +### + +demo_user = User.create_user('jd', 'foo') + +class CustomAuthMixin(WebMessageHandler, UserHandlingMixin): + """This Mixin provides a `get_current_user` implementation that + validates auth against our hardcoded user: `demo_user` + """ + def get_current_user(self): + """Attempts to load user information from cookie. If that + fails, it looks for credentials as arguments. + + If then attempts auth with the found credentials. + """ + # Try loading credentials from cookie + username = self.get_cookie('username') + password = self.get_cookie('password') + + # Fall back to args if cookie isn't provided + if username is None or password is None: + username = self.get_argument('username') + password = self.get_argument('password') + + if demo_user.username != username: + logging.error('Auth fail: bad username') + return + + if not demo_user.check_password(password): + logging.error('Auth fail: bad password') + return + + logging.debug('Access granted for user: %s' % username) + self.set_cookie('username', username) # DEMO: Don't actually put a + self.set_cookie('password', password) # password in a cookie... + + return demo_user + + +### +### Handlers +### + +class LandingHandler(CustomAuthMixin, Jinja2Rendering): + @web_authenticated + def get(self): + """Landing page. Forbids access without authentication + """ + return self.render_template('landing.html') + + +class LoginHandler(CustomAuthMixin, Jinja2Rendering): + def get(self): + """Offers login form to user + """ + return self.render_template('login.html') + + @web_authenticated + def post(self): + """Checks credentials with decorator and sends user authenticated + users to the landing page. + """ + return self.redirect('/') + + +class LogoutHandler(CustomAuthMixin, Jinja2Rendering): + def get(self): + """Clears cookie and sends user to login page + """ + self.delete_cookies() + return self.redirect('/login') + + +### +### Configuration +### + +handler_tuples = [ + (r'^/login', LoginHandler), + (r'^/logout', LogoutHandler), + (r'^/', LandingHandler), +] + +config = { + 'msg_conn': Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + 'handler_tuples': handler_tuples, + 'template_loader': load_jinja2_env('./templates/login'), + 'login_url': '/login', +} + +app = Brubeck(**config) +app.run() diff --git a/brubeck/demos/demo_longpolling.py b/brubeck/demos/demo_longpolling.py new file mode 100755 index 0000000..b86873c --- /dev/null +++ b/brubeck/demos/demo_longpolling.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python + + +from brubeck.request_handling import Brubeck, WebMessageHandler +from brubeck.templating import load_jinja2_env, Jinja2Rendering +from brubeck.connections import Mongrel2Connection +import sys +import datetime +import time + +try: + import eventlet +except: + import gevent + +class DemoHandler(Jinja2Rendering): + def get(self): + name = self.get_argument('name', 'dude') + self.set_body('Take five, %s!' % name) + return self.render_template('base.html') + + +class FeedHandler(WebMessageHandler): + def get(self): + try: + eventlet.sleep(2) # simple way to demo long polling :) + except: + gevent.sleep(2) + self.set_body('The current time is: %s' % datetime.datetime.now(), + headers={'Content-Type': 'text/plain'}) + return self.render() + + +config = { + 'msg_conn': Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + 'handler_tuples': [(r'^/$', DemoHandler), + (r'^/feed', FeedHandler)], + 'template_loader': load_jinja2_env('./templates/longpolling'), +} + + +app = Brubeck(**config) +app.run() diff --git a/brubeck/demos/demo_mako.py b/brubeck/demos/demo_mako.py new file mode 100755 index 0000000..ec58dd2 --- /dev/null +++ b/brubeck/demos/demo_mako.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +from brubeck.request_handling import Brubeck +from brubeck.templating import MakoRendering, load_mako_env +from brubeck.connections import Mongrel2Connection +import sys + + +class DemoHandler(MakoRendering): + def get(self): + name = self.get_argument('name', 'dude') + context = { + 'name': name + } + return self.render_template('success.html', **context) + +app = Brubeck(msg_conn=Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + handler_tuples=[(r'^/brubeck', DemoHandler)], + template_loader=load_mako_env('./templates/mako')) +app.run() diff --git a/brubeck/demos/demo_minimal.py b/brubeck/demos/demo_minimal.py new file mode 100755 index 0000000..cb41bbc --- /dev/null +++ b/brubeck/demos/demo_minimal.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +from brubeck.request_handling import Brubeck, WebMessageHandler +from brubeck.connections import Mongrel2Connection +import sys + +class DemoHandler(WebMessageHandler): + def get(self): + name = self.get_argument('name', 'dude') + self.set_body('Take five, %s!' % name) + return self.render() + +config = { + 'msg_conn': Mongrel2Connection('tcp://127.0.0.1:9999', + 'tcp://127.0.0.1:9998'), + 'handler_tuples': [(r'^/brubeck', DemoHandler)], +} +app = Brubeck(**config) +app.run() diff --git a/brubeck/demos/demo_multipart.py b/brubeck/demos/demo_multipart.py new file mode 100755 index 0000000..43849f3 --- /dev/null +++ b/brubeck/demos/demo_multipart.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python + + +from brubeck.request_handling import Brubeck +from brubeck.models import User +from brubeck.templating import Jinja2Rendering, load_jinja2_env +from brubeck.connections import WSGIConnection, Mongrel2Connection +import sys +import logging +import Image +import StringIO + + +### +### Handlers +### + +class UploadHandler(Jinja2Rendering): + def get(self): + """Offers login form to user + """ + return self.render_template('landing.html') + + def post(self): + """Checks credentials with decorator and sends user authenticated + users to the landing page. + """ + if hasattr(self.message, 'files'): + print 'FILES:', self.message.files['data'][0]['body'] + im = Image.open(StringIO.StringIO(self.message.files['data'][0]['body'])) + print 'IM:', im + im.save('word.png') + return self.redirect('/') + + +### +### Configuration +### + +config = { + #'msg_conn': WSGIConnection(), + 'msg_conn': Mongrel2Connection("tcp://127.0.0.1:9999", "tcp://127.0.0.1:9998"), + 'handler_tuples': [(r'^/', UploadHandler)], + 'template_loader': load_jinja2_env('./templates/multipart'), +} + +app = Brubeck(**config) +app.run() diff --git a/brubeck/demos/demo_mustache.py b/brubeck/demos/demo_mustache.py new file mode 100755 index 0000000..0177c79 --- /dev/null +++ b/brubeck/demos/demo_mustache.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +from brubeck.request_handling import Brubeck +from brubeck.templating import MustacheRendering, load_mustache_env +from brubeck.connections import Mongrel2Connection + +class DemoHandler(MustacheRendering): + def get(self): + name = self.get_argument('name', 'dude') + context = { + 'name': name, + } + return self.render_template('success', **context) + +app = Brubeck(msg_conn=Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + handler_tuples=[(r'^/brubeck', DemoHandler)], + template_loader=load_mustache_env('./templates/mustache')) +app.run() diff --git a/brubeck/demos/demo_noclasses.py b/brubeck/demos/demo_noclasses.py new file mode 100755 index 0000000..c3be87d --- /dev/null +++ b/brubeck/demos/demo_noclasses.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + +from brubeck.request_handling import Brubeck, render +from brubeck.connections import Mongrel2Connection + +app = Brubeck(msg_conn=Mongrel2Connection('tcp://127.0.0.1:9999', + 'tcp://127.0.0.1:9998')) + +@app.add_route('^/brubeck', method='GET') +def foo(application, message): + name = message.get_argument('name', 'dude') + body = 'Take five, %s!' % name + return render(body, 200, 'OK', {}) + +app.run() diff --git a/brubeck/demos/demo_tornado.py b/brubeck/demos/demo_tornado.py new file mode 100755 index 0000000..b78c63b --- /dev/null +++ b/brubeck/demos/demo_tornado.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python + +from brubeck.request_handling import Brubeck +from brubeck.templating import TornadoRendering, load_tornado_env +from brubeck.connections import Mongrel2Connection +import sys + +class DemoHandler(TornadoRendering): + def get(self): + name = self.get_argument('name', 'dude') + context = { + 'name': name, + } + return self.render_template('success.html', **context) + +app = Brubeck(msg_conn=Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + handler_tuples=[(r'^/brubeck', DemoHandler)], + template_loader=load_tornado_env('./templates/tornado')) +app.run() diff --git a/brubeck/demos/demo_urlargs.py b/brubeck/demos/demo_urlargs.py new file mode 100755 index 0000000..dcc6ecd --- /dev/null +++ b/brubeck/demos/demo_urlargs.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python + + +from brubeck.request_handling import Brubeck, WebMessageHandler, render +from brubeck.connections import Mongrel2Connection +import sys + + +class IndexHandler(WebMessageHandler): + def get(self): + self.set_body('Take five!') + return self.render() + +class NameHandler(WebMessageHandler): + def get(self, name): + self.set_body('Take five, %s!' % (name)) + return self.render() + +def name_handler(application, message, name): + return render('Take five, %s!' % (name), 200, 'OK', {}) + + +urls = [(r'^/class/(\w+)$', NameHandler), + (r'^/fun/(?P\w+)$', name_handler), + (r'^/$', IndexHandler)] + +config = { + 'msg_conn': Mongrel2Connection('tcp://127.0.0.1:9999', 'tcp://127.0.0.1:9998'), + 'handler_tuples': urls, +} + +app = Brubeck(**config) + + +@app.add_route('^/deco/(?P\w+)$', method='GET') +def new_name_handler(application, message, name): + return render('Take five, %s!' % (name), 200, 'OK', {}) + + +app.run() diff --git a/brubeck/demos/demo_wsgi.py b/brubeck/demos/demo_wsgi.py new file mode 100755 index 0000000..af95c38 --- /dev/null +++ b/brubeck/demos/demo_wsgi.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +import sys +import os +from brubeck.request_handling import Brubeck, WebMessageHandler +from brubeck.connections import WSGIConnection + +class DemoHandler(WebMessageHandler): + def get(self): + name = self.get_argument('name', 'dude') + self.set_body('Take five, %s!' % name) + return self.render() + +config = { + 'msg_conn': WSGIConnection(), + 'handler_tuples': [(r'^/brubeck', DemoHandler)], +} + +app = Brubeck(**config) +app.run() diff --git a/brubeck/demos/log/.gitignore b/brubeck/demos/log/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/brubeck/demos/m2reader.py b/brubeck/demos/m2reader.py new file mode 100755 index 0000000..99d4ea1 --- /dev/null +++ b/brubeck/demos/m2reader.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python + +import zmq + +ctx = zmq.Context() +s = ctx.socket(zmq.PULL) +s.connect("ipc://127.0.0.1:9999") + +while True: + msg = s.recv() + print msg diff --git a/brubeck/demos/media/index.html b/brubeck/demos/media/index.html new file mode 100644 index 0000000..092bfb9 --- /dev/null +++ b/brubeck/demos/media/index.html @@ -0,0 +1 @@ +yo diff --git a/brubeck/demos/media/jquery-1.5.js b/brubeck/demos/media/jquery-1.5.js new file mode 100644 index 0000000..5c99a8d --- /dev/null +++ b/brubeck/demos/media/jquery-1.5.js @@ -0,0 +1,8176 @@ +/*! + * jQuery JavaScript Library v1.5 + * http://jquery.com/ + * + * Copyright 2011, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Mon Jan 31 08:31:29 2011 -0500 + */ +(function( window, undefined ) { + +// Use the correct document accordingly with window argument (sandbox) +var document = window.document; +var jQuery = (function() { + +// Define a local copy of jQuery +var jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // A central reference to the root jQuery(document) + rootjQuery, + + // A simple way to check for HTML strings or ID strings + // (both of which we optimize for) + quickExpr = /^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/, + + // Check if a string has a non-whitespace character in it + rnotwhite = /\S/, + + // Used for trimming whitespace + trimLeft = /^\s+/, + trimRight = /\s+$/, + + // Check for digits + rdigit = /\d/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>)?$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidescape = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, + rvalidtokens = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + + // Useragent RegExp + rwebkit = /(webkit)[ \/]([\w.]+)/, + ropera = /(opera)(?:.*version)?[ \/]([\w.]+)/, + rmsie = /(msie) ([\w.]+)/, + rmozilla = /(mozilla)(?:.*? rv:([\w.]+))?/, + + // Keep a UserAgent string for use with jQuery.browser + userAgent = navigator.userAgent, + + // For matching the engine and version of the browser + browserMatch, + + // Has the ready events already been bound? + readyBound = false, + + // The deferred used on DOM ready + readyList, + + // Promise methods + promiseMethods = "then done fail isResolved isRejected promise".split( " " ), + + // The ready event handler + DOMContentLoaded, + + // Save a reference to some core methods + toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty, + push = Array.prototype.push, + slice = Array.prototype.slice, + trim = String.prototype.trim, + indexOf = Array.prototype.indexOf, + + // [[Class]] -> type pairs + class2type = {}; + +jQuery.fn = jQuery.prototype = { + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem, ret, doc; + + // Handle $(""), $(null), or $(undefined) + if ( !selector ) { + return this; + } + + // Handle $(DOMElement) + if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + } + + // The body element only exists once, optimize finding it + if ( selector === "body" && !context && document.body ) { + this.context = document; + this[0] = document.body; + this.selector = "body"; + this.length = 1; + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + // Are we dealing with HTML string or an ID? + match = quickExpr.exec( selector ); + + // Verify a match, and that no context was specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + doc = (context ? context.ownerDocument || context : document); + + // If a single string is passed in and it's a single tag + // just do a createElement and skip the rest + ret = rsingleTag.exec( selector ); + + if ( ret ) { + if ( jQuery.isPlainObject( context ) ) { + selector = [ document.createElement( ret[1] ) ]; + jQuery.fn.attr.call( selector, context, true ); + + } else { + selector = [ doc.createElement( ret[1] ) ]; + } + + } else { + ret = jQuery.buildFragment( [ match[1] ], [ doc ] ); + selector = (ret.cacheable ? jQuery.clone(ret.fragment) : ret.fragment).childNodes; + } + + return jQuery.merge( this, selector ); + + // HANDLE: $("#id") + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return (context || rootjQuery).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return rootjQuery.ready( selector ); + } + + if (selector.selector !== undefined) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The current version of jQuery being used + jquery: "1.5", + + // The default length of a jQuery object is 0 + length: 0, + + // The number of elements contained in the matched element set + size: function() { + return this.length; + }, + + toArray: function() { + return slice.call( this, 0 ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems, name, selector ) { + // Build a new jQuery matched element set + var ret = this.constructor(); + + if ( jQuery.isArray( elems ) ) { + push.apply( ret, elems ); + + } else { + jQuery.merge( ret, elems ); + } + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + + ret.context = this.context; + + if ( name === "find" ) { + ret.selector = this.selector + (this.selector ? " " : "") + selector; + } else if ( name ) { + ret.selector = this.selector + "." + name + "(" + selector + ")"; + } + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Attach the listeners + jQuery.bindReady(); + + // Add the callback + readyList.done( fn ); + + return this; + }, + + eq: function( i ) { + return i === -1 ? + this.slice( i ) : + this.slice( i, +i + 1 ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + slice: function() { + return this.pushStack( slice.apply( this, arguments ), + "slice", slice.call(arguments).join(",") ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var options, name, src, copy, copyIsArray, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + noConflict: function( deep ) { + window.$ = _$; + + if ( deep ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Handle when the DOM is ready + ready: function( wait ) { + // A third-party is pushing the ready event forwards + if ( wait === true ) { + jQuery.readyWait--; + } + + // Make sure that the DOM is not already loaded + if ( !jQuery.readyWait || (wait !== true && !jQuery.isReady) ) { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready, 1 ); + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.trigger ) { + jQuery( document ).trigger( "ready" ).unbind( "ready" ); + } + } + }, + + bindReady: function() { + if ( readyBound ) { + return; + } + + readyBound = true; + + // Catch cases where $(document).ready() is called after the + // browser event has already occurred. + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + return setTimeout( jQuery.ready, 1 ); + } + + // Mozilla, Opera and webkit nightlies currently support this event + if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", jQuery.ready, false ); + + // If IE event model is used + } else if ( document.attachEvent ) { + // ensure firing before onload, + // maybe late but safe also for iframes + document.attachEvent("onreadystatechange", DOMContentLoaded); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", jQuery.ready ); + + // If IE and not a frame + // continually check to see if the document is ready + var toplevel = false; + + try { + toplevel = window.frameElement == null; + } catch(e) {} + + if ( document.documentElement.doScroll && toplevel ) { + doScrollCheck(); + } + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + // A crude way of determining if an object is a window + isWindow: function( obj ) { + return obj && typeof obj === "object" && "setInterval" in obj; + }, + + isNaN: function( obj ) { + return obj == null || !rdigit.test( obj ) || isNaN( obj ); + }, + + type: function( obj ) { + return obj == null ? + String( obj ) : + class2type[ toString.call(obj) ] || "object"; + }, + + isPlainObject: function( obj ) { + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + // Not own constructor property must be Object + if ( obj.constructor && + !hasOwn.call(obj, "constructor") && + !hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + + var key; + for ( key in obj ) {} + + return key === undefined || hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + for ( var name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw msg; + }, + + parseJSON: function( data ) { + if ( typeof data !== "string" || !data ) { + return null; + } + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test(data.replace(rvalidescape, "@") + .replace(rvalidtokens, "]") + .replace(rvalidbraces, "")) ) { + + // Try to use the native JSON parser first + return window.JSON && window.JSON.parse ? + window.JSON.parse( data ) : + (new Function("return " + data))(); + + } else { + jQuery.error( "Invalid JSON: " + data ); + } + }, + + // Cross-browser xml parsing + // (xml & tmp used internally) + parseXML: function( data , xml , tmp ) { + + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + + tmp = xml.documentElement; + + if ( ! tmp || ! tmp.nodeName || tmp.nodeName === "parsererror" ) { + jQuery.error( "Invalid XML: " + data ); + } + + return xml; + }, + + noop: function() {}, + + // Evalulates a script in a global context + globalEval: function( data ) { + if ( data && rnotwhite.test(data) ) { + // Inspired by code by Andrea Giammarchi + // http://webreflection.blogspot.com/2007/08/global-scope-evaluation-and-dom.html + var head = document.getElementsByTagName("head")[0] || document.documentElement, + script = document.createElement("script"); + + script.type = "text/javascript"; + + if ( jQuery.support.scriptEval() ) { + script.appendChild( document.createTextNode( data ) ); + } else { + script.text = data; + } + + // Use insertBefore instead of appendChild to circumvent an IE6 bug. + // This arises when a base node is used (#2709). + head.insertBefore( script, head.firstChild ); + head.removeChild( script ); + } + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toUpperCase() === name.toUpperCase(); + }, + + // args is for internal usage only + each: function( object, callback, args ) { + var name, i = 0, + length = object.length, + isObj = length === undefined || jQuery.isFunction(object); + + if ( args ) { + if ( isObj ) { + for ( name in object ) { + if ( callback.apply( object[ name ], args ) === false ) { + break; + } + } + } else { + for ( ; i < length; ) { + if ( callback.apply( object[ i++ ], args ) === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isObj ) { + for ( name in object ) { + if ( callback.call( object[ name ], name, object[ name ] ) === false ) { + break; + } + } + } else { + for ( var value = object[0]; + i < length && callback.call( value, i, value ) !== false; value = object[++i] ) {} + } + } + + return object; + }, + + // Use native String.trim function wherever possible + trim: trim ? + function( text ) { + return text == null ? + "" : + trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + text.toString().replace( trimLeft, "" ).replace( trimRight, "" ); + }, + + // results is for internal usage only + makeArray: function( array, results ) { + var ret = results || []; + + if ( array != null ) { + // The window, strings (and functions) also have 'length' + // The extra typeof function check is to prevent crashes + // in Safari 2 (See: #3039) + // Tweaked logic slightly to handle Blackberry 4.7 RegExp issues #6930 + var type = jQuery.type(array); + + if ( array.length == null || type === "string" || type === "function" || type === "regexp" || jQuery.isWindow( array ) ) { + push.call( ret, array ); + } else { + jQuery.merge( ret, array ); + } + } + + return ret; + }, + + inArray: function( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; + }, + + merge: function( first, second ) { + var i = first.length, + j = 0; + + if ( typeof second.length === "number" ) { + for ( var l = second.length; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var ret = [], retVal; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( var i = 0, length = elems.length; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var ret = [], value; + + // Go through the array, translating each of the items to their + // new value (or values). + for ( var i = 0, length = elems.length; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Flatten any nested arrays + return ret.concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + proxy: function( fn, proxy, thisObject ) { + if ( arguments.length === 2 ) { + if ( typeof proxy === "string" ) { + thisObject = fn; + fn = thisObject[ proxy ]; + proxy = undefined; + + } else if ( proxy && !jQuery.isFunction( proxy ) ) { + thisObject = proxy; + proxy = undefined; + } + } + + if ( !proxy && fn ) { + proxy = function() { + return fn.apply( thisObject || this, arguments ); + }; + } + + // Set the guid of unique handler to the same of original handler, so it can be removed + if ( fn ) { + proxy.guid = fn.guid = fn.guid || proxy.guid || jQuery.guid++; + } + + // So proxy can be declared as an argument + return proxy; + }, + + // Mutifunctional method to get and set values to a collection + // The value/s can be optionally by executed if its a function + access: function( elems, key, value, exec, fn, pass ) { + var length = elems.length; + + // Setting many attributes + if ( typeof key === "object" ) { + for ( var k in key ) { + jQuery.access( elems, k, key[k], exec, fn, value ); + } + return elems; + } + + // Setting one attribute + if ( value !== undefined ) { + // Optionally, function values get executed if exec is true + exec = !pass && exec && jQuery.isFunction(value); + + for ( var i = 0; i < length; i++ ) { + fn( elems[i], key, exec ? value.call( elems[i], i, fn( elems[i], key ) ) : value, pass ); + } + + return elems; + } + + // Getting an attribute + return length ? fn( elems[0], key ) : undefined; + }, + + now: function() { + return (new Date()).getTime(); + }, + + // Create a simple deferred (one callbacks list) + _Deferred: function() { + var // callbacks list + callbacks = [], + // stored [ context , args ] + fired, + // to avoid firing when already doing so + firing, + // flag to know if the deferred has been cancelled + cancelled, + // the deferred itself + deferred = { + + // done( f1, f2, ...) + done: function() { + if ( !cancelled ) { + var args = arguments, + i, + length, + elem, + type, + _fired; + if ( fired ) { + _fired = fired; + fired = 0; + } + for ( i = 0, length = args.length; i < length; i++ ) { + elem = args[ i ]; + type = jQuery.type( elem ); + if ( type === "array" ) { + deferred.done.apply( deferred, elem ); + } else if ( type === "function" ) { + callbacks.push( elem ); + } + } + if ( _fired ) { + deferred.resolveWith( _fired[ 0 ], _fired[ 1 ] ); + } + } + return this; + }, + + // resolve with given context and args + resolveWith: function( context, args ) { + if ( !cancelled && !fired && !firing ) { + firing = 1; + try { + while( callbacks[ 0 ] ) { + callbacks.shift().apply( context, args ); + } + } + finally { + fired = [ context, args ]; + firing = 0; + } + } + return this; + }, + + // resolve with this as context and given arguments + resolve: function() { + deferred.resolveWith( jQuery.isFunction( this.promise ) ? this.promise() : this, arguments ); + return this; + }, + + // Has this deferred been resolved? + isResolved: function() { + return !!( firing || fired ); + }, + + // Cancel + cancel: function() { + cancelled = 1; + callbacks = []; + return this; + } + }; + + return deferred; + }, + + // Full fledged deferred (two callbacks list) + Deferred: function( func ) { + var deferred = jQuery._Deferred(), + failDeferred = jQuery._Deferred(), + promise; + // Add errorDeferred methods, then and promise + jQuery.extend( deferred, { + then: function( doneCallbacks, failCallbacks ) { + deferred.done( doneCallbacks ).fail( failCallbacks ); + return this; + }, + fail: failDeferred.done, + rejectWith: failDeferred.resolveWith, + reject: failDeferred.resolve, + isRejected: failDeferred.isResolved, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj , i /* internal */ ) { + if ( obj == null ) { + if ( promise ) { + return promise; + } + promise = obj = {}; + } + i = promiseMethods.length; + while( i-- ) { + obj[ promiseMethods[ i ] ] = deferred[ promiseMethods[ i ] ]; + } + return obj; + } + } ); + // Make sure only one callback list will be used + deferred.then( failDeferred.cancel, deferred.cancel ); + // Unexpose cancel + delete deferred.cancel; + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + return deferred; + }, + + // Deferred helper + when: function( object ) { + var args = arguments, + length = args.length, + deferred = length <= 1 && object && jQuery.isFunction( object.promise ) ? + object : + jQuery.Deferred(), + promise = deferred.promise(), + resolveArray; + + if ( length > 1 ) { + resolveArray = new Array( length ); + jQuery.each( args, function( index, element ) { + jQuery.when( element ).then( function( value ) { + resolveArray[ index ] = arguments.length > 1 ? slice.call( arguments, 0 ) : value; + if( ! --length ) { + deferred.resolveWith( promise, resolveArray ); + } + }, deferred.reject ); + } ); + } else if ( deferred !== object ) { + deferred.resolve( object ); + } + return promise; + }, + + // Use of jQuery.browser is frowned upon. + // More details: http://docs.jquery.com/Utilities/jQuery.browser + uaMatch: function( ua ) { + ua = ua.toLowerCase(); + + var match = rwebkit.exec( ua ) || + ropera.exec( ua ) || + rmsie.exec( ua ) || + ua.indexOf("compatible") < 0 && rmozilla.exec( ua ) || + []; + + return { browser: match[1] || "", version: match[2] || "0" }; + }, + + sub: function() { + function jQuerySubclass( selector, context ) { + return new jQuerySubclass.fn.init( selector, context ); + } + jQuery.extend( true, jQuerySubclass, this ); + jQuerySubclass.superclass = this; + jQuerySubclass.fn = jQuerySubclass.prototype = this(); + jQuerySubclass.fn.constructor = jQuerySubclass; + jQuerySubclass.subclass = this.subclass; + jQuerySubclass.fn.init = function init( selector, context ) { + if ( context && context instanceof jQuery && !(context instanceof jQuerySubclass) ) { + context = jQuerySubclass(context); + } + + return jQuery.fn.init.call( this, selector, context, rootjQuerySubclass ); + }; + jQuerySubclass.fn.init.prototype = jQuerySubclass.fn; + var rootjQuerySubclass = jQuerySubclass(document); + return jQuerySubclass; + }, + + browser: {} +}); + +// Create readyList deferred +readyList = jQuery._Deferred(); + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +browserMatch = jQuery.uaMatch( userAgent ); +if ( browserMatch.browser ) { + jQuery.browser[ browserMatch.browser ] = true; + jQuery.browser.version = browserMatch.version; +} + +// Deprecated, use jQuery.browser.webkit instead +if ( jQuery.browser.webkit ) { + jQuery.browser.safari = true; +} + +if ( indexOf ) { + jQuery.inArray = function( elem, array ) { + return indexOf.call( array, elem ); + }; +} + +// IE doesn't match non-breaking spaces with \s +if ( rnotwhite.test( "\xA0" ) ) { + trimLeft = /^[\s\xA0]+/; + trimRight = /[\s\xA0]+$/; +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); + +// Cleanup functions for the document ready method +if ( document.addEventListener ) { + DOMContentLoaded = function() { + document.removeEventListener( "DOMContentLoaded", DOMContentLoaded, false ); + jQuery.ready(); + }; + +} else if ( document.attachEvent ) { + DOMContentLoaded = function() { + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( document.readyState === "complete" ) { + document.detachEvent( "onreadystatechange", DOMContentLoaded ); + jQuery.ready(); + } + }; +} + +// The DOM ready check for Internet Explorer +function doScrollCheck() { + if ( jQuery.isReady ) { + return; + } + + try { + // If IE is used, use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + document.documentElement.doScroll("left"); + } catch(e) { + setTimeout( doScrollCheck, 1 ); + return; + } + + // and execute any waiting functions + jQuery.ready(); +} + +// Expose jQuery to the global object +return (window.jQuery = window.$ = jQuery); + +})(); + + +(function() { + + jQuery.support = {}; + + var div = document.createElement("div"); + + div.style.display = "none"; + div.innerHTML = "
a"; + + var all = div.getElementsByTagName("*"), + a = div.getElementsByTagName("a")[0], + select = document.createElement("select"), + opt = select.appendChild( document.createElement("option") ); + + // Can't get basic test support + if ( !all || !all.length || !a ) { + return; + } + + jQuery.support = { + // IE strips leading whitespace when .innerHTML is used + leadingWhitespace: div.firstChild.nodeType === 3, + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + tbody: !div.getElementsByTagName("tbody").length, + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + htmlSerialize: !!div.getElementsByTagName("link").length, + + // Get the style information from getAttribute + // (IE uses .cssText insted) + style: /red/.test( a.getAttribute("style") ), + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + hrefNormalized: a.getAttribute("href") === "/a", + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + opacity: /^0.55$/.test( a.style.opacity ), + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + cssFloat: !!a.style.cssFloat, + + // Make sure that if no value is specified for a checkbox + // that it defaults to "on". + // (WebKit defaults to "" instead) + checkOn: div.getElementsByTagName("input")[0].value === "on", + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + optSelected: opt.selected, + + // Will be defined later + deleteExpando: true, + optDisabled: false, + checkClone: false, + _scriptEval: null, + noCloneEvent: true, + boxModel: null, + inlineBlockNeedsLayout: false, + shrinkWrapBlocks: false, + reliableHiddenOffsets: true + }; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as diabled) + select.disabled = true; + jQuery.support.optDisabled = !opt.disabled; + + jQuery.support.scriptEval = function() { + if ( jQuery.support._scriptEval === null ) { + var root = document.documentElement, + script = document.createElement("script"), + id = "script" + jQuery.now(); + + script.type = "text/javascript"; + try { + script.appendChild( document.createTextNode( "window." + id + "=1;" ) ); + } catch(e) {} + + root.insertBefore( script, root.firstChild ); + + // Make sure that the execution of code works by injecting a script + // tag with appendChild/createTextNode + // (IE doesn't support this, fails, and uses .text instead) + if ( window[ id ] ) { + jQuery.support._scriptEval = true; + delete window[ id ]; + } else { + jQuery.support._scriptEval = false; + } + + root.removeChild( script ); + // release memory in IE + root = script = id = null; + } + + return jQuery.support._scriptEval; + }; + + // Test to see if it's possible to delete an expando from an element + // Fails in Internet Explorer + try { + delete div.test; + + } catch(e) { + jQuery.support.deleteExpando = false; + } + + if ( div.attachEvent && div.fireEvent ) { + div.attachEvent("onclick", function click() { + // Cloning a node shouldn't copy over any + // bound event handlers (IE does this) + jQuery.support.noCloneEvent = false; + div.detachEvent("onclick", click); + }); + div.cloneNode(true).fireEvent("onclick"); + } + + div = document.createElement("div"); + div.innerHTML = ""; + + var fragment = document.createDocumentFragment(); + fragment.appendChild( div.firstChild ); + + // WebKit doesn't clone checked state correctly in fragments + jQuery.support.checkClone = fragment.cloneNode(true).cloneNode(true).lastChild.checked; + + // Figure out if the W3C box model works as expected + // document.body must exist before we can do this + jQuery(function() { + var div = document.createElement("div"), + body = document.getElementsByTagName("body")[0]; + + // Frameset documents with no body should not run this code + if ( !body ) { + return; + } + + div.style.width = div.style.paddingLeft = "1px"; + body.appendChild( div ); + jQuery.boxModel = jQuery.support.boxModel = div.offsetWidth === 2; + + if ( "zoom" in div.style ) { + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + // (IE < 8 does this) + div.style.display = "inline"; + div.style.zoom = 1; + jQuery.support.inlineBlockNeedsLayout = div.offsetWidth === 2; + + // Check if elements with layout shrink-wrap their children + // (IE 6 does this) + div.style.display = ""; + div.innerHTML = "
"; + jQuery.support.shrinkWrapBlocks = div.offsetWidth !== 2; + } + + div.innerHTML = "
t
"; + var tds = div.getElementsByTagName("td"); + + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + // (only IE 8 fails this test) + jQuery.support.reliableHiddenOffsets = tds[0].offsetHeight === 0; + + tds[0].style.display = ""; + tds[1].style.display = "none"; + + // Check if empty table cells still have offsetWidth/Height + // (IE < 8 fail this test) + jQuery.support.reliableHiddenOffsets = jQuery.support.reliableHiddenOffsets && tds[0].offsetHeight === 0; + div.innerHTML = ""; + + body.removeChild( div ).style.display = "none"; + div = tds = null; + }); + + // Technique from Juriy Zaytsev + // http://thinkweb2.com/projects/prototype/detecting-event-support-without-browser-sniffing/ + var eventSupported = function( eventName ) { + var el = document.createElement("div"); + eventName = "on" + eventName; + + // We only care about the case where non-standard event systems + // are used, namely in IE. Short-circuiting here helps us to + // avoid an eval call (in setAttribute) which can cause CSP + // to go haywire. See: https://developer.mozilla.org/en/Security/CSP + if ( !el.attachEvent ) { + return true; + } + + var isSupported = (eventName in el); + if ( !isSupported ) { + el.setAttribute(eventName, "return;"); + isSupported = typeof el[eventName] === "function"; + } + el = null; + + return isSupported; + }; + + jQuery.support.submitBubbles = eventSupported("submit"); + jQuery.support.changeBubbles = eventSupported("change"); + + // release memory in IE + div = all = a = null; +})(); + + + +var rbrace = /^(?:\{.*\}|\[.*\])$/; + +jQuery.extend({ + cache: {}, + + // Please use with caution + uuid: 0, + + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( jQuery.fn.jquery + Math.random() ).replace( /\D/g, "" ), + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000", + "applet": true + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + + return !!elem && !jQuery.isEmptyObject(elem); + }, + + data: function( elem, name, data, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var internalKey = jQuery.expando, getByName = typeof name === "string", thisCache, + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ jQuery.expando ] : elem[ jQuery.expando ] && jQuery.expando; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || (pvt && id && !cache[ id ][ internalKey ])) && getByName && data === undefined ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + elem[ jQuery.expando ] = id = ++jQuery.uuid; + } else { + id = jQuery.expando; + } + } + + if ( !cache[ id ] ) { + cache[ id ] = {}; + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" ) { + if ( pvt ) { + cache[ id ][ internalKey ] = jQuery.extend(cache[ id ][ internalKey ], name); + } else { + cache[ id ] = jQuery.extend(cache[ id ], name); + } + } + + thisCache = cache[ id ]; + + // Internal jQuery data is stored in a separate object inside the object's data + // cache in order to avoid key collisions between internal data and user-defined + // data + if ( pvt ) { + if ( !thisCache[ internalKey ] ) { + thisCache[ internalKey ] = {}; + } + + thisCache = thisCache[ internalKey ]; + } + + if ( data !== undefined ) { + thisCache[ name ] = data; + } + + // TODO: This is a hack for 1.5 ONLY. It will be removed in 1.6. Users should + // not attempt to inspect the internal events object using jQuery.data, as this + // internal data object is undocumented and subject to change. + if ( name === "events" && !thisCache[name] ) { + return thisCache[ internalKey ] && thisCache[ internalKey ].events; + } + + return getByName ? thisCache[ name ] : thisCache; + }, + + removeData: function( elem, name, pvt /* Internal Use Only */ ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var internalKey = jQuery.expando, isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + + // See jQuery.data for more information + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + var thisCache = pvt ? cache[ id ][ internalKey ] : cache[ id ]; + + if ( thisCache ) { + delete thisCache[ name ]; + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( !jQuery.isEmptyObject(thisCache) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( pvt ) { + delete cache[ id ][ internalKey ]; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !jQuery.isEmptyObject(cache[ id ]) ) { + return; + } + } + + var internalCache = cache[ id ][ internalKey ]; + + // Browsers that fail expando deletion also refuse to delete expandos on + // the window, but it will allow it on all other JS objects; other browsers + // don't care + if ( jQuery.support.deleteExpando || cache != window ) { + delete cache[ id ]; + } else { + cache[ id ] = null; + } + + // We destroyed the entire user cache at once because it's faster than + // iterating through each key, but we need to continue to persist internal + // data if it existed + if ( internalCache ) { + cache[ id ] = {}; + cache[ id ][ internalKey ] = internalCache; + + // Otherwise, we need to eliminate the expando on the node to avoid + // false lookups in the cache for entries that no longer exist + } else if ( isNode ) { + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( jQuery.support.deleteExpando ) { + delete elem[ jQuery.expando ]; + } else if ( elem.removeAttribute ) { + elem.removeAttribute( jQuery.expando ); + } else { + elem[ jQuery.expando ] = null; + } + } + }, + + // For internal use only. + _data: function( elem, name, data ) { + return jQuery.data( elem, name, data, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + if ( elem.nodeName ) { + var match = jQuery.noData[ elem.nodeName.toLowerCase() ]; + + if ( match ) { + return !(match === true || elem.getAttribute("classid") !== match); + } + } + + return true; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var data = null; + + if ( typeof key === "undefined" ) { + if ( this.length ) { + data = jQuery.data( this[0] ); + + if ( this[0].nodeType === 1 ) { + var attr = this[0].attributes, name; + for ( var i = 0, l = attr.length; i < l; i++ ) { + name = attr[i].name; + + if ( name.indexOf( "data-" ) === 0 ) { + name = name.substr( 5 ); + dataAttr( this[0], name, data[ name ] ); + } + } + } + } + + return data; + + } else if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + var parts = key.split("."); + parts[1] = parts[1] ? "." + parts[1] : ""; + + if ( value === undefined ) { + data = this.triggerHandler("getData" + parts[1] + "!", [parts[0]]); + + // Try to fetch any internally stored data first + if ( data === undefined && this.length ) { + data = jQuery.data( this[0], key ); + data = dataAttr( this[0], key, data ); + } + + return data === undefined && parts[1] ? + this.data( parts[0] ) : + data; + + } else { + return this.each(function() { + var $this = jQuery( this ), + args = [ parts[0], value ]; + + $this.triggerHandler( "setData" + parts[1] + "!", args ); + jQuery.data( this, key, value ); + $this.triggerHandler( "changeData" + parts[1] + "!", args ); + }); + } + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + data = elem.getAttribute( "data-" + key ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + !jQuery.isNaN( data ) ? parseFloat( data ) : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + + + + +jQuery.extend({ + queue: function( elem, type, data ) { + if ( !elem ) { + return; + } + + type = (type || "fx") + "queue"; + var q = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( !data ) { + return q || []; + } + + if ( !q || jQuery.isArray(data) ) { + q = jQuery._data( elem, type, jQuery.makeArray(data) ); + + } else { + q.push( data ); + } + + return q; + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + fn = queue.shift(); + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + } + + if ( fn ) { + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift("inprogress"); + } + + fn.call(elem, function() { + jQuery.dequeue(elem, type); + }); + } + + if ( !queue.length ) { + jQuery.removeData( elem, type + "queue", true ); + } + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + } + + if ( data === undefined ) { + return jQuery.queue( this[0], type ); + } + return this.each(function( i ) { + var queue = jQuery.queue( this, type, data ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[time] || time : time; + type = type || "fx"; + + return this.queue( type, function() { + var elem = this; + setTimeout(function() { + jQuery.dequeue( elem, type ); + }, time ); + }); + }, + + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + } +}); + + + + +var rclass = /[\n\t\r]/g, + rspaces = /\s+/, + rreturn = /\r/g, + rspecialurl = /^(?:href|src|style)$/, + rtype = /^(?:button|input)$/i, + rfocusable = /^(?:button|input|object|select|textarea)$/i, + rclickable = /^a(?:rea)?$/i, + rradiocheck = /^(?:radio|checkbox)$/i; + +jQuery.props = { + "for": "htmlFor", + "class": "className", + readonly: "readOnly", + maxlength: "maxLength", + cellspacing: "cellSpacing", + rowspan: "rowSpan", + colspan: "colSpan", + tabindex: "tabIndex", + usemap: "useMap", + frameborder: "frameBorder" +}; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, name, value, true, jQuery.attr ); + }, + + removeAttr: function( name, fn ) { + return this.each(function(){ + jQuery.attr( this, name, "" ); + if ( this.nodeType === 1 ) { + this.removeAttribute( name ); + } + }); + }, + + addClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.addClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( value && typeof value === "string" ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 ) { + if ( !elem.className ) { + elem.className = value; + + } else { + var className = " " + elem.className + " ", + setClass = elem.className; + + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + if ( className.indexOf( " " + classNames[c] + " " ) < 0 ) { + setClass += " " + classNames[c]; + } + } + elem.className = jQuery.trim( setClass ); + } + } + } + } + + return this; + }, + + removeClass: function( value ) { + if ( jQuery.isFunction(value) ) { + return this.each(function(i) { + var self = jQuery(this); + self.removeClass( value.call(this, i, self.attr("class")) ); + }); + } + + if ( (value && typeof value === "string") || value === undefined ) { + var classNames = (value || "").split( rspaces ); + + for ( var i = 0, l = this.length; i < l; i++ ) { + var elem = this[i]; + + if ( elem.nodeType === 1 && elem.className ) { + if ( value ) { + var className = (" " + elem.className + " ").replace(rclass, " "); + for ( var c = 0, cl = classNames.length; c < cl; c++ ) { + className = className.replace(" " + classNames[c] + " ", " "); + } + elem.className = jQuery.trim( className ); + + } else { + elem.className = ""; + } + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value, + isBool = typeof stateVal === "boolean"; + + if ( jQuery.isFunction( value ) ) { + return this.each(function(i) { + var self = jQuery(this); + self.toggleClass( value.call(this, i, self.attr("class"), stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + state = stateVal, + classNames = value.split( rspaces ); + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space seperated list + state = isBool ? state : !self.hasClass( className ); + self[ state ? "addClass" : "removeClass" ]( className ); + } + + } else if ( type === "undefined" || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // toggle whole className + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " "; + for ( var i = 0, l = this.length; i < l; i++ ) { + if ( (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) > -1 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + if ( !arguments.length ) { + var elem = this[0]; + + if ( elem ) { + if ( jQuery.nodeName( elem, "option" ) ) { + // attributes.value is undefined in Blackberry 4.7 but + // uses .value. See #6932 + var val = elem.attributes.value; + return !val || val.specified ? elem.value : elem.text; + } + + // We need to handle select boxes special + if ( jQuery.nodeName( elem, "select" ) ) { + var index = elem.selectedIndex, + values = [], + options = elem.options, + one = elem.type === "select-one"; + + // Nothing was selected + if ( index < 0 ) { + return null; + } + + // Loop through all the selected options + for ( var i = one ? index : 0, max = one ? index + 1 : options.length; i < max; i++ ) { + var option = options[ i ]; + + // Don't return options that are disabled or in a disabled optgroup + if ( option.selected && (jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null) && + (!option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" )) ) { + + // Get the specific value for the option + value = jQuery(option).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + } + + // Handle the case where in Webkit "" is returned instead of "on" if a value isn't specified + if ( rradiocheck.test( elem.type ) && !jQuery.support.checkOn ) { + return elem.getAttribute("value") === null ? "on" : elem.value; + } + + // Everything else, we just grab the value + return (elem.value || "").replace(rreturn, ""); + + } + + return undefined; + } + + var isFunction = jQuery.isFunction(value); + + return this.each(function(i) { + var self = jQuery(this), val = value; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call(this, i, self.val()); + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray(val) ) { + val = jQuery.map(val, function (value) { + return value == null ? "" : value + ""; + }); + } + + if ( jQuery.isArray(val) && rradiocheck.test( this.type ) ) { + this.checked = jQuery.inArray( self.val(), val ) >= 0; + + } else if ( jQuery.nodeName( this, "select" ) ) { + var values = jQuery.makeArray(val); + + jQuery( "option", this ).each(function() { + this.selected = jQuery.inArray( jQuery(this).val(), values ) >= 0; + }); + + if ( !values.length ) { + this.selectedIndex = -1; + } + + } else { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + attrFn: { + val: true, + css: true, + html: true, + text: true, + data: true, + width: true, + height: true, + offset: true + }, + + attr: function( elem, name, value, pass ) { + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || elem.nodeType === 2 ) { + return undefined; + } + + if ( pass && name in jQuery.attrFn ) { + return jQuery(elem)[name](value); + } + + var notxml = elem.nodeType !== 1 || !jQuery.isXMLDoc( elem ), + // Whether we are setting (or getting) + set = value !== undefined; + + // Try to normalize/fix the name + name = notxml && jQuery.props[ name ] || name; + + // Only do all the following if this is a node (faster for style) + if ( elem.nodeType === 1 ) { + // These attributes require special treatment + var special = rspecialurl.test( name ); + + // Safari mis-reports the default selected property of an option + // Accessing the parent's selectedIndex property fixes it + if ( name === "selected" && !jQuery.support.optSelected ) { + var parent = elem.parentNode; + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + } + + // If applicable, access the attribute via the DOM 0 way + // 'in' checks fail in Blackberry 4.7 #6931 + if ( (name in elem || elem[ name ] !== undefined) && notxml && !special ) { + if ( set ) { + // We can't allow the type property to be changed (since it causes problems in IE) + if ( name === "type" && rtype.test( elem.nodeName ) && elem.parentNode ) { + jQuery.error( "type property can't be changed" ); + } + + if ( value === null ) { + if ( elem.nodeType === 1 ) { + elem.removeAttribute( name ); + } + + } else { + elem[ name ] = value; + } + } + + // browsers index elements by id/name on forms, give priority to attributes. + if ( jQuery.nodeName( elem, "form" ) && elem.getAttributeNode(name) ) { + return elem.getAttributeNode( name ).nodeValue; + } + + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + if ( name === "tabIndex" ) { + var attributeNode = elem.getAttributeNode( "tabIndex" ); + + return attributeNode && attributeNode.specified ? + attributeNode.value : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + undefined; + } + + return elem[ name ]; + } + + if ( !jQuery.support.style && notxml && name === "style" ) { + if ( set ) { + elem.style.cssText = "" + value; + } + + return elem.style.cssText; + } + + if ( set ) { + // convert the value to a string (all browsers do this but IE) see #1070 + elem.setAttribute( name, "" + value ); + } + + // Ensure that missing attributes return undefined + // Blackberry 4.7 returns "" from getAttribute #6938 + if ( !elem.attributes[ name ] && (elem.hasAttribute && !elem.hasAttribute( name )) ) { + return undefined; + } + + var attr = !jQuery.support.hrefNormalized && notxml && special ? + // Some attributes require a special call on IE + elem.getAttribute( name, 2 ) : + elem.getAttribute( name ); + + // Non-existent attributes return null, we normalize to undefined + return attr === null ? undefined : attr; + } + // Handle everything which isn't a DOM element node + if ( set ) { + elem[ name ] = value; + } + return elem[ name ]; + } +}); + + + + +var rnamespaces = /\.(.*)$/, + rformElems = /^(?:textarea|input|select)$/i, + rperiod = /\./g, + rspace = / /g, + rescape = /[^\w\s.|`]/g, + fcleanup = function( nm ) { + return nm.replace(rescape, "\\$&"); + }, + eventKey = "events"; + +/* + * A number of helper functions used for managing events. + * Many of the ideas behind this code originated from + * Dean Edwards' addEvent library. + */ +jQuery.event = { + + // Bind an event to an element + // Original by Dean Edwards + add: function( elem, types, handler, data ) { + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // For whatever reason, IE has trouble passing the window object + // around, causing it to be cloned in the process + if ( jQuery.isWindow( elem ) && ( elem !== window && !elem.frameElement ) ) { + elem = window; + } + + if ( handler === false ) { + handler = returnFalse; + } else if ( !handler ) { + // Fixes bug #7229. Fix recommended by jdalton + return; + } + + var handleObjIn, handleObj; + + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + } + + // Make sure that the function being executed has a unique ID + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure + var elemData = jQuery._data( elem ); + + // If no elemData is found then we must be trying to bind to one of the + // banned noData elements + if ( !elemData ) { + return; + } + + var events = elemData[ eventKey ], + eventHandle = elemData.handle; + + if ( typeof events === "function" ) { + // On plain objects events is a fn that holds the the data + // which prevents this data from being JSON serialized + // the function does not need to be called, it just contains the data + eventHandle = events.handle; + events = events.events; + + } else if ( !events ) { + if ( !elem.nodeType ) { + // On plain objects, create a fn that acts as the holder + // of the values to avoid JSON serialization of event data + elemData[ eventKey ] = elemData = function(){}; + } + + elemData.events = events = {}; + } + + if ( !eventHandle ) { + elemData.handle = eventHandle = function() { + // Handle the second event of a trigger and when + // an event is called after a page has unloaded + return typeof jQuery !== "undefined" && !jQuery.event.triggered ? + jQuery.event.handle.apply( eventHandle.elem, arguments ) : + undefined; + }; + } + + // Add elem as a property of the handle function + // This is to prevent a memory leak with non-native events in IE. + eventHandle.elem = elem; + + // Handle multiple events separated by a space + // jQuery(...).bind("mouseover mouseout", fn); + types = types.split(" "); + + var type, i = 0, namespaces; + + while ( (type = types[ i++ ]) ) { + handleObj = handleObjIn ? + jQuery.extend({}, handleObjIn) : + { handler: handler, data: data }; + + // Namespaced event handlers + if ( type.indexOf(".") > -1 ) { + namespaces = type.split("."); + type = namespaces.shift(); + handleObj.namespace = namespaces.slice(0).sort().join("."); + + } else { + namespaces = []; + handleObj.namespace = ""; + } + + handleObj.type = type; + if ( !handleObj.guid ) { + handleObj.guid = handler.guid; + } + + // Get the current list of functions bound to this event + var handlers = events[ type ], + special = jQuery.event.special[ type ] || {}; + + // Init the event handler queue + if ( !handlers ) { + handlers = events[ type ] = []; + + // Check for a special event handler + // Only use addEventListener/attachEvent if the special + // events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add the function to the element's handler list + handlers.push( handleObj ); + + // Keep track of which events have been used, for global triggering + jQuery.event.global[ type ] = true; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + global: {}, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, pos ) { + // don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + if ( handler === false ) { + handler = returnFalse; + } + + var ret, type, fn, j, i = 0, all, namespaces, namespace, special, eventType, handleObj, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ), + events = elemData && elemData[ eventKey ]; + + if ( !elemData || !events ) { + return; + } + + if ( typeof events === "function" ) { + elemData = events; + events = events.events; + } + + // types is actually an event object here + if ( types && types.type ) { + handler = types.handler; + types = types.type; + } + + // Unbind all events for the element + if ( !types || typeof types === "string" && types.charAt(0) === "." ) { + types = types || ""; + + for ( type in events ) { + jQuery.event.remove( elem, type + types ); + } + + return; + } + + // Handle multiple events separated by a space + // jQuery(...).unbind("mouseover mouseout", fn); + types = types.split(" "); + + while ( (type = types[ i++ ]) ) { + origType = type; + handleObj = null; + all = type.indexOf(".") < 0; + namespaces = []; + + if ( !all ) { + // Namespaced event handlers + namespaces = type.split("."); + type = namespaces.shift(); + + namespace = new RegExp("(^|\\.)" + + jQuery.map( namespaces.slice(0).sort(), fcleanup ).join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + eventType = events[ type ]; + + if ( !eventType ) { + continue; + } + + if ( !handler ) { + for ( j = 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( all || namespace.test( handleObj.namespace ) ) { + jQuery.event.remove( elem, origType, handleObj.handler, j ); + eventType.splice( j--, 1 ); + } + } + + continue; + } + + special = jQuery.event.special[ type ] || {}; + + for ( j = pos || 0; j < eventType.length; j++ ) { + handleObj = eventType[ j ]; + + if ( handler.guid === handleObj.guid ) { + // remove the given handler for the given type + if ( all || namespace.test( handleObj.namespace ) ) { + if ( pos == null ) { + eventType.splice( j--, 1 ); + } + + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + + if ( pos != null ) { + break; + } + } + } + + // remove generic event handler if no more handlers exist + if ( eventType.length === 0 || pos != null && eventType.length === 1 ) { + if ( !special.teardown || special.teardown.call( elem, namespaces ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + ret = null; + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + var handle = elemData.handle; + if ( handle ) { + handle.elem = null; + } + + delete elemData.events; + delete elemData.handle; + + if ( typeof elemData === "function" ) { + jQuery.removeData( elem, eventKey, true ); + + } else if ( jQuery.isEmptyObject( elemData ) ) { + jQuery.removeData( elem, undefined, true ); + } + } + }, + + // bubbling is internal + trigger: function( event, data, elem /*, bubbling */ ) { + // Event object or event type + var type = event.type || event, + bubbling = arguments[3]; + + if ( !bubbling ) { + event = typeof event === "object" ? + // jQuery.Event object + event[ jQuery.expando ] ? event : + // Object literal + jQuery.extend( jQuery.Event(type), event ) : + // Just the event type (string) + jQuery.Event(type); + + if ( type.indexOf("!") >= 0 ) { + event.type = type = type.slice(0, -1); + event.exclusive = true; + } + + // Handle a global trigger + if ( !elem ) { + // Don't bubble custom events when global (to avoid too much overhead) + event.stopPropagation(); + + // Only trigger if we've ever bound an event for it + if ( jQuery.event.global[ type ] ) { + // XXX This code smells terrible. event.js should not be directly + // inspecting the data cache + jQuery.each( jQuery.cache, function() { + // internalKey variable is just used to make it easier to find + // and potentially change this stuff later; currently it just + // points to jQuery.expando + var internalKey = jQuery.expando, + internalCache = this[ internalKey ]; + if ( internalCache && internalCache.events && internalCache.events[type] ) { + jQuery.event.trigger( event, data, internalCache.handle.elem ); + } + }); + } + } + + // Handle triggering a single element + + // don't do events on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 ) { + return undefined; + } + + // Clean up in case it is reused + event.result = undefined; + event.target = elem; + + // Clone the incoming data, if any + data = jQuery.makeArray( data ); + data.unshift( event ); + } + + event.currentTarget = elem; + + // Trigger the event, it is assumed that "handle" is a function + var handle = elem.nodeType ? + jQuery._data( elem, "handle" ) : + (jQuery._data( elem, eventKey ) || {}).handle; + + if ( handle ) { + handle.apply( elem, data ); + } + + var parent = elem.parentNode || elem.ownerDocument; + + // Trigger an inline bound script + try { + if ( !(elem && elem.nodeName && jQuery.noData[elem.nodeName.toLowerCase()]) ) { + if ( elem[ "on" + type ] && elem[ "on" + type ].apply( elem, data ) === false ) { + event.result = false; + event.preventDefault(); + } + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (inlineError) {} + + if ( !event.isPropagationStopped() && parent ) { + jQuery.event.trigger( event, data, parent, true ); + + } else if ( !event.isDefaultPrevented() ) { + var old, + target = event.target, + targetType = type.replace( rnamespaces, "" ), + isClick = jQuery.nodeName( target, "a" ) && targetType === "click", + special = jQuery.event.special[ targetType ] || {}; + + if ( (!special._default || special._default.call( elem, event ) === false) && + !isClick && !(target && target.nodeName && jQuery.noData[target.nodeName.toLowerCase()]) ) { + + try { + if ( target[ targetType ] ) { + // Make sure that we don't accidentally re-trigger the onFOO events + old = target[ "on" + targetType ]; + + if ( old ) { + target[ "on" + targetType ] = null; + } + + jQuery.event.triggered = true; + target[ targetType ](); + } + + // prevent IE from throwing an error for some elements with some event types, see #3533 + } catch (triggerError) {} + + if ( old ) { + target[ "on" + targetType ] = old; + } + + jQuery.event.triggered = false; + } + } + }, + + handle: function( event ) { + var all, handlers, namespaces, namespace_re, events, + namespace_sort = [], + args = jQuery.makeArray( arguments ); + + event = args[0] = jQuery.event.fix( event || window.event ); + event.currentTarget = this; + + // Namespaced event handlers + all = event.type.indexOf(".") < 0 && !event.exclusive; + + if ( !all ) { + namespaces = event.type.split("."); + event.type = namespaces.shift(); + namespace_sort = namespaces.slice(0).sort(); + namespace_re = new RegExp("(^|\\.)" + namespace_sort.join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.namespace = event.namespace || namespace_sort.join("."); + + events = jQuery._data(this, eventKey); + + if ( typeof events === "function" ) { + events = events.events; + } + + handlers = (events || {})[ event.type ]; + + if ( events && handlers ) { + // Clone the handlers to prevent manipulation + handlers = handlers.slice(0); + + for ( var j = 0, l = handlers.length; j < l; j++ ) { + var handleObj = handlers[ j ]; + + // Filter the functions by class + if ( all || namespace_re.test( handleObj.namespace ) ) { + // Pass in a reference to the handler function itself + // So that we can later remove it + event.handler = handleObj.handler; + event.data = handleObj.data; + event.handleObj = handleObj; + + var ret = handleObj.handler.apply( this, args ); + + if ( ret !== undefined ) { + event.result = ret; + if ( ret === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + } + + return event.result; + }, + + props: "altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which".split(" "), + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // store a copy of the original event object + // and "clone" to set read-only properties + var originalEvent = event; + event = jQuery.Event( originalEvent ); + + for ( var i = this.props.length, prop; i; ) { + prop = this.props[ --i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Fix target property, if necessary + if ( !event.target ) { + // Fixes #1925 where srcElement might not be defined either + event.target = event.srcElement || document; + } + + // check if target is a textnode (safari) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Add relatedTarget, if necessary + if ( !event.relatedTarget && event.fromElement ) { + event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; + } + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && event.clientX != null ) { + var doc = document.documentElement, + body = document.body; + + event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); + event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); + } + + // Add which for key events + if ( event.which == null && (event.charCode != null || event.keyCode != null) ) { + event.which = event.charCode != null ? event.charCode : event.keyCode; + } + + // Add metaKey to non-Mac browsers (use ctrl for PC's and Meta for Macs) + if ( !event.metaKey && event.ctrlKey ) { + event.metaKey = event.ctrlKey; + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && event.button !== undefined ) { + event.which = (event.button & 1 ? 1 : ( event.button & 2 ? 3 : ( event.button & 4 ? 2 : 0 ) )); + } + + return event; + }, + + // Deprecated, use jQuery.guid instead + guid: 1E8, + + // Deprecated, use jQuery.proxy instead + proxy: jQuery.proxy, + + special: { + ready: { + // Make sure the ready event is setup + setup: jQuery.bindReady, + teardown: jQuery.noop + }, + + live: { + add: function( handleObj ) { + jQuery.event.add( this, + liveConvert( handleObj.origType, handleObj.selector ), + jQuery.extend({}, handleObj, {handler: liveHandler, guid: handleObj.handler.guid}) ); + }, + + remove: function( handleObj ) { + jQuery.event.remove( this, liveConvert( handleObj.origType, handleObj.selector ), handleObj ); + } + }, + + beforeunload: { + setup: function( data, namespaces, eventHandle ) { + // We only want to do this special case on windows + if ( jQuery.isWindow( this ) ) { + this.onbeforeunload = eventHandle; + } + }, + + teardown: function( namespaces, eventHandle ) { + if ( this.onbeforeunload === eventHandle ) { + this.onbeforeunload = null; + } + } + } + } +}; + +jQuery.removeEvent = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + if ( elem.detachEvent ) { + elem.detachEvent( "on" + type, handle ); + } + }; + +jQuery.Event = function( src ) { + // Allow instantiation without the 'new' keyword + if ( !this.preventDefault ) { + return new jQuery.Event( src ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = (src.defaultPrevented || src.returnValue === false || + src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse; + + // Event type + } else { + this.type = src; + } + + // timeStamp is buggy for some events on Firefox(#3843) + // So we won't rely on the native value + this.timeStamp = jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +function returnFalse() { + return false; +} +function returnTrue() { + return true; +} + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + preventDefault: function() { + this.isDefaultPrevented = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + + // if preventDefault exists run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // otherwise set the returnValue property of the original event to false (IE) + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + this.isPropagationStopped = returnTrue; + + var e = this.originalEvent; + if ( !e ) { + return; + } + // if stopPropagation exists run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + // otherwise set the cancelBubble property of the original event to true (IE) + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + }, + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse +}; + +// Checks if an event happened on an element within another element +// Used in jQuery.event.special.mouseenter and mouseleave handlers +var withinElement = function( event ) { + // Check if mouse(over|out) are still within the same parent element + var parent = event.relatedTarget; + + // Firefox sometimes assigns relatedTarget a XUL element + // which we cannot access the parentNode property of + try { + // Traverse up the tree + while ( parent && parent !== this ) { + parent = parent.parentNode; + } + + if ( parent !== this ) { + // set the correct event type + event.type = event.data; + + // handle event if we actually just moused on to a non sub-element + jQuery.event.handle.apply( this, arguments ); + } + + // assuming we've left the element since we most likely mousedover a xul element + } catch(e) { } +}, + +// In case of event delegation, we only need to rename the event.type, +// liveHandler will take care of the rest. +delegate = function( event ) { + event.type = event.data; + jQuery.event.handle.apply( this, arguments ); +}; + +// Create mouseenter and mouseleave events +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + setup: function( data ) { + jQuery.event.add( this, fix, data && data.selector ? delegate : withinElement, orig ); + }, + teardown: function( data ) { + jQuery.event.remove( this, fix, data && data.selector ? delegate : withinElement ); + } + }; +}); + +// submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function( data, namespaces ) { + if ( this.nodeName && this.nodeName.toLowerCase() !== "form" ) { + jQuery.event.add(this, "click.specialSubmit", function( e ) { + var elem = e.target, + type = elem.type; + + if ( (type === "submit" || type === "image") && jQuery( elem ).closest("form").length ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + jQuery.event.add(this, "keypress.specialSubmit", function( e ) { + var elem = e.target, + type = elem.type; + + if ( (type === "text" || type === "password") && jQuery( elem ).closest("form").length && e.keyCode === 13 ) { + e.liveFired = undefined; + return trigger( "submit", this, arguments ); + } + }); + + } else { + return false; + } + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialSubmit" ); + } + }; + +} + +// change delegation, happens here so we have bind. +if ( !jQuery.support.changeBubbles ) { + + var changeFilters, + + getVal = function( elem ) { + var type = elem.type, val = elem.value; + + if ( type === "radio" || type === "checkbox" ) { + val = elem.checked; + + } else if ( type === "select-multiple" ) { + val = elem.selectedIndex > -1 ? + jQuery.map( elem.options, function( elem ) { + return elem.selected; + }).join("-") : + ""; + + } else if ( elem.nodeName.toLowerCase() === "select" ) { + val = elem.selectedIndex; + } + + return val; + }, + + testChange = function testChange( e ) { + var elem = e.target, data, val; + + if ( !rformElems.test( elem.nodeName ) || elem.readOnly ) { + return; + } + + data = jQuery._data( elem, "_change_data" ); + val = getVal(elem); + + // the current data will be also retrieved by beforeactivate + if ( e.type !== "focusout" || elem.type !== "radio" ) { + jQuery._data( elem, "_change_data", val ); + } + + if ( data === undefined || val === data ) { + return; + } + + if ( data != null || val ) { + e.type = "change"; + e.liveFired = undefined; + return jQuery.event.trigger( e, arguments[1], elem ); + } + }; + + jQuery.event.special.change = { + filters: { + focusout: testChange, + + beforedeactivate: testChange, + + click: function( e ) { + var elem = e.target, type = elem.type; + + if ( type === "radio" || type === "checkbox" || elem.nodeName.toLowerCase() === "select" ) { + return testChange.call( this, e ); + } + }, + + // Change has to be called before submit + // Keydown will be called before keypress, which is used in submit-event delegation + keydown: function( e ) { + var elem = e.target, type = elem.type; + + if ( (e.keyCode === 13 && elem.nodeName.toLowerCase() !== "textarea") || + (e.keyCode === 32 && (type === "checkbox" || type === "radio")) || + type === "select-multiple" ) { + return testChange.call( this, e ); + } + }, + + // Beforeactivate happens also before the previous element is blurred + // with this event you can't trigger a change event, but you can store + // information + beforeactivate: function( e ) { + var elem = e.target; + jQuery._data( elem, "_change_data", getVal(elem) ); + } + }, + + setup: function( data, namespaces ) { + if ( this.type === "file" ) { + return false; + } + + for ( var type in changeFilters ) { + jQuery.event.add( this, type + ".specialChange", changeFilters[type] ); + } + + return rformElems.test( this.nodeName ); + }, + + teardown: function( namespaces ) { + jQuery.event.remove( this, ".specialChange" ); + + return rformElems.test( this.nodeName ); + } + }; + + changeFilters = jQuery.event.special.change.filters; + + // Handle when the input is .focus()'d + changeFilters.focus = changeFilters.beforeactivate; +} + +function trigger( type, elem, args ) { + args[0].type = type; + return jQuery.event.handle.apply( elem, args ); +} + +// Create "bubbling" focus and blur events +if ( document.addEventListener ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + jQuery.event.special[ fix ] = { + setup: function() { + this.addEventListener( orig, handler, true ); + }, + teardown: function() { + this.removeEventListener( orig, handler, true ); + } + }; + + function handler( e ) { + e = jQuery.event.fix( e ); + e.type = fix; + return jQuery.event.handle.call( this, e ); + } + }); +} + +jQuery.each(["bind", "one"], function( i, name ) { + jQuery.fn[ name ] = function( type, data, fn ) { + // Handle object literals + if ( typeof type === "object" ) { + for ( var key in type ) { + this[ name ](key, data, type[key], fn); + } + return this; + } + + if ( jQuery.isFunction( data ) || data === false ) { + fn = data; + data = undefined; + } + + var handler = name === "one" ? jQuery.proxy( fn, function( event ) { + jQuery( this ).unbind( event, handler ); + return fn.apply( this, arguments ); + }) : fn; + + if ( type === "unload" && name !== "one" ) { + this.one( type, data, fn ); + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.add( this[i], type, handler, data ); + } + } + + return this; + }; +}); + +jQuery.fn.extend({ + unbind: function( type, fn ) { + // Handle object literals + if ( typeof type === "object" && !type.preventDefault ) { + for ( var key in type ) { + this.unbind(key, type[key]); + } + + } else { + for ( var i = 0, l = this.length; i < l; i++ ) { + jQuery.event.remove( this[i], type, fn ); + } + } + + return this; + }, + + delegate: function( selector, types, data, fn ) { + return this.live( types, data, fn, selector ); + }, + + undelegate: function( selector, types, fn ) { + if ( arguments.length === 0 ) { + return this.unbind( "live" ); + + } else { + return this.die( types, null, fn, selector ); + } + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + + triggerHandler: function( type, data ) { + if ( this[0] ) { + var event = jQuery.Event( type ); + event.preventDefault(); + event.stopPropagation(); + jQuery.event.trigger( event, data, this[0] ); + return event.result; + } + }, + + toggle: function( fn ) { + // Save reference to arguments for access in closure + var args = arguments, + i = 1; + + // link all the functions, so any of them can unbind this click handler + while ( i < args.length ) { + jQuery.proxy( fn, args[ i++ ] ); + } + + return this.click( jQuery.proxy( fn, function( event ) { + // Figure out which function to execute + var lastToggle = ( jQuery._data( this, "lastToggle" + fn.guid ) || 0 ) % i; + jQuery._data( this, "lastToggle" + fn.guid, lastToggle + 1 ); + + // Make sure that clicks stop + event.preventDefault(); + + // and execute the function + return args[ lastToggle ].apply( this, arguments ) || false; + })); + }, + + hover: function( fnOver, fnOut ) { + return this.mouseenter( fnOver ).mouseleave( fnOut || fnOver ); + } +}); + +var liveMap = { + focus: "focusin", + blur: "focusout", + mouseenter: "mouseover", + mouseleave: "mouseout" +}; + +jQuery.each(["live", "die"], function( i, name ) { + jQuery.fn[ name ] = function( types, data, fn, origSelector /* Internal Use Only */ ) { + var type, i = 0, match, namespaces, preType, + selector = origSelector || this.selector, + context = origSelector ? this : jQuery( this.context ); + + if ( typeof types === "object" && !types.preventDefault ) { + for ( var key in types ) { + context[ name ]( key, data, types[key], selector ); + } + + return this; + } + + if ( jQuery.isFunction( data ) ) { + fn = data; + data = undefined; + } + + types = (types || "").split(" "); + + while ( (type = types[ i++ ]) != null ) { + match = rnamespaces.exec( type ); + namespaces = ""; + + if ( match ) { + namespaces = match[0]; + type = type.replace( rnamespaces, "" ); + } + + if ( type === "hover" ) { + types.push( "mouseenter" + namespaces, "mouseleave" + namespaces ); + continue; + } + + preType = type; + + if ( type === "focus" || type === "blur" ) { + types.push( liveMap[ type ] + namespaces ); + type = type + namespaces; + + } else { + type = (liveMap[ type ] || type) + namespaces; + } + + if ( name === "live" ) { + // bind live handler + for ( var j = 0, l = context.length; j < l; j++ ) { + jQuery.event.add( context[j], "live." + liveConvert( type, selector ), + { data: data, selector: selector, handler: fn, origType: type, origHandler: fn, preType: preType } ); + } + + } else { + // unbind live handler + context.unbind( "live." + liveConvert( type, selector ), fn ); + } + } + + return this; + }; +}); + +function liveHandler( event ) { + var stop, maxLevel, related, match, handleObj, elem, j, i, l, data, close, namespace, ret, + elems = [], + selectors = [], + events = jQuery._data( this, eventKey ); + + if ( typeof events === "function" ) { + events = events.events; + } + + // Make sure we avoid non-left-click bubbling in Firefox (#3861) and disabled elements in IE (#6911) + if ( event.liveFired === this || !events || !events.live || event.target.disabled || event.button && event.type === "click" ) { + return; + } + + if ( event.namespace ) { + namespace = new RegExp("(^|\\.)" + event.namespace.split(".").join("\\.(?:.*\\.)?") + "(\\.|$)"); + } + + event.liveFired = this; + + var live = events.live.slice(0); + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( handleObj.origType.replace( rnamespaces, "" ) === event.type ) { + selectors.push( handleObj.selector ); + + } else { + live.splice( j--, 1 ); + } + } + + match = jQuery( event.target ).closest( selectors, event.currentTarget ); + + for ( i = 0, l = match.length; i < l; i++ ) { + close = match[i]; + + for ( j = 0; j < live.length; j++ ) { + handleObj = live[j]; + + if ( close.selector === handleObj.selector && (!namespace || namespace.test( handleObj.namespace )) ) { + elem = close.elem; + related = null; + + // Those two events require additional checking + if ( handleObj.preType === "mouseenter" || handleObj.preType === "mouseleave" ) { + event.type = handleObj.preType; + related = jQuery( event.relatedTarget ).closest( handleObj.selector )[0]; + } + + if ( !related || related !== elem ) { + elems.push({ elem: elem, handleObj: handleObj, level: close.level }); + } + } + } + } + + for ( i = 0, l = elems.length; i < l; i++ ) { + match = elems[i]; + + if ( maxLevel && match.level > maxLevel ) { + break; + } + + event.currentTarget = match.elem; + event.data = match.handleObj.data; + event.handleObj = match.handleObj; + + ret = match.handleObj.origHandler.apply( match.elem, arguments ); + + if ( ret === false || event.isPropagationStopped() ) { + maxLevel = match.level; + + if ( ret === false ) { + stop = false; + } + if ( event.isImmediatePropagationStopped() ) { + break; + } + } + } + + return stop; +} + +function liveConvert( type, selector ) { + return (type && type !== "*" ? type + "." : "") + selector.replace(rperiod, "`").replace(rspace, "&"); +} + +jQuery.each( ("blur focus focusin focusout load resize scroll unload click dblclick " + + "mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave " + + "change select submit keydown keypress keyup error").split(" "), function( i, name ) { + + // Handle event binding + jQuery.fn[ name ] = function( data, fn ) { + if ( fn == null ) { + fn = data; + data = null; + } + + return arguments.length > 0 ? + this.bind( name, data, fn ) : + this.trigger( name ); + }; + + if ( jQuery.attrFn ) { + jQuery.attrFn[ name ] = true; + } +}); + + +/*! + * Sizzle CSS Selector Engine + * Copyright 2011, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * More information: http://sizzlejs.com/ + */ +(function(){ + +var chunker = /((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, + done = 0, + toString = Object.prototype.toString, + hasDuplicate = false, + baseHasDuplicate = true; + +// Here we check if the JavaScript engine is using some sort of +// optimization where it does not always call our comparision +// function. If that is the case, discard the hasDuplicate value. +// Thus far that includes Google Chrome. +[0, 0].sort(function() { + baseHasDuplicate = false; + return 0; +}); + +var Sizzle = function( selector, context, results, seed ) { + results = results || []; + context = context || document; + + var origContext = context; + + if ( context.nodeType !== 1 && context.nodeType !== 9 ) { + return []; + } + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + var m, set, checkSet, extra, ret, cur, pop, i, + prune = true, + contextXML = Sizzle.isXML( context ), + parts = [], + soFar = selector; + + // Reset the position of the chunker regexp (start from head) + do { + chunker.exec( "" ); + m = chunker.exec( soFar ); + + if ( m ) { + soFar = m[3]; + + parts.push( m[1] ); + + if ( m[2] ) { + extra = m[3]; + break; + } + } + } while ( m ); + + if ( parts.length > 1 && origPOS.exec( selector ) ) { + + if ( parts.length === 2 && Expr.relative[ parts[0] ] ) { + set = posProcess( parts[0] + parts[1], context ); + + } else { + set = Expr.relative[ parts[0] ] ? + [ context ] : + Sizzle( parts.shift(), context ); + + while ( parts.length ) { + selector = parts.shift(); + + if ( Expr.relative[ selector ] ) { + selector += parts.shift(); + } + + set = posProcess( selector, set ); + } + } + + } else { + // Take a shortcut and set the context if the root selector is an ID + // (but not if it'll be faster if the inner selector is an ID) + if ( !seed && parts.length > 1 && context.nodeType === 9 && !contextXML && + Expr.match.ID.test(parts[0]) && !Expr.match.ID.test(parts[parts.length - 1]) ) { + + ret = Sizzle.find( parts.shift(), context, contextXML ); + context = ret.expr ? + Sizzle.filter( ret.expr, ret.set )[0] : + ret.set[0]; + } + + if ( context ) { + ret = seed ? + { expr: parts.pop(), set: makeArray(seed) } : + Sizzle.find( parts.pop(), parts.length === 1 && (parts[0] === "~" || parts[0] === "+") && context.parentNode ? context.parentNode : context, contextXML ); + + set = ret.expr ? + Sizzle.filter( ret.expr, ret.set ) : + ret.set; + + if ( parts.length > 0 ) { + checkSet = makeArray( set ); + + } else { + prune = false; + } + + while ( parts.length ) { + cur = parts.pop(); + pop = cur; + + if ( !Expr.relative[ cur ] ) { + cur = ""; + } else { + pop = parts.pop(); + } + + if ( pop == null ) { + pop = context; + } + + Expr.relative[ cur ]( checkSet, pop, contextXML ); + } + + } else { + checkSet = parts = []; + } + } + + if ( !checkSet ) { + checkSet = set; + } + + if ( !checkSet ) { + Sizzle.error( cur || selector ); + } + + if ( toString.call(checkSet) === "[object Array]" ) { + if ( !prune ) { + results.push.apply( results, checkSet ); + + } else if ( context && context.nodeType === 1 ) { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && (checkSet[i] === true || checkSet[i].nodeType === 1 && Sizzle.contains(context, checkSet[i])) ) { + results.push( set[i] ); + } + } + + } else { + for ( i = 0; checkSet[i] != null; i++ ) { + if ( checkSet[i] && checkSet[i].nodeType === 1 ) { + results.push( set[i] ); + } + } + } + + } else { + makeArray( checkSet, results ); + } + + if ( extra ) { + Sizzle( extra, origContext, results, seed ); + Sizzle.uniqueSort( results ); + } + + return results; +}; + +Sizzle.uniqueSort = function( results ) { + if ( sortOrder ) { + hasDuplicate = baseHasDuplicate; + results.sort( sortOrder ); + + if ( hasDuplicate ) { + for ( var i = 1; i < results.length; i++ ) { + if ( results[i] === results[ i - 1 ] ) { + results.splice( i--, 1 ); + } + } + } + } + + return results; +}; + +Sizzle.matches = function( expr, set ) { + return Sizzle( expr, null, null, set ); +}; + +Sizzle.matchesSelector = function( node, expr ) { + return Sizzle( expr, null, null, [node] ).length > 0; +}; + +Sizzle.find = function( expr, context, isXML ) { + var set; + + if ( !expr ) { + return []; + } + + for ( var i = 0, l = Expr.order.length; i < l; i++ ) { + var match, + type = Expr.order[i]; + + if ( (match = Expr.leftMatch[ type ].exec( expr )) ) { + var left = match[1]; + match.splice( 1, 1 ); + + if ( left.substr( left.length - 1 ) !== "\\" ) { + match[1] = (match[1] || "").replace(/\\/g, ""); + set = Expr.find[ type ]( match, context, isXML ); + + if ( set != null ) { + expr = expr.replace( Expr.match[ type ], "" ); + break; + } + } + } + } + + if ( !set ) { + set = typeof context.getElementsByTagName !== "undefined" ? + context.getElementsByTagName( "*" ) : + []; + } + + return { set: set, expr: expr }; +}; + +Sizzle.filter = function( expr, set, inplace, not ) { + var match, anyFound, + old = expr, + result = [], + curLoop = set, + isXMLFilter = set && set[0] && Sizzle.isXML( set[0] ); + + while ( expr && set.length ) { + for ( var type in Expr.filter ) { + if ( (match = Expr.leftMatch[ type ].exec( expr )) != null && match[2] ) { + var found, item, + filter = Expr.filter[ type ], + left = match[1]; + + anyFound = false; + + match.splice(1,1); + + if ( left.substr( left.length - 1 ) === "\\" ) { + continue; + } + + if ( curLoop === result ) { + result = []; + } + + if ( Expr.preFilter[ type ] ) { + match = Expr.preFilter[ type ]( match, curLoop, inplace, result, not, isXMLFilter ); + + if ( !match ) { + anyFound = found = true; + + } else if ( match === true ) { + continue; + } + } + + if ( match ) { + for ( var i = 0; (item = curLoop[i]) != null; i++ ) { + if ( item ) { + found = filter( item, match, i, curLoop ); + var pass = not ^ !!found; + + if ( inplace && found != null ) { + if ( pass ) { + anyFound = true; + + } else { + curLoop[i] = false; + } + + } else if ( pass ) { + result.push( item ); + anyFound = true; + } + } + } + } + + if ( found !== undefined ) { + if ( !inplace ) { + curLoop = result; + } + + expr = expr.replace( Expr.match[ type ], "" ); + + if ( !anyFound ) { + return []; + } + + break; + } + } + } + + // Improper expression + if ( expr === old ) { + if ( anyFound == null ) { + Sizzle.error( expr ); + + } else { + break; + } + } + + old = expr; + } + + return curLoop; +}; + +Sizzle.error = function( msg ) { + throw "Syntax error, unrecognized expression: " + msg; +}; + +var Expr = Sizzle.selectors = { + order: [ "ID", "NAME", "TAG" ], + + match: { + ID: /#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + CLASS: /\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/, + NAME: /\[name=['"]*((?:[\w\u00c0-\uFFFF\-]|\\.)+)['"]*\]/, + ATTR: /\[\s*((?:[\w\u00c0-\uFFFF\-]|\\.)+)\s*(?:(\S?=)\s*(?:(['"])(.*?)\3|(#?(?:[\w\u00c0-\uFFFF\-]|\\.)*)|)|)\s*\]/, + TAG: /^((?:[\w\u00c0-\uFFFF\*\-]|\\.)+)/, + CHILD: /:(only|nth|last|first)-child(?:\(\s*(even|odd|(?:[+\-]?\d+|(?:[+\-]?\d*)?n\s*(?:[+\-]\s*\d+)?))\s*\))?/, + POS: /:(nth|eq|gt|lt|first|last|even|odd)(?:\((\d*)\))?(?=[^\-]|$)/, + PSEUDO: /:((?:[\w\u00c0-\uFFFF\-]|\\.)+)(?:\((['"]?)((?:\([^\)]+\)|[^\(\)]*)+)\2\))?/ + }, + + leftMatch: {}, + + attrMap: { + "class": "className", + "for": "htmlFor" + }, + + attrHandle: { + href: function( elem ) { + return elem.getAttribute( "href" ); + } + }, + + relative: { + "+": function(checkSet, part){ + var isPartStr = typeof part === "string", + isTag = isPartStr && !/\W/.test( part ), + isPartStrNotTag = isPartStr && !isTag; + + if ( isTag ) { + part = part.toLowerCase(); + } + + for ( var i = 0, l = checkSet.length, elem; i < l; i++ ) { + if ( (elem = checkSet[i]) ) { + while ( (elem = elem.previousSibling) && elem.nodeType !== 1 ) {} + + checkSet[i] = isPartStrNotTag || elem && elem.nodeName.toLowerCase() === part ? + elem || false : + elem === part; + } + } + + if ( isPartStrNotTag ) { + Sizzle.filter( part, checkSet, true ); + } + }, + + ">": function( checkSet, part ) { + var elem, + isPartStr = typeof part === "string", + i = 0, + l = checkSet.length; + + if ( isPartStr && !/\W/.test( part ) ) { + part = part.toLowerCase(); + + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + var parent = elem.parentNode; + checkSet[i] = parent.nodeName.toLowerCase() === part ? parent : false; + } + } + + } else { + for ( ; i < l; i++ ) { + elem = checkSet[i]; + + if ( elem ) { + checkSet[i] = isPartStr ? + elem.parentNode : + elem.parentNode === part; + } + } + + if ( isPartStr ) { + Sizzle.filter( part, checkSet, true ); + } + } + }, + + "": function(checkSet, part, isXML){ + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test(part) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "parentNode", part, doneName, checkSet, nodeCheck, isXML ); + }, + + "~": function( checkSet, part, isXML ) { + var nodeCheck, + doneName = done++, + checkFn = dirCheck; + + if ( typeof part === "string" && !/\W/.test( part ) ) { + part = part.toLowerCase(); + nodeCheck = part; + checkFn = dirNodeCheck; + } + + checkFn( "previousSibling", part, doneName, checkSet, nodeCheck, isXML ); + } + }, + + find: { + ID: function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }, + + NAME: function( match, context ) { + if ( typeof context.getElementsByName !== "undefined" ) { + var ret = [], + results = context.getElementsByName( match[1] ); + + for ( var i = 0, l = results.length; i < l; i++ ) { + if ( results[i].getAttribute("name") === match[1] ) { + ret.push( results[i] ); + } + } + + return ret.length === 0 ? null : ret; + } + }, + + TAG: function( match, context ) { + if ( typeof context.getElementsByTagName !== "undefined" ) { + return context.getElementsByTagName( match[1] ); + } + } + }, + preFilter: { + CLASS: function( match, curLoop, inplace, result, not, isXML ) { + match = " " + match[1].replace(/\\/g, "") + " "; + + if ( isXML ) { + return match; + } + + for ( var i = 0, elem; (elem = curLoop[i]) != null; i++ ) { + if ( elem ) { + if ( not ^ (elem.className && (" " + elem.className + " ").replace(/[\t\n\r]/g, " ").indexOf(match) >= 0) ) { + if ( !inplace ) { + result.push( elem ); + } + + } else if ( inplace ) { + curLoop[i] = false; + } + } + } + + return false; + }, + + ID: function( match ) { + return match[1].replace(/\\/g, ""); + }, + + TAG: function( match, curLoop ) { + return match[1].toLowerCase(); + }, + + CHILD: function( match ) { + if ( match[1] === "nth" ) { + if ( !match[2] ) { + Sizzle.error( match[0] ); + } + + match[2] = match[2].replace(/^\+|\s*/g, ''); + + // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6' + var test = /(-?)(\d*)(?:n([+\-]?\d*))?/.exec( + match[2] === "even" && "2n" || match[2] === "odd" && "2n+1" || + !/\D/.test( match[2] ) && "0n+" + match[2] || match[2]); + + // calculate the numbers (first)n+(last) including if they are negative + match[2] = (test[1] + (test[2] || 1)) - 0; + match[3] = test[3] - 0; + } + else if ( match[2] ) { + Sizzle.error( match[0] ); + } + + // TODO: Move to normal caching system + match[0] = done++; + + return match; + }, + + ATTR: function( match, curLoop, inplace, result, not, isXML ) { + var name = match[1] = match[1].replace(/\\/g, ""); + + if ( !isXML && Expr.attrMap[name] ) { + match[1] = Expr.attrMap[name]; + } + + // Handle if an un-quoted value was used + match[4] = ( match[4] || match[5] || "" ).replace(/\\/g, ""); + + if ( match[2] === "~=" ) { + match[4] = " " + match[4] + " "; + } + + return match; + }, + + PSEUDO: function( match, curLoop, inplace, result, not ) { + if ( match[1] === "not" ) { + // If we're dealing with a complex expression, or a simple one + if ( ( chunker.exec(match[3]) || "" ).length > 1 || /^\w/.test(match[3]) ) { + match[3] = Sizzle(match[3], null, null, curLoop); + + } else { + var ret = Sizzle.filter(match[3], curLoop, inplace, true ^ not); + + if ( !inplace ) { + result.push.apply( result, ret ); + } + + return false; + } + + } else if ( Expr.match.POS.test( match[0] ) || Expr.match.CHILD.test( match[0] ) ) { + return true; + } + + return match; + }, + + POS: function( match ) { + match.unshift( true ); + + return match; + } + }, + + filters: { + enabled: function( elem ) { + return elem.disabled === false && elem.type !== "hidden"; + }, + + disabled: function( elem ) { + return elem.disabled === true; + }, + + checked: function( elem ) { + return elem.checked === true; + }, + + selected: function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + elem.parentNode.selectedIndex; + + return elem.selected === true; + }, + + parent: function( elem ) { + return !!elem.firstChild; + }, + + empty: function( elem ) { + return !elem.firstChild; + }, + + has: function( elem, i, match ) { + return !!Sizzle( match[3], elem ).length; + }, + + header: function( elem ) { + return (/h\d/i).test( elem.nodeName ); + }, + + text: function( elem ) { + return "text" === elem.type; + }, + radio: function( elem ) { + return "radio" === elem.type; + }, + + checkbox: function( elem ) { + return "checkbox" === elem.type; + }, + + file: function( elem ) { + return "file" === elem.type; + }, + password: function( elem ) { + return "password" === elem.type; + }, + + submit: function( elem ) { + return "submit" === elem.type; + }, + + image: function( elem ) { + return "image" === elem.type; + }, + + reset: function( elem ) { + return "reset" === elem.type; + }, + + button: function( elem ) { + return "button" === elem.type || elem.nodeName.toLowerCase() === "button"; + }, + + input: function( elem ) { + return (/input|select|textarea|button/i).test( elem.nodeName ); + } + }, + setFilters: { + first: function( elem, i ) { + return i === 0; + }, + + last: function( elem, i, match, array ) { + return i === array.length - 1; + }, + + even: function( elem, i ) { + return i % 2 === 0; + }, + + odd: function( elem, i ) { + return i % 2 === 1; + }, + + lt: function( elem, i, match ) { + return i < match[3] - 0; + }, + + gt: function( elem, i, match ) { + return i > match[3] - 0; + }, + + nth: function( elem, i, match ) { + return match[3] - 0 === i; + }, + + eq: function( elem, i, match ) { + return match[3] - 0 === i; + } + }, + filter: { + PSEUDO: function( elem, match, i, array ) { + var name = match[1], + filter = Expr.filters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + + } else if ( name === "contains" ) { + return (elem.textContent || elem.innerText || Sizzle.getText([ elem ]) || "").indexOf(match[3]) >= 0; + + } else if ( name === "not" ) { + var not = match[3]; + + for ( var j = 0, l = not.length; j < l; j++ ) { + if ( not[j] === elem ) { + return false; + } + } + + return true; + + } else { + Sizzle.error( name ); + } + }, + + CHILD: function( elem, match ) { + var type = match[1], + node = elem; + + switch ( type ) { + case "only": + case "first": + while ( (node = node.previousSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + if ( type === "first" ) { + return true; + } + + node = elem; + + case "last": + while ( (node = node.nextSibling) ) { + if ( node.nodeType === 1 ) { + return false; + } + } + + return true; + + case "nth": + var first = match[2], + last = match[3]; + + if ( first === 1 && last === 0 ) { + return true; + } + + var doneName = match[0], + parent = elem.parentNode; + + if ( parent && (parent.sizcache !== doneName || !elem.nodeIndex) ) { + var count = 0; + + for ( node = parent.firstChild; node; node = node.nextSibling ) { + if ( node.nodeType === 1 ) { + node.nodeIndex = ++count; + } + } + + parent.sizcache = doneName; + } + + var diff = elem.nodeIndex - last; + + if ( first === 0 ) { + return diff === 0; + + } else { + return ( diff % first === 0 && diff / first >= 0 ); + } + } + }, + + ID: function( elem, match ) { + return elem.nodeType === 1 && elem.getAttribute("id") === match; + }, + + TAG: function( elem, match ) { + return (match === "*" && elem.nodeType === 1) || elem.nodeName.toLowerCase() === match; + }, + + CLASS: function( elem, match ) { + return (" " + (elem.className || elem.getAttribute("class")) + " ") + .indexOf( match ) > -1; + }, + + ATTR: function( elem, match ) { + var name = match[1], + result = Expr.attrHandle[ name ] ? + Expr.attrHandle[ name ]( elem ) : + elem[ name ] != null ? + elem[ name ] : + elem.getAttribute( name ), + value = result + "", + type = match[2], + check = match[4]; + + return result == null ? + type === "!=" : + type === "=" ? + value === check : + type === "*=" ? + value.indexOf(check) >= 0 : + type === "~=" ? + (" " + value + " ").indexOf(check) >= 0 : + !check ? + value && result !== false : + type === "!=" ? + value !== check : + type === "^=" ? + value.indexOf(check) === 0 : + type === "$=" ? + value.substr(value.length - check.length) === check : + type === "|=" ? + value === check || value.substr(0, check.length + 1) === check + "-" : + false; + }, + + POS: function( elem, match, i, array ) { + var name = match[2], + filter = Expr.setFilters[ name ]; + + if ( filter ) { + return filter( elem, i, match, array ); + } + } + } +}; + +var origPOS = Expr.match.POS, + fescape = function(all, num){ + return "\\" + (num - 0 + 1); + }; + +for ( var type in Expr.match ) { + Expr.match[ type ] = new RegExp( Expr.match[ type ].source + (/(?![^\[]*\])(?![^\(]*\))/.source) ); + Expr.leftMatch[ type ] = new RegExp( /(^(?:.|\r|\n)*?)/.source + Expr.match[ type ].source.replace(/\\(\d+)/g, fescape) ); +} + +var makeArray = function( array, results ) { + array = Array.prototype.slice.call( array, 0 ); + + if ( results ) { + results.push.apply( results, array ); + return results; + } + + return array; +}; + +// Perform a simple check to determine if the browser is capable of +// converting a NodeList to an array using builtin methods. +// Also verifies that the returned array holds DOM nodes +// (which is not the case in the Blackberry browser) +try { + Array.prototype.slice.call( document.documentElement.childNodes, 0 )[0].nodeType; + +// Provide a fallback method if it does not work +} catch( e ) { + makeArray = function( array, results ) { + var i = 0, + ret = results || []; + + if ( toString.call(array) === "[object Array]" ) { + Array.prototype.push.apply( ret, array ); + + } else { + if ( typeof array.length === "number" ) { + for ( var l = array.length; i < l; i++ ) { + ret.push( array[i] ); + } + + } else { + for ( ; array[i]; i++ ) { + ret.push( array[i] ); + } + } + } + + return ret; + }; +} + +var sortOrder, siblingCheck; + +if ( document.documentElement.compareDocumentPosition ) { + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + if ( !a.compareDocumentPosition || !b.compareDocumentPosition ) { + return a.compareDocumentPosition ? -1 : 1; + } + + return a.compareDocumentPosition(b) & 4 ? -1 : 1; + }; + +} else { + sortOrder = function( a, b ) { + var al, bl, + ap = [], + bp = [], + aup = a.parentNode, + bup = b.parentNode, + cur = aup; + + // The nodes are identical, we can exit early + if ( a === b ) { + hasDuplicate = true; + return 0; + + // If the nodes are siblings (or identical) we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + + // If no parents were found then the nodes are disconnected + } else if ( !aup ) { + return -1; + + } else if ( !bup ) { + return 1; + } + + // Otherwise they're somewhere else in the tree so we need + // to build up a full list of the parentNodes for comparison + while ( cur ) { + ap.unshift( cur ); + cur = cur.parentNode; + } + + cur = bup; + + while ( cur ) { + bp.unshift( cur ); + cur = cur.parentNode; + } + + al = ap.length; + bl = bp.length; + + // Start walking down the tree looking for a discrepancy + for ( var i = 0; i < al && i < bl; i++ ) { + if ( ap[i] !== bp[i] ) { + return siblingCheck( ap[i], bp[i] ); + } + } + + // We ended someplace up the tree so do a sibling check + return i === al ? + siblingCheck( a, bp[i], -1 ) : + siblingCheck( ap[i], b, 1 ); + }; + + siblingCheck = function( a, b, ret ) { + if ( a === b ) { + return ret; + } + + var cur = a.nextSibling; + + while ( cur ) { + if ( cur === b ) { + return -1; + } + + cur = cur.nextSibling; + } + + return 1; + }; +} + +// Utility function for retreiving the text value of an array of DOM nodes +Sizzle.getText = function( elems ) { + var ret = "", elem; + + for ( var i = 0; elems[i]; i++ ) { + elem = elems[i]; + + // Get the text from text nodes and CDATA nodes + if ( elem.nodeType === 3 || elem.nodeType === 4 ) { + ret += elem.nodeValue; + + // Traverse everything else, except comment nodes + } else if ( elem.nodeType !== 8 ) { + ret += Sizzle.getText( elem.childNodes ); + } + } + + return ret; +}; + +// Check to see if the browser returns elements by name when +// querying by getElementById (and provide a workaround) +(function(){ + // We're going to inject a fake input element with a specified name + var form = document.createElement("div"), + id = "script" + (new Date()).getTime(), + root = document.documentElement; + + form.innerHTML = ""; + + // Inject it into the root element, check its status, and remove it quickly + root.insertBefore( form, root.firstChild ); + + // The workaround has to do additional checks after a getElementById + // Which slows things down for other browsers (hence the branching) + if ( document.getElementById( id ) ) { + Expr.find.ID = function( match, context, isXML ) { + if ( typeof context.getElementById !== "undefined" && !isXML ) { + var m = context.getElementById(match[1]); + + return m ? + m.id === match[1] || typeof m.getAttributeNode !== "undefined" && m.getAttributeNode("id").nodeValue === match[1] ? + [m] : + undefined : + []; + } + }; + + Expr.filter.ID = function( elem, match ) { + var node = typeof elem.getAttributeNode !== "undefined" && elem.getAttributeNode("id"); + + return elem.nodeType === 1 && node && node.nodeValue === match; + }; + } + + root.removeChild( form ); + + // release memory in IE + root = form = null; +})(); + +(function(){ + // Check to see if the browser returns only elements + // when doing getElementsByTagName("*") + + // Create a fake element + var div = document.createElement("div"); + div.appendChild( document.createComment("") ); + + // Make sure no comments are found + if ( div.getElementsByTagName("*").length > 0 ) { + Expr.find.TAG = function( match, context ) { + var results = context.getElementsByTagName( match[1] ); + + // Filter out possible comments + if ( match[1] === "*" ) { + var tmp = []; + + for ( var i = 0; results[i]; i++ ) { + if ( results[i].nodeType === 1 ) { + tmp.push( results[i] ); + } + } + + results = tmp; + } + + return results; + }; + } + + // Check to see if an attribute returns normalized href attributes + div.innerHTML = ""; + + if ( div.firstChild && typeof div.firstChild.getAttribute !== "undefined" && + div.firstChild.getAttribute("href") !== "#" ) { + + Expr.attrHandle.href = function( elem ) { + return elem.getAttribute( "href", 2 ); + }; + } + + // release memory in IE + div = null; +})(); + +if ( document.querySelectorAll ) { + (function(){ + var oldSizzle = Sizzle, + div = document.createElement("div"), + id = "__sizzle__"; + + div.innerHTML = "

"; + + // Safari can't handle uppercase or unicode characters when + // in quirks mode. + if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) { + return; + } + + Sizzle = function( query, context, extra, seed ) { + context = context || document; + + // Only use querySelectorAll on non-XML documents + // (ID selectors don't work in non-HTML documents) + if ( !seed && !Sizzle.isXML(context) ) { + // See if we find a selector to speed up + var match = /^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec( query ); + + if ( match && (context.nodeType === 1 || context.nodeType === 9) ) { + // Speed-up: Sizzle("TAG") + if ( match[1] ) { + return makeArray( context.getElementsByTagName( query ), extra ); + + // Speed-up: Sizzle(".CLASS") + } else if ( match[2] && Expr.find.CLASS && context.getElementsByClassName ) { + return makeArray( context.getElementsByClassName( match[2] ), extra ); + } + } + + if ( context.nodeType === 9 ) { + // Speed-up: Sizzle("body") + // The body element only exists once, optimize finding it + if ( query === "body" && context.body ) { + return makeArray( [ context.body ], extra ); + + // Speed-up: Sizzle("#ID") + } else if ( match && match[3] ) { + var elem = context.getElementById( match[3] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id === match[3] ) { + return makeArray( [ elem ], extra ); + } + + } else { + return makeArray( [], extra ); + } + } + + try { + return makeArray( context.querySelectorAll(query), extra ); + } catch(qsaError) {} + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + } else if ( context.nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + var old = context.getAttribute( "id" ), + nid = old || id, + hasParent = context.parentNode, + relativeHierarchySelector = /^\s*[+~]/.test( query ); + + if ( !old ) { + context.setAttribute( "id", nid ); + } else { + nid = nid.replace( /'/g, "\\$&" ); + } + if ( relativeHierarchySelector && hasParent ) { + context = context.parentNode; + } + + try { + if ( !relativeHierarchySelector || hasParent ) { + return makeArray( context.querySelectorAll( "[id='" + nid + "'] " + query ), extra ); + } + + } catch(pseudoError) { + } finally { + if ( !old ) { + context.removeAttribute( "id" ); + } + } + } + } + + return oldSizzle(query, context, extra, seed); + }; + + for ( var prop in oldSizzle ) { + Sizzle[ prop ] = oldSizzle[ prop ]; + } + + // release memory in IE + div = null; + })(); +} + +(function(){ + var html = document.documentElement, + matches = html.matchesSelector || html.mozMatchesSelector || html.webkitMatchesSelector || html.msMatchesSelector, + pseudoWorks = false; + + try { + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( document.documentElement, "[test!='']:sizzle" ); + + } catch( pseudoError ) { + pseudoWorks = true; + } + + if ( matches ) { + Sizzle.matchesSelector = function( node, expr ) { + // Make sure that attribute selectors are quoted + expr = expr.replace(/\=\s*([^'"\]]*)\s*\]/g, "='$1']"); + + if ( !Sizzle.isXML( node ) ) { + try { + if ( pseudoWorks || !Expr.match.PSEUDO.test( expr ) && !/!=/.test( expr ) ) { + return matches.call( node, expr ); + } + } catch(e) {} + } + + return Sizzle(expr, null, null, [node]).length > 0; + }; + } +})(); + +(function(){ + var div = document.createElement("div"); + + div.innerHTML = "
"; + + // Opera can't find a second classname (in 9.6) + // Also, make sure that getElementsByClassName actually exists + if ( !div.getElementsByClassName || div.getElementsByClassName("e").length === 0 ) { + return; + } + + // Safari caches class attributes, doesn't catch changes (in 3.2) + div.lastChild.className = "e"; + + if ( div.getElementsByClassName("e").length === 1 ) { + return; + } + + Expr.order.splice(1, 0, "CLASS"); + Expr.find.CLASS = function( match, context, isXML ) { + if ( typeof context.getElementsByClassName !== "undefined" && !isXML ) { + return context.getElementsByClassName(match[1]); + } + }; + + // release memory in IE + div = null; +})(); + +function dirNodeCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 && !isXML ){ + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( elem.nodeName.toLowerCase() === cur ) { + match = elem; + break; + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +function dirCheck( dir, cur, doneName, checkSet, nodeCheck, isXML ) { + for ( var i = 0, l = checkSet.length; i < l; i++ ) { + var elem = checkSet[i]; + + if ( elem ) { + var match = false; + + elem = elem[dir]; + + while ( elem ) { + if ( elem.sizcache === doneName ) { + match = checkSet[elem.sizset]; + break; + } + + if ( elem.nodeType === 1 ) { + if ( !isXML ) { + elem.sizcache = doneName; + elem.sizset = i; + } + + if ( typeof cur !== "string" ) { + if ( elem === cur ) { + match = true; + break; + } + + } else if ( Sizzle.filter( cur, [elem] ).length > 0 ) { + match = elem; + break; + } + } + + elem = elem[dir]; + } + + checkSet[i] = match; + } + } +} + +if ( document.documentElement.contains ) { + Sizzle.contains = function( a, b ) { + return a !== b && (a.contains ? a.contains(b) : true); + }; + +} else if ( document.documentElement.compareDocumentPosition ) { + Sizzle.contains = function( a, b ) { + return !!(a.compareDocumentPosition(b) & 16); + }; + +} else { + Sizzle.contains = function() { + return false; + }; +} + +Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = (elem ? elem.ownerDocument || elem : 0).documentElement; + + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +var posProcess = function( selector, context ) { + var match, + tmpSet = [], + later = "", + root = context.nodeType ? [context] : context; + + // Position selectors must be done after the filter + // And so must :not(positional) so we move all PSEUDOs to the end + while ( (match = Expr.match.PSEUDO.exec( selector )) ) { + later += match[0]; + selector = selector.replace( Expr.match.PSEUDO, "" ); + } + + selector = Expr.relative[selector] ? selector + "*" : selector; + + for ( var i = 0, l = root.length; i < l; i++ ) { + Sizzle( selector, root[i], tmpSet ); + } + + return Sizzle.filter( later, tmpSet ); +}; + +// EXPOSE +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.filters; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + +})(); + + +var runtil = /Until$/, + rparentsprev = /^(?:parents|prevUntil|prevAll)/, + // Note: This RegExp should be improved, or likely pulled from Sizzle + rmultiselector = /,/, + isSimple = /^.[^:#\[\.,]*$/, + slice = Array.prototype.slice, + POS = jQuery.expr.match.POS, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var ret = this.pushStack( "", "find", selector ), + length = 0; + + for ( var i = 0, l = this.length; i < l; i++ ) { + length = ret.length; + jQuery.find( selector, this[i], ret ); + + if ( i > 0 ) { + // Make sure that the results are unique + for ( var n = length; n < ret.length; n++ ) { + for ( var r = 0; r < length; r++ ) { + if ( ret[r] === ret[n] ) { + ret.splice(n--, 1); + break; + } + } + } + } + } + + return ret; + }, + + has: function( target ) { + var targets = jQuery( target ); + return this.filter(function() { + for ( var i = 0, l = targets.length; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector, false), "not", selector); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector, true), "filter", selector ); + }, + + is: function( selector ) { + return !!selector && jQuery.filter( selector, this ).length > 0; + }, + + closest: function( selectors, context ) { + var ret = [], i, l, cur = this[0]; + + if ( jQuery.isArray( selectors ) ) { + var match, selector, + matches = {}, + level = 1; + + if ( cur && selectors.length ) { + for ( i = 0, l = selectors.length; i < l; i++ ) { + selector = selectors[i]; + + if ( !matches[selector] ) { + matches[selector] = jQuery.expr.match.POS.test( selector ) ? + jQuery( selector, context || this.context ) : + selector; + } + } + + while ( cur && cur.ownerDocument && cur !== context ) { + for ( selector in matches ) { + match = matches[selector]; + + if ( match.jquery ? match.index(cur) > -1 : jQuery(cur).is(match) ) { + ret.push({ selector: selector, elem: cur, level: level }); + } + } + + cur = cur.parentNode; + level++; + } + } + + return ret; + } + + var pos = POS.test( selectors ) ? + jQuery( selectors, context || this.context ) : null; + + for ( i = 0, l = this.length; i < l; i++ ) { + cur = this[i]; + + while ( cur ) { + if ( pos ? pos.index(cur) > -1 : jQuery.find.matchesSelector(cur, selectors) ) { + ret.push( cur ); + break; + + } else { + cur = cur.parentNode; + if ( !cur || !cur.ownerDocument || cur === context ) { + break; + } + } + } + } + + ret = ret.length > 1 ? jQuery.unique(ret) : ret; + + return this.pushStack( ret, "closest", selectors ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + if ( !elem || typeof elem === "string" ) { + return jQuery.inArray( this[0], + // If it receives a string, the selector is used + // If it receives nothing, the siblings are used + elem ? jQuery( elem ) : this.parent().children() ); + } + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( isDisconnected( set[0] ) || isDisconnected( all[0] ) ? + all : + jQuery.unique( all ) ); + }, + + andSelf: function() { + return this.add( this.prevObject ); + } +}); + +// A painfully simple check to see if an element is disconnected +// from a document (should be improved, where feasible). +function isDisconnected( node ) { + return !node || !node.parentNode || node.parentNode.nodeType === 11; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return jQuery.nth( elem, 2, "nextSibling" ); + }, + prev: function( elem ) { + return jQuery.nth( elem, 2, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( elem.parentNode.firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.makeArray( elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ), + // The variable 'args' was introduced in + // https://github.com/jquery/jquery/commit/52a0238 + // to work around a bug in Chrome 10 (Dev) and should be removed when the bug is fixed. + // http://code.google.com/p/v8/issues/detail?id=1050 + args = slice.call(arguments); + + if ( !runtil.test( name ) ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + ret = this.length > 1 && !guaranteedUnique[ name ] ? jQuery.unique( ret ) : ret; + + if ( (this.length > 1 || rmultiselector.test( selector )) && rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + + return this.pushStack( ret, name, args.join(",") ); + }; +}); + +jQuery.extend({ + filter: function( expr, elems, not ) { + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 ? + jQuery.find.matchesSelector(elems[0], expr) ? [ elems[0] ] : [] : + jQuery.find.matches(expr, elems); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + nth: function( cur, result, dir, elem ) { + result = result || 1; + var num = 0; + + for ( ; cur; cur = cur[dir] ) { + if ( cur.nodeType === 1 && ++num === result ) { + break; + } + } + + return cur; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, keep ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep(elements, function( elem, i ) { + var retVal = !!qualifier.call( elem, i, elem ); + return retVal === keep; + }); + + } else if ( qualifier.nodeType ) { + return jQuery.grep(elements, function( elem, i ) { + return (elem === qualifier) === keep; + }); + + } else if ( typeof qualifier === "string" ) { + var filtered = jQuery.grep(elements, function( elem ) { + return elem.nodeType === 1; + }); + + if ( isSimple.test( qualifier ) ) { + return jQuery.filter(qualifier, filtered, !keep); + } else { + qualifier = jQuery.filter( qualifier, filtered ); + } + } + + return jQuery.grep(elements, function( elem, i ) { + return (jQuery.inArray( elem, qualifier ) >= 0) === keep; + }); +} + + + + +var rinlinejQuery = / jQuery\d+="(?:\d+|null)"/g, + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig, + rtagName = /<([\w:]+)/, + rtbody = /", "" ], + legend: [ 1, "
", "
" ], + thead: [ 1, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + col: [ 2, "", "
" ], + area: [ 1, "", "" ], + _default: [ 0, "", "" ] + }; + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// IE can't serialize and + + + + +

API Helper

+

Use the buttons below to submit data to the autoapi with some HTTP method. Try posting the sample document below, then try get. Modify the document, press post again, and then show that two documents come back from pressing get.

+ +
+ + + + + + +
+
+ + diff --git a/brubeck/demos/templates/jinja2/base.html b/brubeck/demos/templates/jinja2/base.html new file mode 100644 index 0000000..22b8828 --- /dev/null +++ b/brubeck/demos/templates/jinja2/base.html @@ -0,0 +1,8 @@ + + + {% block title %}{% endblock %} + + +{% block body %}{% endblock %} + + diff --git a/brubeck/demos/templates/jinja2/errors.html b/brubeck/demos/templates/jinja2/errors.html new file mode 100644 index 0000000..6b5cd83 --- /dev/null +++ b/brubeck/demos/templates/jinja2/errors.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block title %}Error{% endblock %} +{% block body %}Drat! An error {{ error_code }} has occurred.{% endblock %} + diff --git a/brubeck/demos/templates/jinja2/success.html b/brubeck/demos/templates/jinja2/success.html new file mode 100644 index 0000000..6a7abc7 --- /dev/null +++ b/brubeck/demos/templates/jinja2/success.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block title %}Successful Jinja2 Template Render{% endblock %} +{% block body %}

Take five, {{ name }}!

{% endblock %} + diff --git a/brubeck/demos/templates/login/base.html b/brubeck/demos/templates/login/base.html new file mode 100644 index 0000000..22b8828 --- /dev/null +++ b/brubeck/demos/templates/login/base.html @@ -0,0 +1,8 @@ + + + {% block title %}{% endblock %} + + +{% block body %}{% endblock %} + + diff --git a/brubeck/demos/templates/login/errors.html b/brubeck/demos/templates/login/errors.html new file mode 100644 index 0000000..6b5cd83 --- /dev/null +++ b/brubeck/demos/templates/login/errors.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block title %}Error{% endblock %} +{% block body %}Drat! An error {{ error_code }} has occurred.{% endblock %} + diff --git a/brubeck/demos/templates/login/landing.html b/brubeck/demos/templates/login/landing.html new file mode 100644 index 0000000..4d052ec --- /dev/null +++ b/brubeck/demos/templates/login/landing.html @@ -0,0 +1,6 @@ +{% extends "base.html" %} +{% block title %}Welcome{% endblock %} +{% block body %}

Hello!

+[ logout ] +{% endblock %} + diff --git a/brubeck/demos/templates/login/login.html b/brubeck/demos/templates/login/login.html new file mode 100644 index 0000000..0a0427f --- /dev/null +++ b/brubeck/demos/templates/login/login.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block title %}Private system{% endblock %} +{% block body %}

Login

+
+Username ::
+Password ::
+ +
+{% endblock %} + diff --git a/brubeck/demos/templates/longpolling/base.html b/brubeck/demos/templates/longpolling/base.html new file mode 100644 index 0000000..f20d80a --- /dev/null +++ b/brubeck/demos/templates/longpolling/base.html @@ -0,0 +1,55 @@ + + + Long Poller + + + + + + + + +
+
+ + diff --git a/brubeck/demos/templates/mako/base.html b/brubeck/demos/templates/mako/base.html new file mode 100644 index 0000000..1d18b92 --- /dev/null +++ b/brubeck/demos/templates/mako/base.html @@ -0,0 +1,8 @@ + + + <%block name="title">${title}</%block> + + +${self.body()} + + diff --git a/brubeck/demos/templates/mako/errors.html b/brubeck/demos/templates/mako/errors.html new file mode 100644 index 0000000..760589c --- /dev/null +++ b/brubeck/demos/templates/mako/errors.html @@ -0,0 +1 @@ +error \ No newline at end of file diff --git a/brubeck/demos/templates/mako/success.html b/brubeck/demos/templates/mako/success.html new file mode 100644 index 0000000..e7643f5 --- /dev/null +++ b/brubeck/demos/templates/mako/success.html @@ -0,0 +1,5 @@ +<%inherit file="base.html"/> +<%block name="title"> + Successful Mako Template Render + +

Take five, ${name}!

\ No newline at end of file diff --git a/brubeck/demos/templates/multipart/base.html b/brubeck/demos/templates/multipart/base.html new file mode 100644 index 0000000..36ebd6b --- /dev/null +++ b/brubeck/demos/templates/multipart/base.html @@ -0,0 +1,19 @@ + + + {% block title %}{% endblock %} + + + + + +{% block body %}{% endblock %} + + diff --git a/brubeck/demos/templates/multipart/errors.html b/brubeck/demos/templates/multipart/errors.html new file mode 100644 index 0000000..6b5cd83 --- /dev/null +++ b/brubeck/demos/templates/multipart/errors.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block title %}Error{% endblock %} +{% block body %}Drat! An error {{ error_code }} has occurred.{% endblock %} + diff --git a/brubeck/demos/templates/multipart/landing.html b/brubeck/demos/templates/multipart/landing.html new file mode 100644 index 0000000..46d713b --- /dev/null +++ b/brubeck/demos/templates/multipart/landing.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% block title %}Welcome{% endblock %} +{% block body %} +
+ Name: + Comment: + +
+{% endblock %} + diff --git a/brubeck/demos/templates/mustache/errors.mustache b/brubeck/demos/templates/mustache/errors.mustache new file mode 100644 index 0000000..94e8b0e --- /dev/null +++ b/brubeck/demos/templates/mustache/errors.mustache @@ -0,0 +1,8 @@ + + + Error! + + +

Oh noes! An error {{error_code}} occurred.

+ + diff --git a/brubeck/demos/templates/mustache/nested_partial.mustache b/brubeck/demos/templates/mustache/nested_partial.mustache new file mode 100644 index 0000000..70efb02 --- /dev/null +++ b/brubeck/demos/templates/mustache/nested_partial.mustache @@ -0,0 +1 @@ +

And take five more.

diff --git a/brubeck/demos/templates/mustache/success.mustache b/brubeck/demos/templates/mustache/success.mustache new file mode 100644 index 0000000..0600003 --- /dev/null +++ b/brubeck/demos/templates/mustache/success.mustache @@ -0,0 +1,8 @@ + + + Successful Mustache Template Render + + + {{> take_five}} + + diff --git a/brubeck/demos/templates/mustache/take_five.mustache b/brubeck/demos/templates/mustache/take_five.mustache new file mode 100644 index 0000000..a64416c --- /dev/null +++ b/brubeck/demos/templates/mustache/take_five.mustache @@ -0,0 +1,2 @@ +

Take five, {{name}}!

+{{> nested_partial}} diff --git a/brubeck/demos/templates/tornado/base.html b/brubeck/demos/templates/tornado/base.html new file mode 100644 index 0000000..15d1d9a --- /dev/null +++ b/brubeck/demos/templates/tornado/base.html @@ -0,0 +1,8 @@ + + + {% block title %}{% end %} + + +{% block body %}{% end %} + + diff --git a/brubeck/demos/templates/tornado/errors.html b/brubeck/demos/templates/tornado/errors.html new file mode 100644 index 0000000..09f6d27 --- /dev/null +++ b/brubeck/demos/templates/tornado/errors.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block title %}Error{% end %} +{% block body %}Drat! An error {{ error_code }} has occurred.{% end %} + diff --git a/brubeck/demos/templates/tornado/success.html b/brubeck/demos/templates/tornado/success.html new file mode 100644 index 0000000..4f4f1f8 --- /dev/null +++ b/brubeck/demos/templates/tornado/success.html @@ -0,0 +1,4 @@ +{% extends "base.html" %} +{% block title %}Successful Tornado Template Render{% end %} +{% block body %}

Take five, {{ name }}!

{% end %} + diff --git a/brubeck/docs/AUTHENTICATION.md b/brubeck/docs/AUTHENTICATION.md new file mode 100644 index 0000000..f01ff4c --- /dev/null +++ b/brubeck/docs/AUTHENTICATION.md @@ -0,0 +1,33 @@ +# Authentication + +Authentication is provided by decorating functions with the `@web_authenticated` decorator. This decorator expects the handler to have a `current_user` property that returns either an authenticated `User` model or None. + +The `UserHandlingMixin` provides functionality for authenticating a user and creating the `current_user` property. + +The work that's required will depend on how you build your system. The authentication framework uses a DictShield Document to create the `User` model, so you can implement a database query or check user information in a sorted CSV. Either way, you still get the authentication framework you need. + +In practice, this is what your code looks like. + + from brubeck.auth import web_authenticated, UserHandlingMixin + + class DemoHandler(WebMessageHandler, UserHandlingMixin): + @web_authenticated + def post(self): + ... + +The `User` model in brubeck.auth will probably serve as a good basis for your needs. A Brubeck user looks roughly like below. + + class User(Document): + """Bare minimum to have the concept of a User. + """ + username = StringField(max_length=30, required=True) + email = EmailField(max_length=100) + password = StringField(max_length=128) + is_active = BooleanField(default=False) + last_login = LongField(default=curtime) + date_joined = LongField(default=curtime) + ... + +* [Basic Demo](https://github.com/j2labs/brubeck/blob/master/demos/demo_auth.py) +* [Login System](https://github.com/j2labs/brubeck/blob/master/demos/demo_login.py) + diff --git a/brubeck/docs/AUTOAPI.md b/brubeck/docs/AUTOAPI.md new file mode 100644 index 0000000..87a200a --- /dev/null +++ b/brubeck/docs/AUTOAPI.md @@ -0,0 +1,140 @@ +# AutoAPI + +Brubeck combines the metaprogramming in DictShield along with the assumption +that REST is generally similar to CRUD to provide a mechanism for generating +APIs from DictShield models. + +There are two things that must be consider: + +1. Data Processing +2. Persistence + + +## Data Processing + +The data processing is essentially to provide GET, POST, PUT and DELETE for a +document design. The document provides a mechanism for validating the ID of a +document, which is useful for both GET and DELETE. It also provides the +mechanism for validating an entire document, as we'd expect to receive with +either POST or PUT. + +We could define a simple model to look like this: + + class Todo(Document): + completed = BooleanField(default=False) + text = StringField(required=True) + class Meta: + id_options = {'auto_fill': True} + +DictShield provides a way to validate input against this structure. It also +provides a way to format output, like when we serialize the structure to JSON, +with some fields removed, based on what permissions are available to the user. + +We'll create a `Todo` instance. + + >>> t = Todo(text='This is some text') + >>> t.validate() + True + +Let's serialize it to a Python dictionary. This is probably what we'd save in +a database. + + >>> t.to_python() + {'_types': ['Todo'], 'text': u'This is some text', 'completed': False, '_id': UUID('c4ac6aff-737c-47db-ab07-fbe402b08d1c'), '_cls': 'Todo'} + +Or maybe we're just gonna store JSON in something. + + >>> t.to_json() + '{"_types": ["Todo"], "text": "This is some text", "completed": false, "_id": "7e48a600-f599-4a3a-9244-73760841f70e", "_cls": "Todo"}' + +It's useful for APIs too, because you can combine one of it's `make_safe` +functions with whatever access rights the user has. DictShield provides the +concept of *owner* and *public* in the form of a blacklist and a whitelist +respectively. + + >>> Todo.make_json_ownersafe(t) + '{"text": "This is some text", "completed": false}' + >>> + +If we provide GET and PUT we will need to handle ID fields. DictShield +documents let us validate individual fields if we want. That looks like this: + + >>> Todo.id.validate('c4ac6aff-737c-47db-ab07-fbe402b08d1c') + UUID('c4ac6aff-737c-47db-ab07-fbe402b08d1c') + +We can see that the returned value is the input coerced into the type of the +field. + +This is what failed validation looks like, notice that the input is a munged +version of the input above. + + >>> Todo.id.validate('c4ac6aff-737c-47db-ab07-fbe402b0c') + Traceback (most recent call last): + File "", line 1, in + File "/Users/jd/Projects/dictshield/dictshield/fields/base.py", line 178, in validate + self.field_name, value) + dictshield.base.ShieldException: Not a valid UUID value - None:c4ac6aff-737c-47db-ab07-fbe402b0c + + +## Persistence + +Persistence is then handled by way of a `QuerySet`. A dict based QuerySet, +called `DictQueryset`, is provided by default. Other implementations for +supporting MongoDB, Redis and MySQL are on the way. + +The interface to the QuerySets is defined in the `AbstractQueryset`. We see +some familiar names defined: `create()`, `read()`, `update()` and `destroy()`. +These functions then either call `create_one` or `create_many` for each CRUD +operation. + +CRUD doesn't map exactly to REST, but it's close, so Brubeck attempts to +accurately cover REST's behavior using CRUD operations. It's not a 1:1 mapping. + +The `DictQueryset` then subclasses `AbstractQueryset` and implements +`create_one`, `create_many`, etc. These functions are focused primarily around +a document's ID. The ID, as provided by DictShield, is how we identify which +documents should be deleted or updated or retrieved. + + +## Putting Both Together + +Putting the two together is a simple process. + +First we import the persistence layer and define the data's structure: + + from brubeck.queryset import DictQueryset + + class Todo(Document): + completed = BooleanField(default=False) + text = StringField(required=True) + class Meta: + id_options = {'auto_fill': True} + +Then we subclass `AutoAPIBase` and define two fields, `queries` and `model`. +The model is our Document from above. The queries is whichever queryset we're +using. + + class TodosAPI(AutoAPIBase): + queries = DictQueryset() + model = Todo + +Setup a Brubeck instance as you normally would, but then register the AutoApi +instance with the app. + + app = Brubeck(...) + app.register_api(TodosAPI) + +Done. + + +# Examples + +Brubeck comes with an AutoAPI example that is slightly more elaborate than what +we see above. + +* [AutoAPI Demo](https://github.com/j2labs/brubeck/blob/master/demos/demo_autoapi.py) + +There is also an example where Brubeck's AutoAPI is used in conjunction with +the well known Todo list javascript demo. + +* [Todos](https://github.com/j2labs/todos) diff --git a/brubeck/docs/CONCURRENCY.md b/brubeck/docs/CONCURRENCY.md new file mode 100644 index 0000000..5cd8588 --- /dev/null +++ b/brubeck/docs/CONCURRENCY.md @@ -0,0 +1,63 @@ +# Concurrency + +Brubeck is basically a pipeline of coroutines attempting to fulfill web requests. Each `MessageHandler` is executed as a coroutine. [Greenlet's](http://packages.python.org/greenlet/), the coroutines in Brubeck, are optimized for fast context-switching. + +Coroutines, combined with a scheduler (aka "a hub"), make for an interesting and lightweight alternative to threads. Greenlets are so lightweight that we don't have to think too hard on how many we spawn, and Brubeck handles each request as a single coroutine. + +Brubeck supports Eventlet and Gevent. They are similar in design. Both use Greenlets for coroutines. Both provide a mechanism for converting blocking network drivers into nonblocking. They both provide a schedular, aka a "hub", to provide *thread-like* behavior. + + +## The Flow + +Processing flows from the incoming message to a function that processes that message into the form of a `Request`. This request will operate until it reaches some point of I/O, or, it completes. + +Brubeck has a scheduler, like Twisted's Reactor or Tornado's IOLoop, but it's behind the scenes. Being behind the scenes allows it to create a simple interface to nonblocking behavior, but can be confusing upfront. + +If you're reaching out to the database, Brubeck might go back and check for incoming messages. If you're reaching out to some http service, Brubeck might check for incoming messages, or complete that other request that now has data from the database. In this sense, the context switching is *implicit*. + +Brubeck can offer nonblocking access to: + +* SSH w/ [paramiko](http://www.lag.net/paramiko/) +* MySQL +* Postgres +* Redis w/ [redis-py](https://github.com/andymccurdy/redis-py) +* MongoDB w/ [pymongo](http://api.mongodb.org/python/current/) +* Riak w/ [riak](https://github.com/basho/riak-python-client) +* Memcache + + +## Gevent + +Gevent was started by Denis Bilenko and is written to use `libevent`. Gevent's performance characteristics suggest it is very fast, stable and efficient on resources. + +Install the `envs/gevent.reqs` to use gevent. + +* [Gevent](http://gevent.org) +* [Gevent Introduction](http://gevent.org/intro.html) + +Extras: + +* MySQL w/ [ultramysql](https://github.com/esnme/ultramysql) +* Postgres w/ [psychopg](http://wiki.postgresql.org/wiki/Psycopg) +* Memcache w/ [ultramemcache](https://github.com/esnme/ultramemcache) + + +## Eventlet + +Eventlet is distinct for being mostly in Python. It later added support for libevent too. Eventlet was started by developers at Linden Labs and used to support Second Life. + +Install `envs/eventlet.reqs` to use eventlet. + +* [Eventlet](http://eventlet.net). +* [Eventlet History](http://eventlet.net/doc/history.html) + +Extras: + +* [Database Connection Pooling](http://eventlet.net/doc/modules/db_pool.html) + + +## Making A Decision + +I tend to choose gevent. My tests have shown that it is significantly faster and lighter on resources than Eventlet. + +If you have virtualenv, try experimenting and seeing which one you like best. diff --git a/brubeck/docs/DATAMODELING.md b/brubeck/docs/DATAMODELING.md new file mode 100644 index 0000000..ee6bde0 --- /dev/null +++ b/brubeck/docs/DATAMODELING.md @@ -0,0 +1,155 @@ +# Data Modeling + +Brubeck uses [DictShield](https://github.com/j2labs/dictshield) for modeling. + +DictShield offers input validation and structuring without taking a stance on what database you should be using. There are many good reasons to use all kinds of databases. DictShield only cares about Python dictionaries. If you can get your data into those, DictShield will handle the rest. + +DictShield strives to be database agnostic in the same way that Mongrel2 is language agnostic. + +* [DictShield](https://github.com/j2labs/dictshield) + + +## A Look At The Code + +Let's say we're going to store a BlogPost and all of it's comments in a single structure. Maybe we'll even keep a copy of the author information there too. + +An `Author` will have some information we only want to share with the author, like the email address associated with a post. But we want every user to be able to see the author's username and name, so those will be public fields. + + class Author(EmbeddedDocument): + name = StringField() + username = StringField() + email = EmailField() + a_setting = BooleanField() # private + is_active = BooleanField() # private + _private_fields=['is_active'] + _public_fields=['username', 'name'] + +A `Comment` will contain the comment text, username and email address of the commenter. We only show the email address to the owner of the blog though, so it's not listedd as a public field. + + class Comment(EmbeddedDocument): + text = StringField() + username = StringField() + email = EmailField() + _public_fields=['username', 'text'] + +And now the `BlogPost`. It will have a title, content, author, a post date, the list of comments, and a flag for whether or not it's a deleted entry (eg. a *tombstone*). + + class BlogPost(Document): + title = StringField() + content = StringField() + author = EmbeddedDocumentField(Author) + post_date = DateTimeField(default=datetime.datetime.now) + comments = ListField(EmbeddedDocumentField(Comment)) + deleted = BooleanField() + _private_fields=['personal_thoughts'] + _public_fields=['author', 'content', 'comments'] + +Notice that the `BlogPost` has a `ListField` containing a list of `Comments` objects. It also has an `EmbededDocumentField` anytime it's using another DictShield model as the field's value. + + +## Using It + +This is what it might look to instantiate the structures. + + >>> author = Author(name='james', username='j2d2', email='jdennis@gmail.com', + ... a_setting=True, is_active=True) + >>> comment1 = Comment(text='This post was awesome!', username='bro', + ... email='bru@dudegang.com') + >>> comment2 = Comment(text='This post is ridiculous', username='barbie', + ... email='barbie@dudegang.com') + >>> content = """Retro single-origin coffee chambray stumptown, scenester VHS + ... bicycle rights 8-bit keytar aesthetic cosby sweater photo booth. Gluten-free + ... trust fund keffiyeh dreamcatcher skateboard, williamsburg yr salvia tattooed + ... """ + >>> blogpost = BlogPost(title='Hipster Hodgepodge', author=author, content=content, + ... comments=[comment1, comment2], deleted=False) + +We'd probably call `to_python()` to make the data suitable for saving in a database. This process converts the values exactly as they're found into a dictionary of Python values. + + >>> blogpost.to_python() + { + '_types': ['BlogPost'], + '_cls': 'BlogPost' + 'post_date': datetime.datetime(2012, 4, 22, 13, 6, 50, 530609), + 'deleted': False, + 'title': u'Hipster Hodgepodge', + 'content': u'Retro single-origin coffee chambray stumptown, scenester VHS\nbicycle rights 8-bit keytar aesthetic cosby sweater photo booth. Gluten-free\ntrust fund keffiyeh dreamcatcher skateboard, williamsburg yr salvia tattooed\n', + 'author': { + 'username': u'j2d2', + '_types': ['Author'], + 'name': u'james', + 'a_setting': True, + 'is_active': True, + '_cls': 'Author', + 'email': u'jdennis@gmail.com' + }, + 'comments': [ + { + 'username': u'bro', + 'text': u'This post was awesome!', + '_types': ['Comment'], + 'email': u'bru@dudegang.com', + '_cls': 'Comment' + }, + { + 'username': u'barbie', + 'text': u'This post is ridiculous', + '_types': ['Comment'], + 'email': u'barbie@dudegang.com', + '_cls': 'Comment' + } + ], + } + +DictShield also has the concept of an owner formalized in the `make_*_ownersafe()` function, which can serialize to either Python or JSON. Notice that the date is converted to [iso8601 format](http://en.wikipedia.org/wiki/ISO_8601) too. + + >>> BlogPost.make_json_ownersafe(blogpost) + { + "post_date": "2012-04-22T13:06:50.530609", + "deleted": false, + "title": "Hipster Hodgepodge", + "content": "Retro single-origin coffee chambray stumptown, scenester VHS\nbicycle rights 8-bit keytar aesthetic cosby sweater photo booth. Gluten-free\ntrust fund keffiyeh dreamcatcher skateboard, williamsburg yr salvia tattooed\n" + "author": { + "username": "j2d2", + "a_setting": true, + "name": "james", + "email": "jdennis@gmail.com" + }, + "comments": [ + { + "username": "bro", + "text": "This post was awesome!", + "email": "bru@dudegang.com" + }, + { + "username": "barbie", + "text": "This post is ridiculous", + "email": "barbie@dudegang.com" + } + ], + } + +This is what the document looks like serialized for the general public. The same basic mechanism is at work as the other serilializations, but this has removed the data that is not for public consumption, like email addresses. + + >>> BlogPost.make_json_publicsafe(blogpost) + { + "content": "Retro single-origin coffee chambray stumptown, scenester VHS\nbicycle rights 8-bit keytar aesthetic cosby sweater photo booth. Gluten-free\ntrust fund keffiyeh dreamcatcher skateboard, williamsburg yr salvia tattooed\n" + "author": { + "username": "j2d2", + "name": "james" + }, + "comments": [ + { + "username": "bro", + "text": "This post was awesome!" + }, + { + "username": "barbie", + "text": "This post is ridiculous" + } + ], + } + +Notice that in all of these cases, the permissions and the serialization was done recursively across the structures and embedded structures. + + diff --git a/brubeck/docs/DEMOS.md b/brubeck/docs/DEMOS.md new file mode 120000 index 0000000..6080ce8 --- /dev/null +++ b/brubeck/docs/DEMOS.md @@ -0,0 +1 @@ +../demos/README.md \ No newline at end of file diff --git a/brubeck/docs/DEPENDENCIES.md b/brubeck/docs/DEPENDENCIES.md new file mode 100644 index 0000000..1072eb0 --- /dev/null +++ b/brubeck/docs/DEPENDENCIES.md @@ -0,0 +1,75 @@ +# Dependencies + +Brubeck leverages a few awesome Python packages and some other stuff, mainly in C, for a significant piece of it's capabilities. Credit must be given where credit is due. + + +## Web Serving + +Brubeck can support Mongrel2 or WSGI. + +### Mongrel2 + +[Mongrel2](http://mongrel2.org) is an asynchronous and language-agnostic (!!) web server by [Zed Shaw](http://zedshaw.com/). Mongrel2 handles everything relevant to HTTP or Web Sockets and has facilities for passing request handling to external services via [ZeroMQ guide](http://zguide.zeromq.org/) sockets. + +This decoupling of the webserver from the request handling allows for interesting web service topologies. It also allows for easy scaling too, as servers can be added or taken down as necessary with restarting or HUPing anything. + +### WSGI + +Brubeck also supports WSGI. This means you can put it behind [Gunicorn](http://gunicorn.org/) or run Brubeck apps on [Heroku](http://www.heroku.com/). + +WSGI support is provided by each of the concurrency options, which are described next. + + +## Concurrency + +Brubeck is basically a pipeline of coroutines attempting to fulfill web requests. Each `MessageHandler` is executed as a coroutine, implemented as a `greenlet`. + +[Greenlet's](http://packages.python.org/greenlet/) are a Python implementation of coroutines optimized for fast context-switching. Greenlet's can be thought of as similar to generators that don't require a `yield` statement. + +Coroutines, combined with a scheduler (aka "a hub"), make for an interesting and lightweight alternative to threads. Greenlets are so lightweight that we don't have to think too hard on how many we spawn, and Brubeck handlers each request as a single coroutine. + + +### Eventlet + +Eventlet is an implementation of a scheduling system. In addition to scheduling, it will convert your blocking calls into nonblocking automatically as part of it's scheduling. + +This makes building nonblocking, asynchronous systems look the same as building blocking, synchronous systems. The kind that normally live in threads. + +Eventlet was started by developers at Linden Labs and used to support Second Life. + +Install `envs/eventlet.reqs` to use eventlet. + +* [Eventlet](http://eventlet.net). +* [Eventlet History](http://eventlet.net/doc/history.html) + + +### Gevent + +Gevent was started by Denis Bilenko as an alternative to Eventlet. It is similar in design but uses an event loop implemented in C; `libevent`. It will be soon be on the newer `libev`. + +Tests suggest that Gevent's performance characteristics are both lightweight and very fast. + +Install the `envs/gevent.reqs` to use gevent. + +* [Gevent](http://gevent.org) +* [Gevent Introduction](http://gevent.org/intro.html) + + +### Alternatives + +There are also reasonable arguments for explicit context switching. Or perhaps even a different language. If you prefer that model, I recommend the systems below: + +* [Twisted Project](http://twistedmatrix.com/) +* [Node.js](http://nodejs.org) +* [EventMachine](https://github.com/eventmachine/eventmachine/wiki) + + +## DictShield + +DictShield offers input validation and structuring without taking a stance on what database you should be using. There are many good reasons to use all kinds of databases. DictShield only cares about Python dictionaries. If you can get your data into those, DictShield will handle the rest. + +DictShield strives to be database agnostic in the same way that Mongrel2 is language agnostic. + +* [DictShield](https://github.com/j2labs/dictshield) + + diff --git a/brubeck/docs/DEPLOYING.md b/brubeck/docs/DEPLOYING.md new file mode 100644 index 0000000..fac9aca --- /dev/null +++ b/brubeck/docs/DEPLOYING.md @@ -0,0 +1,155 @@ +# Deploying + +Brubeck can support Mongrel2 or WSGI. + + +## Mongrel2 + +[Mongrel2](http://mongrel2.org) is an asynchronous and language-agnostic (!) web server by [Zed Shaw](http://zedshaw.com/). Mongrel2 handles everything relevant to HTTP or Web Sockets and has facilities for passing request handling to external services via [ZeroMQ guide](http://zguide.zeromq.org/) sockets. + +This decoupling of the webserver from the request handling allows for interesting web service topologies. It also allows for easy scaling too, as servers can be added or taken down as necessary with restarting or HUPing anything. + +If you are using Mongrel2, you will need to turn Mongrel2 on in addition to running a Brubeck process. This can be a little tedious while developing, but it leads to efficient production deployment capabilities similar to that of HAProxy or Nginx. + +Interacting with Mongrel2 is best done with the `m2sh` command. + + $ m2sh load -config mongrel2.conf -db the.db + $ m2sh start -db the.db -every + +Mongrel2 is now running. + +If you want Mongrel2 to run on port 80 you will need to use sudo. This also causes Mongrel2 to run in the background and detach from the command shell. In this case, you can stop Mongrel2 using another m2sh command. + + $ m2sh stop -db the.db -every + + +## WSGI + +Brubeck supports WSGI by way of it's concurrency systems. This means you can put it behind [Gunicorn](http://gunicorn.org/) or run Brubeck apps on [Heroku](http://www.heroku.com/). + +From an app design point of view, it is a one line change to specify a WSGI handler instead of a Mongrel2 handler. + +* [Gevent WSGI](http://www.gevent.org/gevent.wsgi.html) +* [Eventlet WSGI](http://eventlet.net/doc/modules/wsgi.html) +* [Brubeck WSGI Demo](https://github.com/j2labs/brubeck/blob/master/demos/demo_wsgi.py) + + +## Deployment Environments + +There are multiple ways to deploy Brubeck. A vanilla Ubuntu system on AWS or +Linode can work well. A Heroku dyno can work. + + +### Quickness + +Quickness is a project for experimenting. It helps experimenters by creating +a simple environment for deploying big ideas, like Brubeck and all of it's +dependencies or Erlang or Clojure & Java & any other things worth having when +using Clojure. + +It is built with Ubuntu in mind and works nicely with +[Vagrant](http://vagranup.com). + +A typical Quickness install of Brubeck looks like this: + + $ git clone https://github.com/j2labs/quickness.git + $ source quickness/env/profile + Q: quick_new + Q: quick_install brubeck + +Quickness is developed by the same folks that build Brubeck & DictShield. This deployment strategy uses Mongrel2 as the web server. This involves compiling and installing both ZeroMQ and Mongrel2, but Quickness will handle all of that for you. + +* [Quickness](https://github.com/j2labs/quickness) + + +### Heroku + +To deploy to Heroku your app needs to be configured to use WSGI, which you'll see in the snippet below, and y + +Install [Heroku Toolbelt](https://toolbelt.herokuapp.com/) + +Prepare the project directory + + $ mkdir herokuapp && cd herokuapp + +Initialize our git repo and pull Brubeck in + + $ git init + $ git submodule add git://github.com/j2labs/brubeck.git brubeck + $ git submodule init + $ git submodule update + +Initialize our Heroku app + + $ heroku login + $ heroku create --stack cedar + +Set up the environment + + $ virtualenv --distribute venv + $ source venv/bin/activate + $ pip install dictshield ujson gevent + $ pip freeze -l > requirements.txt + +Create .gitignore. + + $ cat .gitignore + venv + *.pyc + +Create Procfile + + $ cat Procile + web: python app.py + +Create .env + + $ cat .env + PYTHONPATH=brubeck + +Create app.py + + import os + + from brubeck.request_handling import Brubeck, WebMessageHandler + from brubeck.connections import WSGIConnection + + class DemoHandler(WebMessageHandler): + def get(self): + self.set_body("Hello, from Brubeck!") + return self.render() + + config = { + 'msg_conn': WSGIConnection(int(os.environ.get('PORT', 6767))), + 'handler_tuples': [ + (r'^/', DemoHandler) + ] + } + + if __name__ == '__main__': + app = Brubeck(**config) + app.run() + +Try it out + + $ foreman start + +You should now be able to visit [localhost:5000](http://localhost:5000). Notice +that this uses port 5000 instead of the usual 6767. + +Is it working? Great! Let's put it on Heroku + + git add . + git commit -m "init" + git push heroku master + +Seems like Heroku will clobber whatever PYTHONPATH you set when you first push a Python project, so set it now + + heroku config:add PYTHONPATH=/app/brubeck/:/app/ + +Navigate to your new Brubeck app on Heroku! + + +### Gunicorn + +Instructions coming soon. diff --git a/brubeck/docs/FILEUPLOADING.md b/brubeck/docs/FILEUPLOADING.md new file mode 100644 index 0000000..5162d33 --- /dev/null +++ b/brubeck/docs/FILEUPLOADING.md @@ -0,0 +1,43 @@ +# File Uploading + +Brubeck supports file uploading as form-urlencoded or as multipart form data. +It's easy to upload a file to Brubeck using curl. + + $ cd brubeck/demos + $ ./demo_multipart.py + +In this demo we see code that finds each file uploaded in a field on the +request message. That looks like this: + + class UploadHandler(...): + def post(self): + file_one = self.message.files['data'][0] + i = Image.open(StringIO.StringIO(file_one['body'])) + i.save('word.png') + ... + +This demo receives an image and writes it to the file system as `word.png`. It +wouldn't be much work to adjust this to whatever your needs are. + +The demo also uses PIL, so install that if you don't already have it. + + $ pip install PIL + +Use sudo if necessary. + + +## Trying It + +If you're using Mongrel2, you'll need to turn that on too. It works fine with +WSGI too. + + $ m2sh load -db the.db -config mongrel2.conf + $ m2sh start -db the.db -every + +OK. Now we can use curl to upload some image. + + $ curl -F data=@someimage.png http://localhost:6767/ + +The end result is that you'll have an image called `word.png` written to the +same directory as your Brubeck process. + diff --git a/brubeck/docs/HANDLERS.md b/brubeck/docs/HANDLERS.md new file mode 100644 index 0000000..d890189 --- /dev/null +++ b/brubeck/docs/HANDLERS.md @@ -0,0 +1,79 @@ +# Message Handlers + +Let's take a look at that demo handler from before. + + class DemoHandler(WebMessageHandler): + def get(self): + self.set_body('Take five') + return self.render() + + options = { + 'handler_tuples': [(r'^/', DemoHandler)], + 'msg_conn': WSGIConnection(port=6767), + } + + app = Brubeck(**options) + app.run() + +The `DemoHandler` class has a `get()` implementation, so we know that handler answers HTTP GET and that handler is mapped to the root URL, '/'. + +Brubeck is also configured to run as a WSGI server on port 6767. Turn the app on and it will answer requests at http://localhost:6767. + + +## Handling Requests + +The framework can be used for different requirements. It can be lean and lightweight for high throughput or you can fatten it up and use it for rendering pages in a database backed CMS. + +The general architecture of the system is to map requests for a specific URL to some [callable](http://docs.python.org/library/functions.html#callable) for processing the request. The configuration attempts to match handlers to URL's by inspecting a list of `(url pattern, callable)` tuples. First regex to match provides the callable. + +Some people like to use classes as handlers. Some folks prefer to use functions. Brubeck supports both. + +The HTTP methods allowed are: GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE, CONNECT. + + +### MessageHandler Classes + +When a class model is used, the class will be instantiated for the life of the request and then thrown away. This keeps our memory requirements nice and light. + +Brubeck's `MessageHandler` design is similar to what you see in [Tornado](https://github.com/facebook/tornado), or [web.py](http://webpy.org/). + +To answer HTTP GET requests, implement `get()` on a WebMessageHandler instance. + + class DemoHandler(WebMessageHandler): + def get(self): + self.set_body('Take five!') + return self.render() + +Then we add `DemoHandler` to the routing config and instantiate a Brubeck instance. + + urls = [(r'^/brubeck', DemoHandler)] + config = { + 'handler_tuples': urls, + ... + } + + Brubeck(**config).run() + +Notice the url regex is `^/brubeck`. This will put our handler code on `http://hostname/brubeck`. (Probably [http://localhost:6767/brubeck](http://localhost:6767/brubeck)). + +* [Runnable demo](https://github.com/j2labs/brubeck/blob/master/demos/demo_minimal.py) + + +### Functions and Decorators + +If you'd prefer to just use a simple function, you instantiate a Brubeck instance and wrap your function with the `add_route` decorator. + +Your function will be given two arguments. First, is the `application` itself. This provides the function with a hook almost all the information it might need. The second argument, the `message`, provides all the information available about the request. + +That looks like this: + + app = Brubeck(mongrel2_pair=('ipc://127.0.0.1:9999', + 'ipc://127.0.0.1:9998')) + + @app.add_route('^/brubeck', method='GET') + def foo(application, message): + return http_response('Take five!', 200, 'OK', {}) + + app.run() + +* [Runnable demo](https://github.com/j2labs/brubeck/blob/master/demos/demo_noclasses.py) diff --git a/brubeck/docs/INDEX.md b/brubeck/docs/INDEX.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/brubeck/docs/INDEX.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/brubeck/docs/INSTALLING.md b/brubeck/docs/INSTALLING.md new file mode 100644 index 0000000..f24df22 --- /dev/null +++ b/brubeck/docs/INSTALLING.md @@ -0,0 +1,148 @@ +# Installing The Environment + +First, we have to install a few things. Brubeck depends on Mongrel2, ZeroMQ and a few python packages. + +All three packages live in github, so we'll clone the repos to our Desktop. + + $ cd ~/Desktop/ + $ git clone https://github.com/j2labs/brubeck.git + $ git clone https://github.com/zedshaw/mongrel2.git + $ wget http://download.zeromq.org/historic/zeromq-3.2.2.tar.gz + $ tar zxf zeromq-3.2.2.tar.gz + + +## ZeroMQ + +ZeroMQ, from a Python perspective, is actually two pieces: libzmq and pyzmq. libzmq must be installed by hand like you see below. + + $ cd ~/Desktop/zeromq-3.2.2 + $ ./autogen.sh + $ ./configure ## for mac ports use: ./configure --prefix=/opt/local + $ make + $ sudo make install + + +## Mongrel2 + +Mongrel2 is also painless to setup. + + $ cd ~/Desktop/mongrel2 + $ make ## for mac ports use: make macports + ## Mongrel 2 requires sqlite3 and dev libraries of sqlite3 + $ sudo apt-get install sqlite3 + $ sudo apt-get install libsqlite3-dev + $ sudo make install + +There are a few compile options available at the bottom of Mongrel2's `Makefile`. Take a look if the code above doesn't compile successfully. + + +## Virtualenv & Virtualenvwrapper + +Brubeck works great with virtualenv. I highly recommend using it. + +Virtualenv is a way to construct isolated python environments. Very handy for managing multiple environments in a single machine. + +Install both virtualenv and virtualenvwrapper with `pip`. + + pip install virtualenv virtualenvwrapper + +Then, we must configure our shell to know where to store our virtualenv's. While we're there, we'll source the virtualenvwrapper shell script. + +Open your `.profile` or `.bashrc` and add the following two lines. + + export WORKON_HOME="~/.virtualenvs" + source /usr/local/bin/virtualenvwrapper.sh + +By sourcing virtualenvwrapper, you get a simple interface for creating, managing and removing virutalenv environments. + + $ mkvirtualenv # Creates a virtual environment + $ deactivate # Turn off a virtual environment + $ workon # Turn on a virtual environment + +For more information, see my quick & dirty howto. + +* [Quick & Dirty Virtualenv & Virtualenvwrapper](http://j2labs.tumblr.com/post/5181438807/quick-dirty-virtualenv-virtualenvwrapper) + + +## Python Packages & Brubeck + +If you have pip installed, you can install everything with the requirements file. + + $ cd ~/Desktop/brubeck + $ pip install -I -r ./envs/brubeck.reqs + +We now choose either eventlet or gevent and install the relevent requirements file in the same directory. + +To install `eventlet` support: + + $ pip install -I -r ./envs/eventlet.reqs + +To install `gevent` support: + + $ pip install -I -r ./envs/gevent.reqs + +Note that gevent requires `libevent`, which should be available on the package-manager of your choice. + +### Brubeck Itself + +As the last step, install Brubeck. + + $ cd ~/Desktop/brubeck + $ python setup.py install + + +# A Demo + +Assuming the environment installation went well we can now turn on Brubeck. + +First, we setup the Mongrel2 config. + + $ cd ~/Desktop/brubeck/demos + $ m2sh load -config mongrel2.conf -db the.db + $ m2sh start -db the.db -host localhost + +Now we'll turn on a Brubeck instance. + + $ cd ~/Desktop/brubeck/demos + $ ./demo_minimal.py + +If you see `Brubeck v0.x.x online ]------------` we can try loading a URL in a browser. +Now try [a web request](http://localhost:6767/brubeck). + + +## Mongrel2 Configuration + +Mongrel2 is a separate process from Brubeck, so it is configured separately. + +This is what the Mongrel2 configuration looks like for the demo project. + + brubeck_handler = Handler( + send_spec='ipc://127.0.0.1:9999', + send_ident='34f9ceee-cd52-4b7f-b197-88bf2f0ec378', + recv_spec='ipc://127.0.0.1:9998', + recv_ident='') + + brubeck_host = Host( + name="localhost", + routes={'/': brubeck_handler}) + + brubeck_serv = Server( + uuid="f400bf85-4538-4f7a-8908-67e313d515c2", + access_log="/log/mongrel2.access.log", + error_log="/log/mongrel2.error.log", + chroot="./", + default_host="localhost", + name="brubeck test", + pid_file="/run/mongrel2.pid", + port=6767, + hosts = [brubeck_host]) + + settings = {"zeromq.threads": 1} + + servers = [brubeck_serv] + +In short: any requests for `http://localhost:6767/` should be sent to the Brubeck handler. + +Don't forget that our Brubeck handler is only configured to answer `http://localhost:6767/brubeck` for now. You could add another route once you're comfortable building `MessageHandler`'s + +The web server answers requests on port `6767`. It logs to the `./log` directory. It also writes a pidfile in the `./run` directory. diff --git a/brubeck/docs/LICENSE.md b/brubeck/docs/LICENSE.md new file mode 100644 index 0000000..5d4dcbc --- /dev/null +++ b/brubeck/docs/LICENSE.md @@ -0,0 +1,26 @@ +# Copyright 2012 J2 Labs LLC. 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 J2 Labs LLC ``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 J2 Labs LLC 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. + +The views and conclusions contained in the software and documentation are +those of the authors and should not be interpreted as representing official +policies, either expressed or implied, of J2 Labs LLC. diff --git a/brubeck/docs/QUERYSETS.md b/brubeck/docs/QUERYSETS.md new file mode 100644 index 0000000..094b9f3 --- /dev/null +++ b/brubeck/docs/QUERYSETS.md @@ -0,0 +1,18 @@ +# QuerySets + +There are times when a carefully crafted query is right and there are times +when a simple CRUD interface can do the trick. Brubeck provides querysets that +implement a simple CRUD interface. This is useful for the AutoAPI, support for +basic caching and a consistent way of handling database connections. + +A `Queryset` is then an implementation of CRUD functions for a single item or +multiple items. This type of interface makes the system consistent with the +idea that data should be stored in a simple manner, eg. a key that maps to a +particular document. + +If you need anything more complicated than that, it's easy enough to go from +the DictShield model into something that will fit nicely with your custom +query. + +Querysets are an area of active development but are still young in +implementation. diff --git a/brubeck/docs/TEMPLATING.md b/brubeck/docs/TEMPLATING.md new file mode 100644 index 0000000..231e079 --- /dev/null +++ b/brubeck/docs/TEMPLATING.md @@ -0,0 +1,68 @@ +# Templates + +Brubeck currently supports [Jinja2](http://jinja.pocoo.org/), [Tornado](http://www.tornadoweb.org/documentation#templates), [Mako](http://www.makotemplates.org/) or [Pystache](https://github.com/defunkt/pystache) templates. + +Template support is contained in `brubeck.templates` as rendering handlers. Each handler will attach a `render_template` function to your handler and overwrite the default `render_error` to produce templated errors messages. + +Using a template system is then as easy as calling `render_template` with the template filename and some context, just like you're used to. + + +## Jinja2 Example + +Using Jinja2 template looks like this. + + from brubeck.templating import Jinja2Rendering + + class DemoHandler(WebMessageHandler, Jinja2Rendering): + def get(self): + context = { + 'name': 'J2D2', + } + return self.render_template('success.html', **context) + +The corresponding HTML looks like this: + + + + Jinja2 Render + + +

Take five, {{ name }}!

+ + + +* [Runnable demo](https://github.com/j2labs/brubeck/blob/master/demos/demo_jinja2.py) +* [Demo templates](https://github.com/j2labs/brubeck/tree/master/demos/templates/jinja2) + + +### Template Loading + +In addition to using a rendering handler, you need to provide the path to your +templates. + +That looks like this: + + from brubeck.templating import load_jinja2_env + + config = { + template_loader=load_jinja2_env('./templates/jinja2') + ... + } + +Using a function here keeps the config lightweight and flexible. +`template_loader` needs to be some function that returns an environment. + + +## Demos + +* Jinja2 ([Code](https://github.com/j2labs/brubeck/blob/master/demos/demo_jinja2.py), [Templates](https://github.com/j2labs/brubeck/tree/master/demos/templates/jinja2)) +* Mako ([Code](https://github.com/j2labs/brubeck/tree/master/demos/demo_mako.py), [Templates](https://github.com/j2labs/brubeck/tree/master/demos/templates/mako)) +* Tornado ([Code](https://github.com/j2labs/brubeck/tree/master/demos/demo_tornado.py), [Templates](https://github.com/j2labs/brubeck/tree/master/demos/templates/tornado)) +* Mustache ([Code](https://github.com/j2labs/brubeck/tree/master/demos/demo_mustache.py), [Templates](https://github.com/j2labs/brubeck/tree/master/demos/templates/mustache)) + +Is your favorite template system not in this list? Please take a look at the other implementations. It's probably easy to add support. + +* [brubeck.templating](https://github.com/j2labs/brubeck/blob/master/brubeck/templating.py) + + + diff --git a/brubeck/docs/ZEROMQ.md b/brubeck/docs/ZEROMQ.md new file mode 100644 index 0000000..8c995ab --- /dev/null +++ b/brubeck/docs/ZEROMQ.md @@ -0,0 +1,69 @@ +# ZeroMQ + +ZeroMQ, aka ZMQ, is essentially a sockets framework. It makes it easy to build +messaging topologies across different types of sockets. They also are language +agnostic by way of having driver implementations in every language: Scheme, +Java, C, Ruby, Haskell, Erlang; and Brubeck uses the Python driver. + +It is common for service oriented architectures to be constructed with HTTP +interfaces, but Brubeck believes ZMQ is a more suitable tool. It provides +multiple message distribution patterns and lets you open multiple types of +sockets. + + +## Simple Examples + +Here is a simple job distributor. It passes messages round-robin to any +connected hosts. + + import zmq + import time + + ctx = zmq.Context() + s = ctx.socket(zmq.PUSH) + s.bind("ipc://hellostream:5678") + + while True: + s.send("hello") + print 'Sending a hello' + time.sleep(1) + +This what a simple consumer could look like. See what happens if you hook up +multiple consumers. + + import zmq + import datetime + + ctx = zmq.Context() + s = ctx.socket(zmq.PULL) + s.connect("ipc://hellostream:5678") + + while True: + msg = s.recv() + print 'Received:', msg + + +# Brubeck and ZMQ + +Brubeck can uses this system when it communicates with Mongrel2. It can also +use this to talk to pools of workers, or AMQP servers, or data mining engines. + +ZMQ is part of Brubeck's concurrency pool, so working with it is just like +working with any networked system. When you use Brubeck with Mongrel2, you +communicate with Mongrel2 over two ZMQ sockets. + +There is a PUSH/PULL socket that Mongrel2 uses to send messages to handlers, +like Brubeck. An added bonus is that PUSH/PULL sockets automatically load balance +requests between any connected handlers. Add another handler and it is +automatically part of the round robin queue. + +When the handlers are ready to respond, they use a PUB/SUB socket, meaning +Mongrel2 subscribes to responses from Brubeck handlers. This can be interesting +for multiple reasons, such as having media served from a single Brubeck handler +to multiple Mongrel2 frontends. + +Having two sockets allows for an interesting messaging topology between Mongrel2 +and Brubeck. All of ZeroMQ is available to you for communicating with workers too. +You might enjoy building an image processing system in Scheme and can do so by +opening a ZeroMQ socket in your Scheme process to connect with a Brubeck socket. +ZeroMQ is mostly language agnostic. diff --git a/brubeck/docs/upupdowndown.py b/brubeck/docs/upupdowndown.py new file mode 100755 index 0000000..cb57e11 --- /dev/null +++ b/brubeck/docs/upupdowndown.py @@ -0,0 +1,5 @@ +### Could live in settings +src_dir = './' +html_dir = '../../brubeck.io/' +header_file = '%s%s' % (html_dir + 'media/', 'header.html') +footer_file = '%s%s' % (html_dir + 'media/', 'footer.html') diff --git a/brubeck/envs/brubeck.reqs b/brubeck/envs/brubeck.reqs new file mode 100644 index 0000000..1c161d4 --- /dev/null +++ b/brubeck/envs/brubeck.reqs @@ -0,0 +1,9 @@ +Cython==0.15.1 +Jinja2==2.6 +dictshield==0.4.3 +py-bcrypt==0.2 +pymongo==2.1.1 +python-dateutil==2.1 +pyzmq==2.1.11 +ujson==1.18 +-e git+git://github.com/reduxdj/schematics/tree/v0.9.2 \ No newline at end of file diff --git a/brubeck/envs/eventlet.reqs b/brubeck/envs/eventlet.reqs new file mode 100644 index 0000000..b7343db --- /dev/null +++ b/brubeck/envs/eventlet.reqs @@ -0,0 +1 @@ +eventlet==0.9.16 diff --git a/brubeck/envs/gevent.reqs b/brubeck/envs/gevent.reqs new file mode 100644 index 0000000..7f99262 --- /dev/null +++ b/brubeck/envs/gevent.reqs @@ -0,0 +1,2 @@ +gevent==0.13.6 +gevent_zeromq==0.2.2 diff --git a/brubeck/setup.py b/brubeck/setup.py new file mode 100644 index 0000000..bdd4634 --- /dev/null +++ b/brubeck/setup.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from setuptools import setup + +setup(name='brubeck', + version='0.4.0', + description='Python Library for building Mongrel2 / ZeroMQ message handlers', + author='James Dennis', + author_email='jdennis@gmail.com', + url='http://github.com/j2labs/brubeck', + packages=['brubeck', 'brubeck.queryset'], + install_requires=['ujson', 'dictshield']) diff --git a/brubeck/tests/__init__.py b/brubeck/tests/__init__.py new file mode 100644 index 0000000..9fb4f8a --- /dev/null +++ b/brubeck/tests/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +"""The Brubeck Message Handling system test suite.""" +"""Version number is the same as the Brubeck version it is for """ +version = "0.3.5" +version_info = (0, 3, 5) diff --git a/brubeck/tests/fixtures/__init__.py b/brubeck/tests/fixtures/__init__.py new file mode 100644 index 0000000..9fb4f8a --- /dev/null +++ b/brubeck/tests/fixtures/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python + +"""The Brubeck Message Handling system test suite.""" +"""Version number is the same as the Brubeck version it is for """ +version = "0.3.5" +version_info = (0, 3, 5) diff --git a/brubeck/tests/fixtures/http_request_brubeck.txt b/brubeck/tests/fixtures/http_request_brubeck.txt new file mode 100644 index 0000000..e857d97 --- /dev/null +++ b/brubeck/tests/fixtures/http_request_brubeck.txt @@ -0,0 +1 @@ +34f9ceee-cd52-4b7f-b197-88bf2f0ec378 3 /brubeck 508:{"PATH":"/brubeck","x-forwarded-for":"127.0.0.1","cache-control":"max-age=0","accept-language":"en-US,en;q=0.8","accept-encoding":"gzip,deflate,sdch","connection":"keep-alive","accept-charset":"ISO-8859-1,utf-8;q=0.7,*;q=0.3","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","user-agent":"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2","host":"127.0.0.1:6767","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/brubeck","PATTERN":"/"},0:, diff --git a/brubeck/tests/fixtures/http_request_root.txt b/brubeck/tests/fixtures/http_request_root.txt new file mode 100644 index 0000000..aa5aeca --- /dev/null +++ b/brubeck/tests/fixtures/http_request_root.txt @@ -0,0 +1 @@ +34f9ceee-cd52-4b7f-b197-88bf2f0ec378 5 / 466:{"PATH":"/","x-forwarded-for":"127.0.0.1","accept-language":"en-US,en;q=0.8","accept-encoding":"gzip,deflate,sdch","connection":"keep-alive","accept-charset":"ISO-8859-1,utf-8;q=0.7,*;q=0.3","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","user-agent":"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2","host":"127.0.0.1:6767","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/","PATTERN":"/"},0:, diff --git a/brubeck/tests/fixtures/http_request_root_with_cookie.txt b/brubeck/tests/fixtures/http_request_root_with_cookie.txt new file mode 100644 index 0000000..aee1d45 --- /dev/null +++ b/brubeck/tests/fixtures/http_request_root_with_cookie.txt @@ -0,0 +1 @@ +ee-cd52-4b7f-b197-88bf2f0ec378 5 / 487:{"PATH":"/","x-forwarded-for":"127.0.0.1","accept-language":"en-US,en;q=0.8","accept-encoding":"gzip,deflate,sdch","connection":"keep-alive","accept-charset":"ISO-8859-1,utf-8;q=0.7,*;q=0.3","accept":"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8","user-agent":"Mozilla/5.0 (X11; Linux i686) AppleWebKit/535.2 (KHTML, like Gecko) Chrome/15.0.874.106 Safari/535.2","host":"127.0.0.1:6767","cookie":"key=value","METHOD":"GET","VERSION":"HTTP/1.1","URI":"/","PATTERN":"/"},0:, diff --git a/brubeck/tests/fixtures/request_handler_fixtures.py b/brubeck/tests/fixtures/request_handler_fixtures.py new file mode 100644 index 0000000..d1b07d6 --- /dev/null +++ b/brubeck/tests/fixtures/request_handler_fixtures.py @@ -0,0 +1,27 @@ +import os +## +## setup our simple messages for testing """ +## +dir = os.path.abspath(__file__)[0:len(os.path.abspath(__file__))-28] + '/' + +HTTP_REQUEST_BRUBECK = file( dir + 'http_request_brubeck.txt','r').read() + +HTTP_REQUEST_ROOT = file(dir + 'http_request_root.txt','r').read() + +HTTP_REQUEST_ROOT_WITH_COOKIE = file(dir + 'http_request_root_with_cookie.txt','r').read() + +## +## our test body text +## +TEST_BODY_METHOD_HANDLER = file(dir + 'test_body_method_handler.txt','r').read().rstrip('\n') +TEST_BODY_OBJECT_HANDLER = file(dir + 'test_body_object_handler.txt','r').read().rstrip('\n') + +## +## setup our expected reponses +## +HTTP_RESPONSE_OBJECT_ROOT = 'HTTP/1.1 200 OK\r\nContent-Length: ' + str(len(TEST_BODY_OBJECT_HANDLER)) + '\r\n\r\n' + TEST_BODY_OBJECT_HANDLER +HTTP_RESPONSE_METHOD_ROOT = 'HTTP/1.1 200 OK\r\nContent-Length: ' + str(len(TEST_BODY_METHOD_HANDLER)) + '\r\n\r\n' + TEST_BODY_METHOD_HANDLER +HTTP_RESPONSE_JSON_OBJECT_ROOT = 'HTTP/1.1 200 OK\r\nContent-Length: 90\r\nContent-Type: application/json\r\n\r\n{"status_code":200,"status_msg":"OK","message":"Take five dude","timestamp":1320456118809}' + +HTTP_RESPONSE_OBJECT_ROOT_WITH_COOKIE = 'HTTP/1.1 200 OK\r\nSet-Cookie: key=value\r\nContent-Length: ' + str(len(TEST_BODY_OBJECT_HANDLER)) + '\r\n\r\n' + TEST_BODY_OBJECT_HANDLER + diff --git a/brubeck/tests/fixtures/test_body_method_handler.txt b/brubeck/tests/fixtures/test_body_method_handler.txt new file mode 100644 index 0000000..f6678d0 --- /dev/null +++ b/brubeck/tests/fixtures/test_body_method_handler.txt @@ -0,0 +1 @@ +Take five dude method handler diff --git a/brubeck/tests/fixtures/test_body_object_handler.txt b/brubeck/tests/fixtures/test_body_object_handler.txt new file mode 100644 index 0000000..3cc77aa --- /dev/null +++ b/brubeck/tests/fixtures/test_body_object_handler.txt @@ -0,0 +1 @@ +Take five dude object handler diff --git a/brubeck/tests/handlers/__init__.py b/brubeck/tests/handlers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/brubeck/tests/handlers/method_handlers.py b/brubeck/tests/handlers/method_handlers.py new file mode 100644 index 0000000..425c01f --- /dev/null +++ b/brubeck/tests/handlers/method_handlers.py @@ -0,0 +1,6 @@ +from brubeck.request_handling import http_response + +def simple_handler_method(self, application, *args): + """" dummy request action """ + return http_response(file('./fixtures/test_body_method_handler.txt','r').read().rstrip('\n'), 200, 'OK', dict()) + diff --git a/brubeck/tests/handlers/object_handlers.py b/brubeck/tests/handlers/object_handlers.py new file mode 100644 index 0000000..edafa9f --- /dev/null +++ b/brubeck/tests/handlers/object_handlers.py @@ -0,0 +1,46 @@ +from brubeck.request_handling import Brubeck, WebMessageHandler, JSONMessageHandler + +from tests.fixtures import request_handler_fixtures as FIXTURES + + + +class SimpleWebHandlerObject(WebMessageHandler): + def get(self): + self.set_body(FIXTURES.TEST_BODY_OBJECT_HANDLER) + return self.render() + +class CookieWebHandlerObject(WebMessageHandler): + def get(self): + self.set_cookie("key", self.get_cookie("key")); + self.set_body(FIXTURES.TEST_BODY_OBJECT_HANDLER) + return self.render() + +class SimpleJSONHandlerObject(JSONMessageHandler): + def get(self): + self.add_to_payload('message', 'Take five dude') + self.set_status(200) + """ we only set time so it matches our expected response """ + self.add_to_payload("timestamp",1320456118809) + return self.render() + +class CookieAddWebHandlerObject(WebMessageHandler): + def get(self): + self.set_cookie("key", "value"); + self.set_body(FIXTURES.TEST_BODY_OBJECT_HANDLER) + return self.render() + +class PrepareHookWebHandlerObject(WebMessageHandler): + def get(self): + return self.render() + + def prepare(self): + self.set_body(FIXTURES.TEST_BODY_OBJECT_HANDLER) + +class InitializeHookWebHandlerObject(WebMessageHandler): + def get(self): + return self.render() + + def initialize(self): + self.headers = dict() + self.set_body(FIXTURES.TEST_BODY_OBJECT_HANDLER) + diff --git a/brubeck/tests/test_queryset.py b/brubeck/tests/test_queryset.py new file mode 100755 index 0000000..2ff6905 --- /dev/null +++ b/brubeck/tests/test_queryset.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python + +import unittest + +import mock + +import brubeck +from handlers.method_handlers import simple_handler_method +from brubeck.request_handling import Brubeck, WebMessageHandler, JSONMessageHandler +from brubeck.connections import to_bytes, Request +from brubeck.request_handling import( + cookie_encode, cookie_decode, + cookie_is_encoded, http_response +) +from handlers.object_handlers import( + SimpleWebHandlerObject, CookieWebHandlerObject, + SimpleJSONHandlerObject, CookieAddWebHandlerObject, + PrepareHookWebHandlerObject, InitializeHookWebHandlerObject +) +from fixtures import request_handler_fixtures as FIXTURES + +from brubeck.autoapi import AutoAPIBase +from brubeck.queryset import DictQueryset, AbstractQueryset, RedisQueryset + +from dictshield.document import Document +from dictshield.fields import StringField +from brubeck.request_handling import FourOhFourException + +##TestDocument +class TestDoc(Document): + data = StringField() + class Meta: + id_field = StringField + +### +### Tests for ensuring that the autoapi returns good data +### +class TestQuerySetPrimitives(unittest.TestCase): + """ + a test class for brubeck's queryset objects' core operations. + """ + + def setUp(self): + self.queryset = AbstractQueryset() + + def create(self): + pass + + def read(self): + pass + + def update(self): + pass + + def destroy(self): + pass + + +class TestDictQueryset(unittest.TestCase): + """ + a test class for brubeck's dictqueryset's operations. + """ + + + def setUp(self): + self.queryset = DictQueryset() + + def seed_reads(self): + shields = [TestDoc(id="foo"), TestDoc(id="bar"), TestDoc(id="baz")] + self.queryset.create_many(shields) + return shields + + + def test__create_one(self): + shield = TestDoc(id="foo") + status, return_shield = self.queryset.create_one(shield) + self.assertEqual(self.queryset.MSG_CREATED, status) + self.assertEqual(shield, return_shield) + + status, return_shield = self.queryset.create_one(shield) + self.assertEqual(self.queryset.MSG_UPDATED, status) + + + def test__create_many(self): + shield0 = TestDoc(id="foo") + shield1 = TestDoc(id="bar") + shield2 = TestDoc(id="baz") + statuses = self.queryset.create_many([shield0, shield1, shield2]) + for status, datum in statuses: + self.assertEqual(self.queryset.MSG_CREATED, status) + + shield3 = TestDoc(id="bloop") + statuses = self.queryset.create_many([shield0, shield3, shield2]) + status, datum = statuses[1] + self.assertEqual(self.queryset.MSG_CREATED, status) + status, datum = statuses[0] + self.assertEqual(self.queryset.MSG_UPDATED, status) + + def test__read_all(self): + shields = self.seed_reads() + statuses = self.queryset.read_all() + + for status, datum in statuses: + self.assertEqual(self.queryset.MSG_OK, status) + + actual = sorted([datum for trash, datum in statuses]) + expected = sorted([shield.to_python() for shield in shields]) + self.assertEqual(expected, actual) + + def test__read_one(self): + shields = self.seed_reads() + for shield in shields: + status, datum = self.queryset.read_one(shield.id) + self.assertEqual(self.queryset.MSG_OK, status) + self.assertEqual(datum, shield.to_python()) + bad_key = 'DOESNTEXISIT' + status, datum = self.queryset.read(bad_key) + self.assertEqual(bad_key, datum) + self.assertEqual(self.queryset.MSG_FAILED, status) + + def test__read_many(self): + shields = self.seed_reads() + expected = [shield.to_python() for shield in shields] + responses = self.queryset.read_many([s.id for s in shields]) + for status, datum in responses: + self.assertEqual(self.queryset.MSG_OK, status) + self.assertTrue(datum in expected) + + bad_ids = [s.id for s in shields] + bad_ids.append('DOESNTEXISIT') + status, iid = self.queryset.read_many(bad_ids)[-1] + self.assertEqual(self.queryset.MSG_FAILED, status) + + + def test_update_one(self): + shields = self.seed_reads() + test_shield = shields[0] + test_shield.data = "foob" + status, datum = self.queryset.update_one(test_shield) + + self.assertEqual(self.queryset.MSG_UPDATED, status) + self.assertEqual('foob', datum['data']) + + status, datum = self.queryset.read_one(test_shield.id) + self.assertEqual('foob', datum['data']) + + + def test_update_many(self): + shields = self.seed_reads() + for shield in shields: + shield.data = "foob" + responses = self.queryset.update_many(shields) + for status, datum in responses: + self.assertEqual(self.queryset.MSG_UPDATED, status) + self.assertEqual('foob', datum['data']) + for status, datum in self.queryset.read_all(): + self.assertEqual('foob', datum['data']) + + + def test_destroy_one(self): + shields = self.seed_reads() + test_shield = shields[0] + status, datum = self.queryset.destroy_one(test_shield.id) + self.assertEqual(self.queryset.MSG_UPDATED, status) + + status, datum = self.queryset.read_one(test_shield.id) + self.assertEqual(test_shield.id, datum) + self.assertEqual(self.queryset.MSG_FAILED, status) + + + def test_destroy_many(self): + shields = self.seed_reads() + shield_to_keep = shields.pop() + responses = self.queryset.destroy_many([shield.id for shield in shields]) + for status, datum in responses: + self.assertEqual(self.queryset.MSG_UPDATED, status) + + responses = self.queryset.read_many([shield.id for shield in shields]) + for status, datum in responses: + self.assertEqual(self.queryset.MSG_FAILED, status) + + status, datum = self.queryset.read_one(shield_to_keep.id) + self.assertEqual(self.queryset.MSG_OK, status) + self.assertEqual(shield_to_keep.to_python(), datum) + + +class TestRedisQueryset(TestQuerySetPrimitives): + """ + Test RedisQueryset operations. + """ + def setUp(self): + pass + + def seed_reads(self): + shields = [TestDoc(id="foo"), TestDoc(id="bar"), TestDoc(id="baz")] + return shields + + def test__create_one(self): + with mock.patch('redis.StrictRedis') as patchedRedis: + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + queryset = RedisQueryset(db_conn=redis_connection) + + shield = TestDoc(id="foo") + queryset.create_one(shield) + + name, args, kwargs = redis_connection.mock_calls[0] + self.assertEqual(name, 'hset') + self.assertEqual(args, (queryset.api_id, 'foo', '{"_types": ["TestDoc"], "id": "foo", "_cls": "TestDoc"}')) + + def test__create_many(self): + with mock.patch('redis.StrictRedis') as patchedRedis: + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + queryset = RedisQueryset(db_conn=redis_connection) + queryset.create_many(self.seed_reads()) + expected = [ + ('pipeline', (), {}), + ('pipeline().hset', (queryset.api_id, 'foo', '{"_types": ["TestDoc"], "id": "foo", "_cls": "TestDoc"}'), {}), + ('pipeline().hset', (queryset.api_id, 'bar', '{"_types": ["TestDoc"], "id": "bar", "_cls": "TestDoc"}'), {}), + ('pipeline().hset', (queryset.api_id, 'baz', '{"_types": ["TestDoc"], "id": "baz", "_cls": "TestDoc"}'), {}), + ('pipeline().execute', (), {}), + ('pipeline().execute().__iter__', (), {}), + ('pipeline().reset', (), {}) + ] + for call in zip(expected, redis_connection.mock_calls): + self.assertEqual(call[0], call[1]) + + def test__read_all(self): + with mock.patch('redis.StrictRedis') as patchedRedis: + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + queryset = RedisQueryset(db_conn=redis_connection) + statuses = queryset.read_all() + + name, args, kwargs = redis_connection.mock_calls[0] + self.assertEqual(name, 'hvals') + self.assertEqual(args, (queryset.api_id,)) + + name, args, kwargs = redis_connection.mock_calls[1] + self.assertEqual(name, 'hvals().__iter__') + self.assertEqual(args, ()) + + def test__read_one(self): + for _id in ['foo', 'bar', 'baz']: + with mock.patch('redis.StrictRedis') as patchedRedis: + instance = patchedRedis.return_value + instance.hget.return_value = '{"called": "hget"}' + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + queryset = RedisQueryset(db_conn=redis_connection) + + msg, result = queryset.read_one(_id) + assert (RedisQueryset.MSG_OK, {'called': 'hget'}) == (msg, result) + + name, args, kwargs = redis_connection.mock_calls[0] + self.assertEqual(name, 'hget') + self.assertEqual(args, (queryset.api_id, _id)) + self.assertEqual(kwargs, {}) + + def test__read_many(self): + with mock.patch('redis.StrictRedis') as patchedRedis: + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + queryset = RedisQueryset(db_conn=redis_connection) + queryset.read_many(['foo', 'bar', 'baz', 'laser', 'beams']) + expected = [('pipeline', (), {}), + ('pipeline().hget', (queryset.api_id, 'foo'), {}), + ('pipeline().hget', (queryset.api_id, 'bar'), {}), + ('pipeline().hget', (queryset.api_id, 'baz'), {}), + ('pipeline().hget', (queryset.api_id, 'laser'), {}), + ('pipeline().hget', (queryset.api_id, 'beams'), {}), + ('pipeline().execute', (), {}), + ('pipeline().reset', (), {}), + ('pipeline().execute().__iter__', (), {}), + ('pipeline().execute().__iter__', (), {}), + ('pipeline().execute().__len__', (), {}), + ] + for call in zip(expected, redis_connection.mock_calls): + assert call[0] == call[1] + + def test_update_one(self): + with mock.patch('redis.StrictRedis') as patchedRedis: + instance = patchedRedis.return_value + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + queryset = RedisQueryset(db_conn=redis_connection) + + original = mock.Mock() + doc_instance = original.return_value + doc_instance.id = 'foo' + doc_instance.to_json.return_value = '{"to": "json"}' + + queryset.update_one(doc_instance) + + expected = ('hset', ('id', 'foo', '{"to": "json"}'), {}) + + self.assertEqual(expected, redis_connection.mock_calls[0]) + + + def test_update_many(self): + with mock.patch('redis.StrictRedis') as patchedRedis: + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + queryset = RedisQueryset(db_conn=redis_connection) + queryset.update_many(self.seed_reads()) + expected = [ + ('pipeline', (), {}), + ('pipeline().hset', (queryset.api_id, 'foo', '{"_types": ["TestDoc"], "id": "foo", "_cls": "TestDoc"}'), {}), + ('pipeline().hset', (queryset.api_id, 'bar', '{"_types": ["TestDoc"], "id": "bar", "_cls": "TestDoc"}'), {}), + ('pipeline().hset', (queryset.api_id, 'baz', '{"_types": ["TestDoc"], "id": "baz", "_cls": "TestDoc"}'), {}), + ('pipeline().execute', (), {}), + ('pipeline().reset', (), {}), + ('pipeline().execute().__iter__', (), {}), + ] + + for call in zip(expected, redis_connection.mock_calls): + self.assertEqual(call[0], call[1]) + + def test_destroy_one(self): + with mock.patch('redis.StrictRedis') as patchedRedis: + instance = patchedRedis.return_value + instance.pipeline = mock.Mock() + pipe_instance = instance.pipeline.return_value + pipe_instance.execute.return_value = ('{"success": "hget"}', 1) + + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + queryset = RedisQueryset(db_conn=redis_connection) + queryset.destroy_one('bar') + + expected = [ + ('pipeline', (), {}), + ('pipeline().hget', ('id', 'bar'), {}), + ('pipeline().hdel', ('id', 'bar'), {}), + ('pipeline().execute', (), {}) + ] + for call in zip(expected, redis_connection.mock_calls): + self.assertEqual(call[0], call[1]) + + def test_destroy_many(self): + with mock.patch('redis.StrictRedis') as patchedRedis: + instance = patchedRedis.return_value + instance.pipeline = mock.Mock() + pipe_instance = instance.pipeline.return_value + shields = self.seed_reads() + json_shields = [shield.to_json() for shield in shields] + results = json_shields + pipe_instance.execute.return_value = results + + redis_connection = patchedRedis(host='localhost', port=6379, db=0) + + queryset = RedisQueryset(db_conn=redis_connection) + + queryset.destroy_many([shield.id for shield in shields]) + + expected = [('pipeline', (), {}), + ('pipeline().hget', (queryset.api_id, 'foo'), {}), + ('pipeline().hget', (queryset.api_id, 'bar'), {}), + ('pipeline().hget', (queryset.api_id, 'baz'), {}), + ('pipeline().execute', (), {}), + ('pipeline().hdel', (queryset.api_id, 'foo'), {}), + ('pipeline().hdel', (queryset.api_id, 'bar'), {}), + ('pipeline().hdel', (queryset.api_id, 'baz'), {}), + ('pipeline().execute', (), {}), + ('pipeline().reset', (), {}) + ] + for call in zip(expected, redis_connection.mock_calls): + self.assertEqual(call[0], call[1]) + +## +## This will run our tests +## +if __name__ == '__main__': + unittest.main() diff --git a/brubeck/tests/test_request_handling.py b/brubeck/tests/test_request_handling.py new file mode 100755 index 0000000..8c67bba --- /dev/null +++ b/brubeck/tests/test_request_handling.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python + +import unittest +import sys +import brubeck +from handlers.method_handlers import simple_handler_method +from brubeck.request_handling import Brubeck, WebMessageHandler, JSONMessageHandler +from brubeck.connections import to_bytes, Request, WSGIConnection +from brubeck.request_handling import( + cookie_encode, cookie_decode, + cookie_is_encoded, http_response +) +from handlers.object_handlers import( + SimpleWebHandlerObject, CookieWebHandlerObject, + SimpleJSONHandlerObject, CookieAddWebHandlerObject, + PrepareHookWebHandlerObject, InitializeHookWebHandlerObject +) +from fixtures import request_handler_fixtures as FIXTURES + +### +### Message handling (non)coroutines for testing +### +def route_message(application, message): + handler = application.route_message(message) + return request_handler(application, message, handler) + +def request_handler(application, message, handler): + if callable(handler): + return handler() + +class MockMessage(object): + """ we are enough of a message to test routing rules message """ + def __init__(self, path = '/', msg = FIXTURES.HTTP_REQUEST_ROOT): + self.path = path + self.msg = msg + + def get(self): + return self.msg + +class TestRequestHandling(unittest.TestCase): + """ + a test class for brubeck's request_handler + """ + + def setUp(self): + """ will get run for each test """ + config = { + 'mongrel2_pair': ('ipc://127.0.0.1:9999', 'ipc://127.0.0.1:9998'), + 'msg_conn': WSGIConnection() + } + self.app = Brubeck(**config) + ## + ## our actual tests( test _xxxx_xxxx(self) ) + ## + def test_add_route_rule_method(self): + # Make sure we have no routes + self.assertEqual(hasattr(self.app,'_routes'),False) + + # setup a route + self.setup_route_with_method() + + # Make sure we have some routes + self.assertEqual(hasattr(self.app,'_routes'),True) + + # Make sure we have exactly one route + self.assertEqual(len(self.app._routes),1) + + def test_init_routes_with_methods(self): + # Make sure we have no routes + self.assertEqual(hasattr(self.app, '_routes'), False) + + # Create a tuple with routes with method handlers + routes = [ (r'^/', simple_handler_method), (r'^/brubeck', simple_handler_method) ] + # init our routes + self.app.init_routes( routes ) + + # Make sure we have two routes + self.assertEqual(len(self.app._routes), 2) + + def test_init_routes_with_objects(self): + # Make sure we have no routes + self.assertEqual(hasattr(self.app, '_routes'), False) + + # Create a tuple of routes with object handlers + routes = [(r'^/', SimpleWebHandlerObject), (r'^/brubeck', SimpleWebHandlerObject)] + self.app.init_routes( routes ) + + # Make sure we have two routes + self.assertEqual(len(self.app._routes), 2) + + def test_init_routes_with_objects_and_methods(self): + # Make sure we have no routes + self.assertEqual(hasattr(self.app, '_routes'), False) + + # Create a tuple of routes with a method handler and an object handler + routes = [(r'^/', SimpleWebHandlerObject), (r'^/brubeck', simple_handler_method)] + self.app.init_routes( routes ) + + # Make sure we have two routes + self.assertEqual(len(self.app._routes), 2) + + def test_add_route_rule_object(self): + # Make sure we have no routes + self.assertEqual(hasattr(self.app,'_routes'),False) + self.setup_route_with_object() + + # Make sure we have some routes + self.assertEqual(hasattr(self.app,'_routes'),True) + + # Make sure we have exactly one route + self.assertEqual(len(self.app._routes),1) + + def test_brubeck_handle_request_with_object(self): + # set up our route + self.setup_route_with_object() + + # Make sure we get a handler back when we request one + message = MockMessage(path='/') + handler = self.app.route_message(message) + self.assertNotEqual(handler,None) + + def test_brubeck_handle_request_with_method(self): + # We ran tests on this already, so assume it works + self.setup_route_with_method() + + # Make sure we get a handler back when we request one + message = MockMessage(path='/') + handler = self.app.route_message(message) + self.assertNotEqual(handler,None) + + def test_cookie_handling(self): + # set our cookie key and values + cookie_key = 'my_key' + cookie_value = 'my_secret' + + # encode our cookie + encoded_cookie = cookie_encode(cookie_value, cookie_key) + + # Make sure we do not contain our value (i.e. we are really encrypting) + self.assertEqual(encoded_cookie.find(cookie_value) == -1, True) + + # Make sure we are an encoded cookie using the function + self.assertEqual(cookie_is_encoded(encoded_cookie), True) + + # Make sure after decoding our cookie we are the same as the unencoded cookie + decoded_cookie_value = cookie_decode(encoded_cookie, cookie_key) + self.assertEqual(decoded_cookie_value, cookie_value) + + ## + ## test a bunch of very simple requests making sure we get the expected results + ## + def test_web_request_handling_with_object(self): + self.setup_route_with_object() + result = route_message(self.app, Request.parse_msg(FIXTURES.HTTP_REQUEST_ROOT)) + response = http_response(result['body'], result['status_code'], result['status_msg'], result['headers']) + self.assertEqual(FIXTURES.HTTP_RESPONSE_OBJECT_ROOT, response) + + def test_web_request_handling_with_method(self): + self.setup_route_with_method() + response = route_message(self.app, Request.parse_msg(FIXTURES.HTTP_REQUEST_ROOT)) + self.assertEqual(FIXTURES.HTTP_RESPONSE_METHOD_ROOT, response) + + def test_json_request_handling_with_object(self): + self.app.add_route_rule(r'^/$',SimpleJSONHandlerObject) + result = route_message(self.app, Request.parse_msg(FIXTURES.HTTP_REQUEST_ROOT)) + response = http_response(result['body'], result['status_code'], result['status_msg'], result['headers']) + self.assertEqual(FIXTURES.HTTP_RESPONSE_JSON_OBJECT_ROOT, response) + + def test_request_with_cookie_handling_with_object(self): + self.app.add_route_rule(r'^/$',CookieWebHandlerObject) + result = route_message(self.app, Request.parse_msg(FIXTURES.HTTP_REQUEST_ROOT_WITH_COOKIE)) + response = http_response(result['body'], result['status_code'], result['status_msg'], result['headers']) + self.assertEqual(FIXTURES.HTTP_RESPONSE_OBJECT_ROOT_WITH_COOKIE, response) + + def test_request_with_cookie_response_with_cookie_handling_with_object(self): + self.app.add_route_rule(r'^/$',CookieWebHandlerObject) + result = route_message(self.app, Request.parse_msg(FIXTURES.HTTP_REQUEST_ROOT_WITH_COOKIE)) + response = http_response(result['body'], result['status_code'], result['status_msg'], result['headers']) + self.assertEqual(FIXTURES.HTTP_RESPONSE_OBJECT_ROOT_WITH_COOKIE, response) + + def test_request_without_cookie_response_with_cookie_handling_with_object(self): + self.app.add_route_rule(r'^/$',CookieAddWebHandlerObject) + result = route_message(self.app, Request.parse_msg(FIXTURES.HTTP_REQUEST_ROOT)) + response = http_response(result['body'], result['status_code'], result['status_msg'], result['headers']) + self.assertEqual(FIXTURES.HTTP_RESPONSE_OBJECT_ROOT_WITH_COOKIE, response) + + def test_build_http_response(self): + response = http_response(FIXTURES.TEST_BODY_OBJECT_HANDLER, 200, 'OK', dict()) + self.assertEqual(FIXTURES.HTTP_RESPONSE_OBJECT_ROOT, response) + + def test_handler_initialize_hook(self): + ## create a handler that sets the expected body(and headers) in the initialize hook + handler = InitializeHookWebHandlerObject(self.app, Request.parse_msg(FIXTURES.HTTP_REQUEST_ROOT)) + result = handler() + response = http_response(result['body'], result['status_code'], result['status_msg'], result['headers']) + self.assertEqual(response, FIXTURES.HTTP_RESPONSE_OBJECT_ROOT) + + def test_handler_prepare_hook(self): + # create a handler that sets the expected body in the prepare hook + handler = PrepareHookWebHandlerObject(self.app, Request.parse_msg(FIXTURES.HTTP_REQUEST_ROOT)) + result = handler() + response = http_response(result['body'], result['status_code'], result['status_msg'], result['headers']) + self.assertEqual(response, FIXTURES.HTTP_RESPONSE_OBJECT_ROOT) + + ## + ## some simple helper functions to setup a route """ + ## + def setup_route_with_object(self, url_pattern='^/$'): + self.app.add_route_rule(url_pattern,SimpleWebHandlerObject) + + def setup_route_with_method(self, url_pattern='^/$'): + method = simple_handler_method + self.app.add_route_rule(url_pattern, method) + +## +## This will run our tests +## +if __name__ == '__main__': + unittest.main() diff --git a/brubeck/tox.ini b/brubeck/tox.ini new file mode 100644 index 0000000..ad91902 --- /dev/null +++ b/brubeck/tox.ini @@ -0,0 +1,17 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py26, py27 + +[testenv] +setenv = CFLAGS="-I/usr/local/include" +commands = nosetests --exe -w tests +deps = + gevent + mock + nose + redis + testtools diff --git a/brubeck/wsgi.py b/brubeck/wsgi.py new file mode 100644 index 0000000..fb16f2d --- /dev/null +++ b/brubeck/wsgi.py @@ -0,0 +1,36 @@ +from brubeck.request import Request +from brubeck.request_handling import coro_spawn, route_message + + +def receive_wsgi_req(environ, start_response): + request = Request.parse_wsgi_request(environ) + coro_spawn(route_message, self, request) + +class WSGIConnection(object): + """This class defines request handling methods for wsgi implimentations.""" + + def __init__(self): + pass + + def recv(self): + """Receives the request from the wsgi server.""" + pass + +if __name__ == "__main__": + from wsgiref.util import setup_testing_defaults + from wsgiref.simple_server import make_server + def simple_app(environ, start_response): + setup_testing_defaults(environ) + + status = '200 OK' + headers = [('Content-type', 'text/plain')] + + start_response(status, headers) + + ret = ["%s: %s\n" % (key, value) + for key, value in environ.iteritems()] + return ret + + httpd = make_server('', 8000, receive_wsgi_req) + print "Serving on port 8000..." + httpd.serve_forever() From 0c663fa84b627b0bfd0b98f14eae3e9d6cc88b11 Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Fri, 20 Sep 2013 08:40:36 -0400 Subject: [PATCH 6/9] added request handling --- brubeck/brubeck/request_handling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brubeck/brubeck/request_handling.py b/brubeck/brubeck/request_handling.py index ec01b02..228c312 100755 --- a/brubeck/brubeck/request_handling.py +++ b/brubeck/brubeck/request_handling.py @@ -55,7 +55,7 @@ def coro_spawn(function, app, message, *a, **kw): import os, sys from request import Request, to_bytes, to_unicode -from schematics.types.serializable import for_jsonschema, from_jsonschema + import ujson as json From c39a944d9592e0521fbd792b80ec98e654cf12e3 Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Fri, 20 Sep 2013 08:45:44 -0400 Subject: [PATCH 7/9] removed bad imports --- brubeck/request_handling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/brubeck/request_handling.py b/brubeck/request_handling.py index ec01b02..228c312 100755 --- a/brubeck/request_handling.py +++ b/brubeck/request_handling.py @@ -55,7 +55,7 @@ def coro_spawn(function, app, message, *a, **kw): import os, sys from request import Request, to_bytes, to_unicode -from schematics.types.serializable import for_jsonschema, from_jsonschema + import ujson as json From 575d2afbd9df0b55f6a329b9717eec0734731557 Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Sat, 21 Sep 2013 20:37:49 -0400 Subject: [PATCH 8/9] made shit work --- brubeck/models.py | 16 ++++++++-------- brubeck/request_handling.py | 2 -- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/brubeck/models.py b/brubeck/models.py index 74dc8e6..a0b4880 100644 --- a/brubeck/models.py +++ b/brubeck/models.py @@ -11,9 +11,9 @@ import auth -from timekeeping import curtime -from datamosh import OwnedModelMixin, StreamedModelMixin - +from timekeeping import curtime, MillisecondType +from schematics.contrib.mongo import ObjectIdType +from schematics.transforms import blacklist, whitelist import re @@ -94,17 +94,17 @@ def create_user(cls, username, password, email=str()): ### UserProfile ### -class UserProfile(Model, OwnedModelMixin, StreamedModelMixin): +class UserProfile(Model): """The basic things a user profile tends to carry. Isolated in separate class to keep separate from private data. """ # Provided by OwnedModelMixin - #owner_id = ObjectIdField(required=True) - #owner_username = StringField(max_length=30, required=True) + owner_id = ObjectIdType(required=True) + owner_username = StringType(max_length=30, required=True) # streamable # provided by StreamedModelMixin now - #created_at = MillisecondField() - #updated_at = MillisecondField() + created_at = MillisecondType() + updated_at = MillisecondType() # identity info name = StringType(max_length=255) diff --git a/brubeck/request_handling.py b/brubeck/request_handling.py index 68f6556..7aa3e5c 100755 --- a/brubeck/request_handling.py +++ b/brubeck/request_handling.py @@ -55,8 +55,6 @@ def coro_spawn(function, app, message, *a, **kw): import os, sys from request import Request, to_bytes, to_unicode -from schematics.serialize import for_jsonschema, from_jsonschema - import ujson as json ### From 0c3bbb836d32b797ccb8b24fa6a3d249e5f2ff7b Mon Sep 17 00:00:00 2001 From: Patrick Lemiuex Date: Mon, 23 Sep 2013 08:55:39 -0400 Subject: [PATCH 9/9] removed unneeded files --- .travis.yml | 14 -------------- tox.ini | 17 ----------------- wsgi.py | 36 ------------------------------------ 3 files changed, 67 deletions(-) delete mode 100644 .travis.yml delete mode 100644 tox.ini delete mode 100644 wsgi.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e8a6be2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: python - -python: - - 2.6 - - 2.7 - -before_install: - - sudo apt-get install libevent-dev - - pip install --use-mirrors gevent nose redis testtools - -install: - - python setup.py install - -script: nosetests --exe -w tests diff --git a/tox.ini b/tox.ini deleted file mode 100644 index ad91902..0000000 --- a/tox.ini +++ /dev/null @@ -1,17 +0,0 @@ -# Tox (http://tox.testrun.org/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - -[tox] -envlist = py26, py27 - -[testenv] -setenv = CFLAGS="-I/usr/local/include" -commands = nosetests --exe -w tests -deps = - gevent - mock - nose - redis - testtools diff --git a/wsgi.py b/wsgi.py deleted file mode 100644 index fb16f2d..0000000 --- a/wsgi.py +++ /dev/null @@ -1,36 +0,0 @@ -from brubeck.request import Request -from brubeck.request_handling import coro_spawn, route_message - - -def receive_wsgi_req(environ, start_response): - request = Request.parse_wsgi_request(environ) - coro_spawn(route_message, self, request) - -class WSGIConnection(object): - """This class defines request handling methods for wsgi implimentations.""" - - def __init__(self): - pass - - def recv(self): - """Receives the request from the wsgi server.""" - pass - -if __name__ == "__main__": - from wsgiref.util import setup_testing_defaults - from wsgiref.simple_server import make_server - def simple_app(environ, start_response): - setup_testing_defaults(environ) - - status = '200 OK' - headers = [('Content-type', 'text/plain')] - - start_response(status, headers) - - ret = ["%s: %s\n" % (key, value) - for key, value in environ.iteritems()] - return ret - - httpd = make_server('', 8000, receive_wsgi_req) - print "Serving on port 8000..." - httpd.serve_forever()