python-cyclone-1.1/0000755000175000017500000000000012124336260013351 5ustar lunarlunarpython-cyclone-1.1/MANIFEST.in0000644000175000017500000000015312124336260015106 0ustar lunarlunarinclude cyclone/appskel_default.zip include cyclone/appskel_foreman.zip include cyclone/appskel_signup.zip python-cyclone-1.1/setup.py0000644000175000017500000000655412124336260015075 0ustar lunarlunar#!/usr/bin/env python # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import platform from distutils import log from distutils.version import LooseVersion from distutils.version import StrictVersion requires = ["twisted"] # Avoid installation problems on old RedHat distributions (ex. CentOS 5) # http://stackoverflow.com/questions/7340784/easy-install-pyopenssl-error py_version = platform.python_version() if LooseVersion(py_version) < StrictVersion('2.6'): distname, version, _id = platform.dist() else: distname, version, _id = platform.linux_distribution() is_redhat = distname in ["CentOS", "redhat"] if is_redhat and version and StrictVersion(version) < StrictVersion('6.0'): requires.append("pyopenssl==0.12") else: requires.append("pyopenssl") # PyPy and setuptools don't get along too well, yet. if sys.subversion[0].lower().startswith("pypy"): import distutils.core setup = distutils.core.setup extra = dict(requires=requires) else: import setuptools setup = setuptools.setup extra = dict(install_requires=requires) try: from setuptools.command import egg_info egg_info.write_toplevel_names except (ImportError, AttributeError): pass else: """ 'twisted' should not occur in the top_level.txt file as this triggers a bug in pip that removes all of twisted when a package with a twisted plugin is removed. """ def _top_level_package(name): return name.split('.', 1)[0] def _hacked_write_toplevel_names(cmd, basename, filename): pkgs = dict.fromkeys( [_top_level_package(k) for k in cmd.distribution.iter_distribution_names() if _top_level_package(k) != "twisted" ] ) cmd.write_file("top-level names", filename, '\n'.join(pkgs) + '\n') egg_info.write_toplevel_names = _hacked_write_toplevel_names setup( name="cyclone", version="1.1", author="fiorix", author_email="fiorix@gmail.com", url="http://cyclone.io/", license="http://www.apache.org/licenses/LICENSE-2.0", description="Non-blocking web server. " "A facebook's Tornado on top of Twisted.", keywords="python non-blocking web server twisted facebook tornado", packages=["cyclone", "twisted.plugins"], package_data={"twisted": ["plugins/cyclone_plugin.py"], "cyclone": ["appskel_default.zip", "appskel_foreman.zip", "appskel_signup.zip"]}, scripts=["scripts/cyclone"], **extra ) try: from twisted.plugin import IPlugin, getPlugins list(getPlugins(IPlugin)) except Exception, e: log.warn("*** Failed to update Twisted plugin cache. ***") log.warn(str(e)) python-cyclone-1.1/.gitignore0000644000175000017500000000011012124336260015331 0ustar lunarlunar*.swp *.pyc *.so *~ build dist cyclone.egg-info dropin.cache *DS_Store* python-cyclone-1.1/demos/0000755000175000017500000000000012124336260014460 5ustar lunarlunarpython-cyclone-1.1/demos/bottle/0000755000175000017500000000000012124336260015751 5ustar lunarlunarpython-cyclone-1.1/demos/bottle/xmlrpc_client.py0000644000175000017500000000141112124336260021163 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import xmlrpclib srv = xmlrpclib.Server("http://localhost:8888/xmlrpc") print "echo:", srv.echo("hello world!") python-cyclone-1.1/demos/bottle/README0000644000175000017500000000142212124336260016630 0ustar lunarlunarThis is a complete cyclone.bottle demo. For a basic hello world, check out the helloworld_bottle.py demo. The idea is to show how easy it is to create simple bottle-like apps, and still capture all the functionality such as database support, setting a parent class (the BaseHandler) for your request handlers and integrating with other types of handlers such as XmlRPC and WebSocket. The authentication mechanism is similar to the one used in httpauthdemo_redis.py and require that the "cyclone:user" key exists in redis. Basically, run the redis server and use the redis client to set up a fake user account: $ redis-cli set cyclone:root 123 Then authenticate yourself at http://localhost:8888 as root/123. XmlRPC functionality can be tested by executing the xmlrpc_client.py. python-cyclone-1.1/demos/bottle/bottledemo.py0000755000175000017500000000723012124336260020466 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cyclone.escape import cyclone.redis import cyclone.sqlite import cyclone.util import cyclone.web import cyclone.websocket import cyclone.xmlrpc from cyclone.bottle import run, route from twisted.internet import defer from twisted.python import log class BaseHandler(cyclone.web.RequestHandler): @property def redisdb(self): return self.settings.db_handlers.redis def get_current_user(self): print "Getting user cookie" return self.get_secure_cookie("user") @route("/") def index(cli): cli.write('sign in') @route("/auth/login") def auth_login_page(cli): cli.write("""
username:
password:
""") @route("/auth/login", method="post") @defer.inlineCallbacks def auth_login(cli): usr = cli.get_argument("usr") pwd = cli.get_argument("pwd") try: redis_pwd = yield cli.redisdb.get("cyclone:%s" % usr) except Exception, e: log.msg("Redis failed to get('cyclone:%s'): %s" % (usr, str(e))) raise cyclone.web.HTTPError(503) # Service Unavailable if pwd != str(redis_pwd): cli.write("Invalid user or password.
" 'try again') else: cli.set_secure_cookie("user", usr) cli.redirect(cli.get_argument("next", "/private")) @route("/auth/logout") @cyclone.web.authenticated def auth_logout(cli): cli.clear_cookie("user") cli.redirect("/") @route("/private") @cyclone.web.authenticated def private(cli): cli.write("Hi, %s
" % cli.current_user) cli.write("""logout""") class WebSocketHandler(cyclone.websocket.WebSocketHandler): def connectionMade(self, *args, **kwargs): print "connection made:", args, kwargs def messageReceived(self, message): self.sendMessage("echo: %s" % message) def connectionLost(self, why): print "connection lost:", why class XmlrpcHandler(cyclone.xmlrpc.XmlrpcRequestHandler): allowNone = True def xmlrpc_echo(self, text): return text try: raise Exception("COMMENT_THIS_LINE_AND_LOG_TO_DAILY_FILE") from twisted.python.logfile import DailyLogFile logFile = DailyLogFile.fromFullPath("server.log") print("Logging to daily log file: server.log") except Exception, e: import sys logFile = sys.stdout run(host="127.0.0.1", port=8888, log=logFile, debug=True, static_path="./static", template_path="./template", locale_path="./locale", login_url="/auth/login", cookie_secret="32oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", base_handler=BaseHandler, db_handlers=cyclone.util.ObjectDict( #sqlite=cyclone.sqlite.InlineSQLite(":memory:"), redis=cyclone.redis.lazyConnectionPool(), ), more_handlers=[ (r"/websocket", WebSocketHandler), (r"/xmlrpc", XmlrpcHandler), ]) python-cyclone-1.1/demos/chat/0000755000175000017500000000000012124336260015377 5ustar lunarlunarpython-cyclone-1.1/demos/chat/chatdemo.py0000755000175000017500000001120412124336260017536 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import cyclone.web import cyclone.auth import cyclone.escape from twisted.python import log from twisted.internet import reactor import os.path import uuid class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", MainHandler), (r"/auth/login", AuthLoginHandler), (r"/auth/logout", AuthLogoutHandler), (r"/a/message/new", MessageNewHandler), (r"/a/message/updates", MessageUpdatesHandler), ] settings = dict( cookie_secret="43oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", login_url="/auth/login", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, autoescape=None, ) cyclone.web.Application.__init__(self, handlers, **settings) class BaseHandler(cyclone.web.RequestHandler): def get_current_user(self): user_json = self.get_secure_cookie("user") if not user_json: return None return cyclone.escape.json_decode(user_json) class MainHandler(BaseHandler): @cyclone.web.authenticated def get(self): self.render("index.html", messages=MessageMixin.cache) class MessageMixin(object): waiters = [] cache = [] cache_size = 200 def wait_for_messages(self, callback, cursor=None): cls = MessageMixin if cursor: index = 0 for i in xrange(len(cls.cache)): index = len(cls.cache) - i - 1 if cls.cache[index]["id"] == cursor: break recent = cls.cache[index + 1:] if recent: callback(recent) return cls.waiters.append(callback) def new_messages(self, messages): cls = MessageMixin log.msg("Sending new message to %r listeners" % len(cls.waiters)) for callback in cls.waiters: try: callback(messages) except: log.err() cls.waiters = [] cls.cache.extend(messages) if len(cls.cache) > self.cache_size: cls.cache = cls.cache[-self.cache_size:] class MessageNewHandler(BaseHandler, MessageMixin): @cyclone.web.authenticated def post(self): message = { "id": str(uuid.uuid4()), "from": self.current_user["first_name"], "body": self.get_argument("body"), } message["html"] = self.render_string("message.html", message=message) if self.get_argument("next", None): self.redirect(self.get_argument("next")) else: self.write(message) self.new_messages([message]) class MessageUpdatesHandler(BaseHandler, MessageMixin): @cyclone.web.authenticated @cyclone.web.asynchronous def post(self): cursor = self.get_argument("cursor", None) self.wait_for_messages(self.on_new_messages, cursor=cursor) def on_new_messages(self, messages): # Closed client connection #if self.request.connection.stream.closed(): #return self.finish(dict(messages=messages)) class AuthLoginHandler(BaseHandler, cyclone.auth.GoogleMixin): @cyclone.web.asynchronous def get(self): if self.get_argument("openid.mode", None): self.get_authenticated_user(self._on_auth) return self.authenticate_redirect(ax_attrs=["name"]) def _on_auth(self, user): if not user: raise cyclone.web.HTTPError(500, "Google auth failed") self.set_secure_cookie("user", cyclone.escape.json_encode(user)) self.redirect("/") class AuthLogoutHandler(BaseHandler): def get(self): self.clear_cookie("user") self.write("You are now logged out") def main(): reactor.listenTCP(8888, Application()) reactor.run() if __name__ == "__main__": log.startLogging(sys.stdout) main() python-cyclone-1.1/demos/chat/templates/0000755000175000017500000000000012124336260017375 5ustar lunarlunarpython-cyclone-1.1/demos/chat/templates/index.html0000644000175000017500000000241512124336260021374 0ustar lunarlunar Tornado Chat Demo
{% for message in messages %} {% module Template("message.html", message=message) %} {% end %}
{{ xsrf_form_html() }}
python-cyclone-1.1/demos/chat/templates/message.html0000644000175000017500000000017112124336260021706 0ustar lunarlunar
{{ escape(message["from"]) }}: {{ escape(message["body"]) }}
python-cyclone-1.1/demos/chat/static/0000755000175000017500000000000012124336260016666 5ustar lunarlunarpython-cyclone-1.1/demos/chat/static/chat.js0000644000175000017500000000720112124336260020143 0ustar lunarlunar// Copyright 2009 FriendFeed // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. $(document).ready(function() { if (!window.console) window.console = {}; if (!window.console.log) window.console.log = function() {}; $("#messageform").live("submit", function() { newMessage($(this)); return false; }); $("#messageform").live("keypress", function(e) { if (e.keyCode == 13) { newMessage($(this)); return false; } }); $("#message").select(); updater.poll(); }); function newMessage(form) { var message = form.formToDict(); var disabled = form.find("input[type=submit]"); disabled.disable(); $.postJSON("/a/message/new", message, function(response) { updater.showMessage(response); if (message.id) { form.parent().remove(); } else { form.find("input[type=text]").val("").select(); disabled.enable(); } }); } function getCookie(name) { var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); return r ? r[1] : undefined; } jQuery.postJSON = function(url, args, callback) { args._xsrf = getCookie("_xsrf"); $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST", success: function(response) { if (callback) callback(eval("(" + response + ")")); }, error: function(response) { console.log("ERROR:", response) }}); }; jQuery.fn.formToDict = function() { var fields = this.serializeArray(); var json = {} for (var i = 0; i < fields.length; i++) { json[fields[i].name] = fields[i].value; } if (json.next) delete json.next; return json; }; jQuery.fn.disable = function() { this.enable(false); return this; }; jQuery.fn.enable = function(opt_enable) { if (arguments.length && !opt_enable) { this.attr("disabled", "disabled"); } else { this.removeAttr("disabled"); } return this; }; var updater = { errorSleepTime: 500, cursor: null, poll: function() { var args = {"_xsrf": getCookie("_xsrf")}; if (updater.cursor) args.cursor = updater.cursor; $.ajax({url: "/a/message/updates", type: "POST", dataType: "text", data: $.param(args), success: updater.onSuccess, error: updater.onError}); }, onSuccess: function(response) { try { updater.newMessages(eval("(" + response + ")")); } catch (e) { updater.onError(); return; } updater.errorSleepTime = 500; window.setTimeout(updater.poll, 0); }, onError: function(response) { updater.errorSleepTime *= 2; console.log("Poll error; sleeping for", updater.errorSleepTime, "ms"); window.setTimeout(updater.poll, updater.errorSleepTime); }, newMessages: function(response) { if (!response.messages) return; updater.cursor = response.cursor; var messages = response.messages; updater.cursor = messages[messages.length - 1].id; console.log(messages.length, "new messages, cursor:", updater.cursor); for (var i = 0; i < messages.length; i++) { updater.showMessage(messages[i]); } }, showMessage: function(message) { var existing = $("#m" + message.id); if (existing.length > 0) return; var node = $(message.html); node.hide(); $("#inbox").append(node); node.slideDown(); } }; python-cyclone-1.1/demos/chat/static/chat.css0000644000175000017500000000173512124336260020325 0ustar lunarlunar/* * Copyright 2009 FriendFeed * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ body { background: white; margin: 10px; } body, input { font-family: sans-serif; font-size: 10pt; color: black; } table { border-collapse: collapse; border: 0; } td { border: 0; padding: 0; } #body { position: absolute; bottom: 10px; left: 10px; } #input { margin-top: 0.5em; } #inbox .message { padding-top: 0.25em; } #nav { float: right; z-index: 99; } python-cyclone-1.1/demos/httpauth/0000755000175000017500000000000012124336260016321 5ustar lunarlunarpython-cyclone-1.1/demos/httpauth/httpauthdemo_redis.py0000755000175000017500000000560712124336260022602 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import base64 import functools import sys import cyclone.redis import cyclone.web from twisted.python import log from twisted.internet import defer, reactor class Application(cyclone.web.Application): def __init__(self): # Defaults to localhost:6379, dbid=0 redisdb = cyclone.redis.lazyConnectionPool() handlers = [ (r"/", IndexHandler, dict(redisdb=redisdb)), ] cyclone.web.Application.__init__(self, handlers, debug=True) def HTTPBasic(method): @defer.inlineCallbacks @functools.wraps(method) def wrapper(self, *args, **kwargs): msg = None if "Authorization" in self.request.headers: auth_type, data = self.request.headers["Authorization"].split() try: assert auth_type == "Basic" usr, pwd = base64.b64decode(data).split(":", 1) redis_pwd = yield self.redisdb.get("cyclone:%s" % usr) assert pwd == str(redis_pwd) # it may come back as an int! except AssertionError: msg = "Authentication failed" except cyclone.redis.ConnectionError, e: # There's nothing we can do here. Just wait for the # connection to resume. log.msg("Redis is unavailable: %s" % e) raise cyclone.web.HTTPError(503) # Service Unavailable else: msg = "Authentication required" if msg: raise cyclone.web.HTTPAuthenticationRequired( log_message=msg, auth_type="Basic", realm="DEMO") else: self._current_user = usr yield defer.maybeDeferred(method, self, *args, **kwargs) return wrapper class IndexHandler(cyclone.web.RequestHandler): def initialize(self, redisdb): self.redisdb = redisdb @HTTPBasic def get(self): self.write("Hi, %s." % self._current_user) def main(): log.startLogging(sys.stdout) log.msg(">>>> Set the password from command line: " "redis-cli set cyclone:root 123") log.msg(">>>> Then authenticate as root/123 from the browser") reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/httpauth/README0000644000175000017500000000264312124336260017206 0ustar lunarlunarHTTP Basic Authentication httpauthdemo.py: A very simple, pretty much useless example. The idea is to show how to build a decorator to request valid credentials, and send HTTP "401 Unauthorized" when necessary. httpauthdemo_redis.py: This is more of a real-world example, authenticating against a database, in this case, redis. You can easily port it to standard RDBMs such as MySQL, PostgreSQL and others, using twisted.enterprise.adbapi. Again, it does so via the decorator, which extract credentials from Redis and compare them against what people typed on their browser. If you run this example without a redis-server, it will always return HTTP 503 "Service Unavailable" due to lack of communication with the database backend. Once you start redis, it will automatically connect to it. Make sure you create a user in redis to test the authentication system. From the command line, run: $ redis-cli set cyclone:root 123 Then point your browser to http://localhost:8888 and authenticate as root/123. httpauthdemo_mongo.py: Same as the above, but for the MongoDB database. Make sure you have txredis installed, otherwise go grab it from GitHub: https://github.com/fiorix/mongo-async-python-driver Create a new user: curl -D - -d "username=root&password=123" http://localhost:8888/createuser Then point your browser to http://localhost:8888 and authenticate as root/123. Have fun! python-cyclone-1.1/demos/httpauth/httpauthdemo_mongo.py0000755000175000017500000000664312124336260022614 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import base64 import functools import sys import cyclone.redis import cyclone.web from twisted.python import log from twisted.internet import defer, reactor try: import txmongo except ImportError: print("You need txmongo: " "https://github.com/fiorix/mongo-async-python-driver") sys.exit(1) class Application(cyclone.web.Application): def __init__(self): mongodb = txmongo.lazyMongoConnectionPool() handlers = [ (r"/", IndexHandler, dict(mongodb=mongodb)), (r"/createuser", CreateUserHandler, dict(mongodb=mongodb)), ] cyclone.web.Application.__init__(self, handlers, debug=True) def HTTPBasic(method): @defer.inlineCallbacks @functools.wraps(method) def wrapper(self, *args, **kwargs): try: auth_type, auth_data = \ self.request.headers["Authorization"].split() assert auth_type == "Basic" usr, pwd = base64.b64decode(auth_data).split(":", 1) except: raise cyclone.web.HTTPAuthenticationRequired try: # search for user under the "cyclonedb.users" collection response = yield self.mongodb.cyclonedb.users.find_one( {"usr": usr, "pwd": pwd}, fields=["usr"]) mongo_usr = response.get("usr") except Exception, e: log.msg("MongoDB failed to find(): %s" % str(e)) raise cyclone.web.HTTPError(503) # Service Unavailable if usr != mongo_usr: raise cyclone.web.HTTPAuthenticationRequired else: self._current_user = usr defer.returnValue(method(self, *args, **kwargs)) return wrapper class IndexHandler(cyclone.web.RequestHandler): def initialize(self, mongodb): self.mongodb = mongodb @HTTPBasic def get(self): self.write("Hi, %s." % self._current_user) class CreateUserHandler(cyclone.web.RequestHandler): def initialize(self, mongodb): self.mongodb = mongodb @defer.inlineCallbacks def post(self): usr = self.get_argument("username") pwd = self.get_argument("password") try: # create user under the "cyclonedb.users" collection ObjId = yield self.mongodb.cyclonedb.users.update( {"usr": usr}, {"usr": usr, "pwd": pwd}, upsert=True, safe=True) except Exception, e: log.msg("MongoDB failed to upsert(): %s" % str(e)) raise cyclone.web.HTTPError(503) # Service Unavailable self.write("User created. ObjId=%s\r\n" % ObjId) def main(): log.startLogging(sys.stdout) reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/httpauth/httpauthdemo.py0000755000175000017500000000416312124336260021410 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import base64 import functools import sys import cyclone.web from twisted.python import log from twisted.internet import reactor class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", IndexHandler), ] cyclone.web.Application.__init__(self, handlers, debug=True) def HTTPBasic(method): @functools.wraps(method) def wrapper(self, *args, **kwargs): msg = None if "Authorization" in self.request.headers: auth_type, data = self.request.headers["Authorization"].split() try: assert auth_type == "Basic" usr, pwd = base64.b64decode(data).split(":", 1) assert usr == "root@localhost" assert pwd == "123" except AssertionError: msg = "Authentication failed" else: msg = "Authentication required" if msg: raise cyclone.web.HTTPAuthenticationRequired( log_message=msg, auth_type="Basic", realm="DEMO") else: self._current_user = usr return method(self, *args, **kwargs) return wrapper class IndexHandler(cyclone.web.RequestHandler): @HTTPBasic def get(self): self.write("Hi, %s." % self._current_user) def main(): log.startLogging(sys.stdout) reactor.listenTCP(8888, Application(), interface="0.0.0.0") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/sse/0000755000175000017500000000000012124336260015252 5ustar lunarlunarpython-cyclone-1.1/demos/sse/ssedemo.py0000755000175000017500000000532112124336260017267 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import cyclone.sse import cyclone.web from twisted.internet import protocol from twisted.internet import reactor from twisted.protocols import telnet from twisted.python import log class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", cyclone.web.RedirectHandler, {"url": "/static/index.html"}), (r"/live", LiveHandler), ] settings = dict( debug=True, static_path="./static", template_path="./template", ) cyclone.web.Application.__init__(self, handlers, **settings) class StarWarsMixin(object): mbuffer = "" waiters = [] def subscribe(self, client): StarWarsMixin.waiters.append(client) def unsubscribe(self, client): StarWarsMixin.waiters.remove(client) def broadcast(self, message): cls = StarWarsMixin chunks = (self.mbuffer + message.replace("\x1b[J", "")).split("\x1b[H") self.mbuffer = "" for chunk in chunks: if len(chunk) == 985: chunk = chunk.replace("\r\n", "
") log.msg("Sending new message to %r listeners" % \ len(cls.waiters)) for client in cls.waiters: try: client.sendEvent(chunk) except: log.err() else: self.mbuffer = chunk class LiveHandler(cyclone.sse.SSEHandler, StarWarsMixin): def bind(self): self.subscribe(self) def unbind(self): self.unsubscribe(self) class BlinkenlightsProtocol(telnet.Telnet, StarWarsMixin): def dataReceived(self, data): self.broadcast(data) def main(): log.startLogging(sys.stdout) blinkenlights = protocol.ReconnectingClientFactory() blinkenlights.protocol = BlinkenlightsProtocol reactor.connectTCP("towel.blinkenlights.nl", 23, blinkenlights) reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/sse/README0000644000175000017500000000061212124336260016131 0ustar lunarlunarServer Sent Events Demo SSE is a mechanism akin to comet but with fine grained control over the data. You can name events, send structured and partial data and set the proper timeout. To understand better, check http://www.html5rocks.com/en/tutorials/eventsource/basics/ The client side gets really simple than controlling a COMET resource using js, by using the window.EventSource object. python-cyclone-1.1/demos/sse/static/0000755000175000017500000000000012124336260016541 5ustar lunarlunarpython-cyclone-1.1/demos/sse/static/index.html0000644000175000017500000000127312124336260020541 0ustar lunarlunar

Episode IV Live from towel.blinkenlights.nl


  
python-cyclone-1.1/demos/websocket/0000755000175000017500000000000012124336260016446 5ustar lunarlunarpython-cyclone-1.1/demos/websocket/chat/0000755000175000017500000000000012124336260017365 5ustar lunarlunarpython-cyclone-1.1/demos/websocket/chat/chatdemo.py0000755000175000017500000001057212124336260021533 0ustar lunarlunar#!/usr/bin/env python # # Copyright 2009 Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Simplified chat demo for websockets. Authentication, error handling, etc are left as an exercise for the reader :) """ import os.path import uuid import sys import time from collections import defaultdict from twisted.python import log from twisted.internet import reactor, task import cyclone.escape import cyclone.web import cyclone.websocket class Application(cyclone.web.Application): def __init__(self): stats = Stats() handlers = [ (r"/", MainHandler, dict(stats=stats)), (r"/stats", StatsPageHandler), (r"/statssocket", StatsSocketHandler, dict(stats=stats)), (r"/chatsocket", ChatSocketHandler, dict(stats=stats)), ] settings = dict( cookie_secret="43oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, autoescape=None, ) cyclone.web.Application.__init__(self, handlers, **settings) class MainHandler(cyclone.web.RequestHandler): def initialize(self, stats): self.stats = stats def get(self): self.stats.newVisit() self.render("index.html", messages=ChatSocketHandler.cache) class ChatSocketHandler(cyclone.websocket.WebSocketHandler): waiters = set() cache = [] cache_size = 200 def initialize(self, stats): self.stats = stats def connectionMade(self): ChatSocketHandler.waiters.add(self) self.stats.newChatter() def connectionLost(self, reason): ChatSocketHandler.waiters.remove(self) self.stats.lostChatter() @classmethod def update_cache(cls, chat): cls.cache.append(chat) if len(cls.cache) > cls.cache_size: cls.cache = cls.cache[-cls.cache_size:] @classmethod def send_updates(cls, chat): log.msg("sending message to %d waiters" % len(cls.waiters)) for waiter in cls.waiters: try: waiter.sendMessage(chat) except Exception, e: log.err("Error sending message. %s" % str(e)) def messageReceived(self, message): log.msg("got message %s" % message) parsed = cyclone.escape.json_decode(message) chat = { "id": str(uuid.uuid4()), "body": parsed["body"], } chat["html"] = self.render_string("message.html", message=chat) ChatSocketHandler.update_cache(chat) ChatSocketHandler.send_updates(chat) class StatsSocketHandler(cyclone.websocket.WebSocketHandler): def initialize(self, stats): self.stats = stats self._updater = task.LoopingCall(self._sendData) def connectionMade(self): self._updater.start(2) def connectionLost(self, reason): self._updater.stop() def _sendData(self): data = dict(visits=self.stats.todaysVisits(), chatters=self.stats.chatters) self.sendMessage(cyclone.escape.json_encode(data)) class Stats(object): def __init__(self): self.visits = defaultdict(int) self.chatters = 0 def todaysVisits(self): today = time.localtime() key = time.strftime('%Y%m%d', today) return self.visits[key] def newChatter(self): self.chatters += 1 def lostChatter(self): self.chatters -= 1 def newVisit(self): today = time.localtime() key = time.strftime('%Y%m%d', today) self.visits[key] += 1 class StatsPageHandler(cyclone.web.RequestHandler): def get(self): self.render("stats.html") def main(): reactor.listenTCP(8888, Application()) reactor.run() if __name__ == "__main__": log.startLogging(sys.stdout) main() python-cyclone-1.1/demos/websocket/chat/templates/0000755000175000017500000000000012124336260021363 5ustar lunarlunarpython-cyclone-1.1/demos/websocket/chat/templates/index.html0000644000175000017500000000253412124336260023364 0ustar lunarlunar Cyclone Chat Demo
{% for message in messages %} {% include "message.html" %} {% end %}
{{ xsrf_form_html() }}
site statistics
python-cyclone-1.1/demos/websocket/chat/templates/message.html0000644000175000017500000000011312124336260023670 0ustar lunarlunar
{{ message["body"] }}
python-cyclone-1.1/demos/websocket/chat/templates/stats.html0000644000175000017500000000503312124336260023410 0ustar lunarlunar Site statistics
python-cyclone-1.1/demos/websocket/chat/static/0000755000175000017500000000000012124336260020654 5ustar lunarlunarpython-cyclone-1.1/demos/websocket/chat/static/chat.js0000644000175000017500000000436112124336260022135 0ustar lunarlunar// Copyright 2009 FriendFeed // // Licensed under the Apache License, Version 2.0 (the "License"); you may // not use this file except in compliance with the License. You may obtain // a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations // under the License. $(document).ready(function() { if (!window.console) window.console = {}; if (!window.console.log) window.console.log = function() {}; $("#messageform").live("submit", function() { newMessage($(this)); return false; }); $("#messageform").live("keypress", function(e) { if (e.keyCode == 13) { newMessage($(this)); return false; } }); $("#message").select(); updater.start(); }); $(window).unload(function() { updater.stop() }); function newMessage(form) { var message = form.formToDict(); updater.socket.send(JSON.stringify(message)); form.find("input[type=text]").val("").select(); } jQuery.fn.formToDict = function() { var fields = this.serializeArray(); var json = {} for (var i = 0; i < fields.length; i++) { json[fields[i].name] = fields[i].value; } if (json.next) delete json.next; return json; }; var updater = { socket: null, start: function() { if ("WebSocket" in window) { updater.socket = new WebSocket("ws://localhost:8888/chatsocket"); } else { updater.socket = new MozWebSocket("ws://localhost:8888/chatsocket"); } updater.socket.onmessage = function(event) { console.log(event.data); updater.showMessage(JSON.parse(event.data)); } }, stop: function() { updater.socket.close(); updater.socket = null; }, showMessage: function(message) { var existing = $("#m" + message.id); if (existing.length > 0) return; var node = $(message.html); node.hide(); $("#inbox").append(node); node.slideDown(); } }; python-cyclone-1.1/demos/websocket/chat/static/chat.css0000644000175000017500000000173512124336260022313 0ustar lunarlunar/* * Copyright 2009 FriendFeed * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ body { background: white; margin: 10px; } body, input { font-family: sans-serif; font-size: 10pt; color: black; } table { border-collapse: collapse; border: 0; } td { border: 0; padding: 0; } #body { position: absolute; bottom: 10px; left: 10px; } #input { margin-top: 0.5em; } #inbox .message { padding-top: 0.25em; } #nav { float: right; z-index: 99; } python-cyclone-1.1/demos/locale/0000755000175000017500000000000012124336260015717 5ustar lunarlunarpython-cyclone-1.1/demos/locale/localedemo.py0000755000175000017500000000515212124336260020403 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import cyclone.locale import cyclone.web from twisted.python import log from twisted.internet import reactor class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", IndexHandler), (r"/hello", HelloHandler), ] settings = dict( debug=True, template_path="./frontend/template", ) cyclone.locale.load_gettext_translations("./frontend/locale", "mytest") cyclone.web.Application.__init__(self, handlers, **settings) class BaseHandler(cyclone.web.RequestHandler): def get_user_locale(self): lang = self.get_cookie("lang", default="en_US") return cyclone.locale.get(lang) class IndexHandler(BaseHandler): def _apples(self): try: return int(self.get_argument("apples", 1)) except: return 1 def get(self): self.render("index.html", apples=self._apples(), locale=self.locale.code, languages=cyclone.locale.get_supported_locales()) def post(self): lang = self.get_argument("lang") # assert lang in cyclone.locale.get_supported_locales() # Either set self._locale or override get_user_locale() # self._locale = cyclone.locale.get(lang) # self.render(...) self.set_cookie("lang", lang) self.redirect("/?apples=%d" % self._apples()) class HelloHandler(BaseHandler): def get(self): # Test with es_ES or pt_BR: # curl -D - -H "Cookie: lang=es_ES" http://localhost:8888/hello _ = self.locale.translate msg = _("hello world") text = self.render_string("hello.txt", msg=msg) self.set_header("Content-Type", "text/plain") self.write(text) def main(): log.startLogging(sys.stdout) reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/locale/mytest_es_ES.po0000644000175000017500000000245012124336260020663 0ustar lunarlunar# cyclone locale demo # Copyright (C) 2010 Alexandre Fiori # This file is distributed under the same license as the cyclone package. # Alexandre Fiori , 2010 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2010-01-20 12:19-0200\n" "PO-Revision-Date: 2010-01-20 12:21-0300\n" "Last-Translator: Alexandre Fiori \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=n != 1;\n" #: localedemo.py:76 msgid "hello world" msgstr "hola mundo" #: frontend/template/hello.txt:2 #, python-format msgid "%s, translated" msgstr "%s, traducido" #: frontend/template/index.html:5 frontend/template/index.html:25 msgid "cyclone locale demo" msgstr "demonstración de internacionalización de cyclone" #: frontend/template/index.html:29 msgid "Please select your language:" msgstr "Por favor, seleccione su idioma:" #: frontend/template/index.html:35 msgid "How many apples?" msgstr "Cuantas manzanas?" #: frontend/template/index.html:39 msgid "Submit" msgstr "Enviar" #: frontend/template/index.html:42 #, python-format msgid "One apple" msgid_plural "%(count)d apples" msgstr[0] "Una manzana" msgstr[1] "%(count)d manzanas" python-cyclone-1.1/demos/locale/README0000644000175000017500000000235312124336260016602 0ustar lunarlunarGenerate pot translation file using "mytest" as locale domain: xgettext --language=Python --keyword=_:1,2 -d mytest localedemo.py frontend/template/* When using translatable strings in templates, variable arguments must be placed outside of the _() function. Example: _("foobar %s" % x) # wrong _("foobar %s") % x # correct If xgettext fails parsing html elements like this: Try --language=Php, or pipe it to localefix.py: #!/usr/bin/env python # localefix.py import re import sys if __name__ == "__main__": try: filename = sys.argv[1] assert filename != "-" fd = open(filename) except: fd = sys.stdin line_re = re.compile(r'="([^"]+)"') for line in fd: line = line_re.sub(r"=\\1", line) sys.stdout.write(line) fd.close() Merge against existing pot file: msgmerge old.po mytest.po > new.po mv new.po mytest.po Compile: msgfmt mytest.po -o locale/{lang}/LC_MESSAGES/mytest.mo Compile files in this demo: msgfmt mytest_pt_BR.po -o frontend/locale/pt_BR/LC_MESSAGES/mytest.mo msgfmt mytest_es_ES.po -o frontend/locale/es_ES/LC_MESSAGES/mytest.mo python-cyclone-1.1/demos/locale/frontend/0000755000175000017500000000000012124336260017536 5ustar lunarlunarpython-cyclone-1.1/demos/locale/frontend/locale/0000755000175000017500000000000012124336260020775 5ustar lunarlunarpython-cyclone-1.1/demos/locale/frontend/locale/es_ES/0000755000175000017500000000000012124336260021773 5ustar lunarlunarpython-cyclone-1.1/demos/locale/frontend/locale/es_ES/LC_MESSAGES/0000755000175000017500000000000012124336260023560 5ustar lunarlunarpython-cyclone-1.1/demos/locale/frontend/locale/es_ES/LC_MESSAGES/mytest.mo0000644000175000017500000000147312124336260025447 0ustar lunarlunarÞ•\ œÈÉØé!( <MH –¤¶ Õö2ý 0%s, translatedHow many apples?One apple%(count)d applesPlease select your language:Submitcyclone locale demohello worldProject-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2010-01-20 12:19-0200 PO-Revision-Date: 2010-01-20 12:21-0300 Last-Translator: Alexandre Fiori Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=INTEGER; plural=n != 1; %s, traducidoCuantas manzanas?Una manzana%(count)d manzanasPor favor, seleccione su idioma:Enviardemonstración de internacionalización de cyclonehola mundopython-cyclone-1.1/demos/locale/frontend/locale/pt_BR/0000755000175000017500000000000012124336260022003 5ustar lunarlunarpython-cyclone-1.1/demos/locale/frontend/locale/pt_BR/LC_MESSAGES/0000755000175000017500000000000012124336260023570 5ustar lunarlunarpython-cyclone-1.1/demos/locale/frontend/locale/pt_BR/LC_MESSAGES/mytest.mo0000644000175000017500000000147112124336260025455 0ustar lunarlunarÞ•\ œÈÉØé!( <MH –¤µ!Òô2û .%s, translatedHow many apples?One apple%(count)d applesPlease select your language:Submitcyclone locale demohello worldProject-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2010-01-20 12:19-0200 PO-Revision-Date: 2010-01-20 12:21-0300 Last-Translator: Alexandre Fiori Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=INTEGER; plural=n != 1; %s, traduzidoQuantas maçãs?Uma maçã%(count)d maçãsPor favor, selecione sua língua:Enviardemonstração de internacionalização do cycloneolá mundopython-cyclone-1.1/demos/locale/frontend/template/0000755000175000017500000000000012124336260021351 5ustar lunarlunarpython-cyclone-1.1/demos/locale/frontend/template/index.html0000644000175000017500000000246512124336260023355 0ustar lunarlunar {{_("cyclone locale demo")}}

{{_("cyclone locale demo")}}



{{_("One apple", "%(count)d apples", apples) % {"count":apples} }}

python-cyclone-1.1/demos/locale/frontend/template/hello.txt0000644000175000017500000000006212124336260023213 0ustar lunarlunarSimple text file. {{ _("%s, translated") % msg }} python-cyclone-1.1/demos/locale/mytest_pt_BR.po0000644000175000017500000000244612124336260020700 0ustar lunarlunar# cyclone locale demo # Copyright (C) 2010 Alexandre Fiori # This file is distributed under the same license as the cyclone package. # Alexandre Fiori , 2010 # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2010-01-20 12:19-0200\n" "PO-Revision-Date: 2010-01-20 12:21-0300\n" "Last-Translator: Alexandre Fiori \n" "Language-Team: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=INTEGER; plural=n != 1;\n" #: localedemo.py:76 msgid "hello world" msgstr "olá mundo" #: frontend/template/hello.txt:2 #, python-format msgid "%s, translated" msgstr "%s, traduzido" #: frontend/template/index.html:5 frontend/template/index.html:25 msgid "cyclone locale demo" msgstr "demonstração de internacionalização do cyclone" #: frontend/template/index.html:29 msgid "Please select your language:" msgstr "Por favor, selecione sua língua:" #: frontend/template/index.html:35 msgid "How many apples?" msgstr "Quantas maçãs?" #: frontend/template/index.html:39 msgid "Submit" msgstr "Enviar" #: frontend/template/index.html:42 #, python-format msgid "One apple" msgid_plural "%(count)d apples" msgstr[0] "Uma maçã" msgstr[1] "%(count)d maçãs" python-cyclone-1.1/demos/s3/0000755000175000017500000000000012124336260015005 5ustar lunarlunarpython-cyclone-1.1/demos/s3/README0000644000175000017500000000073612124336260015673 0ustar lunarlunarPort of http://github.com/facebook/tornado/raw/master/tornado/s3server.py (s3 clone on tornado) TODO: - plug auth module - plug riak, redis or another storage besides FS - be awesome. RUNNING $ twistd -ny se.tac TESTING: create bucket: $ curl --request PUT "http://localhost:4000/meh/" put some data (README file): curl --data "@README" --request PUT --header "Content-Type: text/plain" "http://localhost:4000/meh/README" retrieve: curl http://localhost:4000/meh/README python-cyclone-1.1/demos/s3/se.tac0000644000175000017500000000060412124336260016105 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # twistd -ny s3.tac # gleicon moraes (http://zenmachine.wordpress.com | http://github.com/gleicon) SERVER_PORT = 4000 import s3server from twisted.application import service, internet application = service.Application("s3") srv = internet.TCPServer(SERVER_PORT, s3server.S3Application(root_directory="/tmp/s3")) srv.setServiceParent(application) python-cyclone-1.1/demos/s3/s3server.py0000644000175000017500000002234612124336260017142 0ustar lunarlunar#!/usr/bin/env python # # Copyright 2009 Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # port to cyclone: took out ioloop initialization, fixed imports and created # a .tac file. # gleicon 04/10 """Implementation of an S3-like storage server based on local files. Useful to test features that will eventually run on S3, or if you want to run something locally that was once running on S3. We don't support all the features of S3, but it does work with the standard S3 client for the most basic semantics. To use the standard S3 client with this module: c = S3.AWSAuthConnection("", "", server="localhost", port=8888, is_secure=False) c.create_bucket("mybucket") c.put("mybucket", "mykey", "a value") print c.get("mybucket", "mykey").body """ from cyclone import escape from cyclone import web from twisted.python import log import datetime import bisect import hashlib import os import os.path import urllib class S3Application(web.Application): """Implementation of an S3-like storage server based on local files. If bucket depth is given, we break files up into multiple directories to prevent hitting file system limits for number of files in each directories. 1 means one level of directories, 2 means 2, etc. """ def __init__(self, root_directory, bucket_depth=0): web.Application.__init__(self, [ (r"/", RootHandler), (r"/([^/]+)/(.+)", ObjectHandler), (r"/([^/]+)/", BucketHandler), ]) self.directory = os.path.abspath(root_directory) if not os.path.exists(self.directory): os.makedirs(self.directory) self.bucket_depth = bucket_depth class BaseRequestHandler(web.RequestHandler): # SUPPORTED_METHODS = ("PUT", "GET", "DELETE", "HEAD") def render_xml(self, value): assert isinstance(value, dict) and len(value) == 1 self.set_header("Content-Type", "application/xml; charset=UTF-8") name = value.keys()[0] parts = [] parts.append('<' + escape.utf8(name) + ' xmlns="http://doc.s3.amazonaws.com/2006-03-01">') self._render_parts(value.values()[0], parts) parts.append('') self.finish('\n' + ''.join(parts)) def _render_parts(self, value, parts=[]): if isinstance(value, basestring): parts.append(escape.xhtml_escape(value)) elif isinstance(value, int) or isinstance(value, long): parts.append(str(value)) elif isinstance(value, datetime.datetime): parts.append(value.strftime("%Y-%m-%dT%H:%M:%S.000Z")) elif isinstance(value, dict): for name, subvalue in value.iteritems(): if not isinstance(subvalue, list): subvalue = [subvalue] for subsubvalue in subvalue: parts.append('<' + escape.utf8(name) + '>') self._render_parts(subsubvalue, parts) parts.append('') else: raise Exception("Unknown S3 value type %r", value) def _object_path(self, bucket, object_name): if self.application.bucket_depth < 1: return os.path.abspath(os.path.join( self.application.directory, bucket, object_name)) hash = hashlib.md5(object_name).hexdigest() path = os.path.abspath(os.path.join( self.application.directory, bucket)) for i in range(self.application.bucket_depth): path = os.path.join(path, hash[:2 * (i + 1)]) return os.path.join(path, object_name) class RootHandler(BaseRequestHandler): def get(self): names = os.listdir(self.application.directory) buckets = [] for name in names: path = os.path.join(self.application.directory, name) info = os.stat(path) buckets.append({ "Name": name, "CreationDate": datetime.datetime.utcfromtimestamp( info.st_ctime), }) self.render_xml({"ListAllMyBucketsResult": { "Buckets": {"Bucket": buckets}, }}) class BucketHandler(BaseRequestHandler): def get(self, bucket_name): prefix = self.get_argument("prefix", u"") marker = self.get_argument("marker", u"") max_keys = int(self.get_argument("max-keys", 50000)) path = os.path.abspath(os.path.join(self.application.directory, bucket_name)) terse = int(self.get_argument("terse", 0)) if not path.startswith(self.application.directory) or \ not os.path.isdir(path): raise web.HTTPError(404) object_names = [] for root, dirs, files in os.walk(path): for file_name in files: object_names.append(os.path.join(root, file_name)) skip = len(path) + 1 for i in range(self.application.bucket_depth): skip += 2 * (i + 1) + 1 object_names = [n[skip:] for n in object_names] object_names.sort() contents = [] start_pos = 0 if marker: start_pos = bisect.bisect_right(object_names, marker, start_pos) if prefix: start_pos = bisect.bisect_left(object_names, prefix, start_pos) truncated = False for object_name in object_names[start_pos:]: if not object_name.startswith(prefix): break if len(contents) >= max_keys: truncated = True break object_path = self._object_path(bucket_name, object_name) c = {"Key": object_name} if not terse: info = os.stat(object_path) c.update({ "LastModified": datetime.datetime.utcfromtimestamp( info.st_mtime), "Size": info.st_size, }) contents.append(c) marker = object_name self.render_xml({"ListBucketResult": { "Name": bucket_name, "Prefix": prefix, "Marker": marker, "MaxKeys": max_keys, "IsTruncated": truncated, "Contents": contents, }}) def put(self, bucket_name): log.msg('bruxao') path = os.path.abspath(os.path.join( self.application.directory, bucket_name)) if not path.startswith(self.application.directory) or \ os.path.exists(path): raise web.HTTPError(403) os.makedirs(path) self.finish() def delete(self, bucket_name): path = os.path.abspath(os.path.join( self.application.directory, bucket_name)) if not path.startswith(self.application.directory) or \ not os.path.isdir(path): raise web.HTTPError(404) if len(os.listdir(path)) > 0: raise web.HTTPError(403) os.rmdir(path) self.set_status(204) self.finish() class ObjectHandler(BaseRequestHandler): def get(self, bucket, object_name): object_name = urllib.unquote(object_name) path = self._object_path(bucket, object_name) if not path.startswith(self.application.directory) or \ not os.path.isfile(path): raise web.HTTPError(404) info = os.stat(path) self.set_header("Content-Type", "application/unknown") self.set_header("Last-Modified", datetime.datetime.utcfromtimestamp( info.st_mtime)) object_file = open(path, "r") try: self.finish(object_file.read()) finally: object_file.close() def put(self, bucket, object_name): object_name = urllib.unquote(object_name) bucket_dir = os.path.abspath(os.path.join( self.application.directory, bucket)) if not bucket_dir.startswith(self.application.directory) or \ not os.path.isdir(bucket_dir): raise web.HTTPError(404) path = self._object_path(bucket, object_name) if not path.startswith(bucket_dir) or os.path.isdir(path): raise web.HTTPError(403) directory = os.path.dirname(path) if not os.path.exists(directory): os.makedirs(directory) object_file = open(path, "w") object_file.write(self.request.body) object_file.close() self.finish() def delete(self, bucket, object_name): object_name = urllib.unquote(object_name) path = self._object_path(bucket, object_name) if not path.startswith(self.application.directory) or \ not os.path.isfile(path): raise web.HTTPError(404) os.unlink(path) self.set_status(204) self.finish() python-cyclone-1.1/demos/auth/0000755000175000017500000000000012124336260015421 5ustar lunarlunarpython-cyclone-1.1/demos/auth/authdemo.py0000755000175000017500000000475212124336260017614 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import cyclone.auth import cyclone.escape import cyclone.web from twisted.python import log from twisted.internet import reactor class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", MainHandler), (r"/auth/login", AuthHandler), (r"/auth/logout", LogoutHandler), ] settings = dict( cookie_secret="32oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", debug=True, login_url="/auth/login", ) cyclone.web.Application.__init__(self, handlers, **settings) class BaseHandler(cyclone.web.RequestHandler): def get_current_user(self): user_json = self.get_secure_cookie("user") if not user_json: return None return cyclone.escape.json_decode(user_json) class MainHandler(BaseHandler): @cyclone.web.authenticated def get(self): name = cyclone.escape.xhtml_escape(self.current_user["name"]) self.write("Hello, " + name) self.write("

Log out") class AuthHandler(BaseHandler, cyclone.auth.GoogleMixin): @cyclone.web.asynchronous def get(self): if self.get_argument("openid.mode", None): self.get_authenticated_user(self._on_auth) return self.authenticate_redirect() def _on_auth(self, user): if not user: raise cyclone.web.HTTPError(500, "Google auth failed") self.set_secure_cookie("user", cyclone.escape.json_encode(user)) self.redirect("/") class LogoutHandler(BaseHandler): def get(self): self.clear_cookie("user") self.redirect("/") def main(): log.startLogging(sys.stdout) reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/pycket/0000755000175000017500000000000012124336260015757 5ustar lunarlunarpython-cyclone-1.1/demos/pycket/README.md0000644000175000017500000000060112124336260017233 0ustar lunarlunar# pycket Demo [pycket] is a session library, written for use with Redis or Memcached, and Tornado web server. It works equally well with cyclone. [pycket]: https://github.com/diogobaeder/pycket This demo shows how to set, get, and delete sessions stored persistently in redis (though it can easily be made to use memcached). See the [pycket] page for additional documentation. python-cyclone-1.1/demos/pycket/pycketdemo.py0000755000175000017500000000574312124336260020511 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import cyclone.auth import cyclone.escape import cyclone.web from twisted.python import log from twisted.internet import reactor from pycket.session import SessionMixin class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", MainHandler), (r"/auth/login", AuthHandler), (r"/auth/logout", LogoutHandler), ] settings = dict( cookie_secret="32oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", debug=True, login_url="/auth/login", logout_url="/auth/logout", ) settings['pycket'] = { 'engine': 'redis', 'storage': { 'host': 'localhost', 'port': 6379, 'db_sessions': 10, 'db_notifications': 11 } } cyclone.web.Application.__init__(self, handlers, **settings) class BaseHandler(cyclone.web.RequestHandler, SessionMixin): def get_current_user(self): user = self.session.get('user') if not user: return None return user class MainHandler(BaseHandler): @cyclone.web.authenticated def get(self): name = cyclone.escape.xhtml_escape(self.current_user) self.write("Hello, " + name) self.write("

Log out") class AuthHandler(BaseHandler, SessionMixin): def get(self): self.write('
' 'Enter your username: ' '
') def post(self): username = self.get_argument('username') if not username: self.write('
Enter your username: ' '' '' '
') else: self.session.set('user', username) self.redirect('/') class LogoutHandler(BaseHandler, SessionMixin): def get(self): self.session.delete('user') self.redirect("/") def main(): log.startLogging(sys.stdout) reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/ssl/0000755000175000017500000000000012124336260015261 5ustar lunarlunarpython-cyclone-1.1/demos/ssl/mkcert.sh0000755000175000017500000000051712124336260017110 0ustar lunarlunar#!/bin/bash echo -- key openssl genrsa -des3 -out server.key 1024 echo -- csr openssl req -new -key server.key -out server.csr echo -- remove passphrase cp server.key orig.server.key openssl rsa -in orig.server.key -out server.key echo -- generate crt openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt python-cyclone-1.1/demos/ssl/helloworld_ssl.py0000755000175000017500000000261112124336260020672 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cyclone.web import sys from twisted.internet import reactor from twisted.internet import ssl from twisted.python import log class MainHandler(cyclone.web.RequestHandler): def get(self): self.write("Hello, %s" % self.request.protocol) def main(): log.startLogging(sys.stdout) application = cyclone.web.Application([ (r"/", MainHandler) ]) interface = "127.0.0.1" reactor.listenTCP(8888, application, interface=interface) reactor.listenSSL(8443, application, ssl.DefaultOpenSSLContextFactory("server.key", "server.crt"), interface=interface) reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/ssl/README0000644000175000017500000000037712124336260016150 0ustar lunarlunarcreate a pair of certs: $ mkcert (some questions, write down your passphrase, etc) $ python helloworld_ssl.py point your browser to https://localhost:8443 try using cyclone's twisted plugin: $ twistd -n cyclone --ssl-app=helloworld_simple.Application python-cyclone-1.1/demos/ssl/helloworld_simple.py0000755000175000017500000000170512124336260021365 0ustar lunarlunar# coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # Run: twistd -n cyclone --ssl-app helloworld_simple.Application # For more info: twistd -n cyclone --help import cyclone.web class MainHandler(cyclone.web.RequestHandler): def get(self): self.write("Hello, %s" % self.request.protocol) Application = lambda: cyclone.web.Application([(r"/", MainHandler)]) python-cyclone-1.1/demos/rpc/0000755000175000017500000000000012124336260015244 5ustar lunarlunarpython-cyclone-1.1/demos/rpc/xmlrpc_client.py0000644000175000017500000000162312124336260020463 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import xmlrpclib srv = xmlrpclib.Server("http://localhost:8888/xmlrpc") print "echo:", srv.echo("hello world!") print "sort:", srv.sort(["foo", "bar"]) print "count:", srv.count(["foo", "bar"]) print "geoip_lookup:\n", srv.geoip_lookup("google.com") python-cyclone-1.1/demos/rpc/rpcdemo.py0000755000175000017500000000401612124336260017253 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import cyclone.httpclient import cyclone.jsonrpc import cyclone.xmlrpc from twisted.python import log from twisted.internet import defer, reactor class XmlrpcHandler(cyclone.xmlrpc.XmlrpcRequestHandler): allowNone = True def xmlrpc_echo(self, text): return text def xmlrpc_sort(self, items): return sorted(items) def xmlrpc_count(self, items): return len(items) @defer.inlineCallbacks def xmlrpc_geoip_lookup(self, address): result = yield cyclone.httpclient.fetch( "http://freegeoip.net/xml/%s" % address.encode("utf-8")) defer.returnValue(result.body) class JsonrpcHandler(cyclone.jsonrpc.JsonrpcRequestHandler): def jsonrpc_echo(self, text): return text def jsonrpc_sort(self, items): return sorted(items) def jsonrpc_count(self, items): return len(items) @defer.inlineCallbacks def jsonrpc_geoip_lookup(self, address): result = yield cyclone.httpclient.fetch( "http://freegeoip.net/json/%s" % address.encode("utf-8")) defer.returnValue(result.body) def main(): log.startLogging(sys.stdout) application = cyclone.web.Application([ (r"/xmlrpc", XmlrpcHandler), (r"/jsonrpc", JsonrpcHandler), ]) reactor.listenTCP(8888, application) reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/rpc/jsonrpc_sync_client.py0000644000175000017500000000235012124336260021666 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import urllib def request(url, func, *args): req = json.dumps({"method": func, "params": args, "id": 1}) result = urllib.urlopen(url, req).read() try: response = json.loads(result) except: return "error: %s" % result else: return response.get("result", response.get("error")) url = "http://localhost:8888/jsonrpc" print "echo:", request(url, "echo", "foo bar") print "sort:", request(url, "sort", ["foo", "bar"]) print "count:", request(url, "count", ["foo", "bar"]) print "geoip_lookup:", request(url, "geoip_lookup", "google.com") python-cyclone-1.1/demos/rpc/jsonrpc_async_client.py0000644000175000017500000000216012124336260022026 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cyclone.httpclient from twisted.internet import defer, reactor @defer.inlineCallbacks def main(): cli = cyclone.httpclient.JsonRPC("http://localhost:8888/jsonrpc") print "echo:", (yield cli.echo("foo bar")) print "sort:", (yield cli.sort(["foo", "bar"])) print "count:", (yield cli.count(["foo", "bar"])) print "geoip_lookup:", (yield cli.geoip_lookup("google.com")) reactor.stop() if __name__ == "__main__": main() reactor.run() python-cyclone-1.1/demos/facebook/0000755000175000017500000000000012124336260016231 5ustar lunarlunarpython-cyclone-1.1/demos/facebook/templates/0000755000175000017500000000000012124336260020227 5ustar lunarlunarpython-cyclone-1.1/demos/facebook/templates/stream.html0000644000175000017500000000146112124336260022412 0ustar lunarlunar Tornado Facebook Stream Demo
{{ escape(current_user["name"]) }} - {{ _("Sign out") }}
{% for post in stream["posts"] %} {{ modules.Post(post, stream["profiles"][post["actor_id"]]) }} {% end %}
python-cyclone-1.1/demos/facebook/templates/modules/0000755000175000017500000000000012124336260021677 5ustar lunarlunarpython-cyclone-1.1/demos/facebook/templates/modules/post.html0000644000175000017500000000233412124336260023554 0ustar lunarlunar
{{ escape(actor["name"]) }} {% if post["message"] %} {{ escape(post["message"]) }} {% end %} {% if post["attachment"] %}
{% if post["attachment"].get("name") %} {% end %} {% if post["attachment"].get("description") %}
{{ post["attachment"]["description"] }}
{% end %} {% for media in filter(lambda m: m.get("src") and m["type"] in ("photo", "link"), post["attachment"].get("media", [])) %} {{ escape(media.get( {% end %}
{% end %}
python-cyclone-1.1/demos/facebook/README0000644000175000017500000000052712124336260017115 0ustar lunarlunarRunning the Tornado Facebook example ===================================== To work with the provided Facebook api key, this example must be accessed at http://localhost:8888/ to match the Connect URL set in the example application. To use any other domain, a new Facebook application must be registered with a Connect URL set to that domain. python-cyclone-1.1/demos/facebook/uimodules.py0000644000175000017500000000141012124336260020605 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cyclone.web class Entry(cyclone.web.UIModule): def render(self): return '
ENTRY
' python-cyclone-1.1/demos/facebook/facebook.py0000755000175000017500000000750112124336260020362 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import os.path import cyclone.auth import cyclone.escape import cyclone.web from twisted.python import log from twisted.internet import reactor class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", MainHandler), (r"/auth/login", AuthLoginHandler), (r"/auth/logout", AuthLogoutHandler), ] settings = dict( cookie_secret="12oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", login_url="/auth/login", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, facebook_api_key="9e2ada1b462142c4dfcc8e894ea1e37c", facebook_secret="32fc6114554e3c53d5952594510021e2", ui_modules={"Post": PostModule}, debug=True, ) cyclone.web.Application.__init__(self, handlers, **settings) class BaseHandler(cyclone.web.RequestHandler): def get_current_user(self): user_json = self.get_secure_cookie("user") if not user_json: return None return cyclone.escape.json_decode(user_json) class MainHandler(BaseHandler, cyclone.auth.FacebookMixin): @cyclone.web.authenticated @cyclone.web.asynchronous def get(self): self.facebook_request( method="stream.get", callback=self._on_stream, session_key=self.current_user["session_key"]) def _on_stream(self, stream): if stream is None: # Session may have expired self.redirect("/auth/login") return # Turn profiles into a dict mapping id => profile stream["profiles"] = dict((p["id"], p) for p in stream["profiles"]) self.render("stream.html", stream=stream) class AuthLoginHandler(BaseHandler, cyclone.auth.FacebookMixin): @cyclone.web.asynchronous def get(self): if self.get_argument("session", None): self.get_authenticated_user(self._on_auth) return self.authorize_redirect("read_stream") def _on_auth(self, user): if not user: raise cyclone.web.HTTPError(500, "Facebook auth failed") self.set_secure_cookie("user", cyclone.escape.json_encode(user)) self.redirect(self.get_argument("next", "/")) class AuthLogoutHandler(BaseHandler, cyclone.auth.FacebookMixin): @cyclone.web.asynchronous def get(self): self.clear_cookie("user") if not self.current_user: self.redirect(self.get_argument("next", "/")) return self.facebook_request( method="auth.revokeAuthorization", callback=self._on_deauthorize, session_key=self.current_user["session_key"]) def _on_deauthorize(self, response): self.redirect(self.get_argument("next", "/")) class PostModule(cyclone.web.UIModule): def render(self, post, actor): return self.render_string("modules/post.html", post=post, actor=actor) def main(): reactor.listenTCP(8888, Application()) reactor.run() if __name__ == "__main__": log.startLogging(sys.stdout) main() python-cyclone-1.1/demos/facebook/static/0000755000175000017500000000000012124336260017520 5ustar lunarlunarpython-cyclone-1.1/demos/facebook/static/facebook.js0000644000175000017500000000000012124336260021615 0ustar lunarlunarpython-cyclone-1.1/demos/facebook/static/facebook.css0000644000175000017500000000272412124336260022010 0ustar lunarlunar/* * Copyright 2009 Facebook * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ body { background: white; color: black; margin: 15px; } body, input, textarea { font-family: "Lucida Grande", Tahoma, Verdana, sans-serif; font-size: 10pt; } table { border-collapse: collapse; border: 0; } td { border: 0; padding: 0; } img { border: 0; } a { text-decoration: none; color: #3b5998; } a:hover { text-decoration: underline; } .post { border-bottom: 1px solid #eeeeee; min-height: 50px; padding-bottom: 10px; margin-top: 10px; } .post .picture { float: left; } .post .picture img { height: 50px; width: 50px; } .post .body { margin-left: 60px; } .post .media img { border: 1px solid #cccccc; padding: 3px; } .post .media:hover img { border: 1px solid #3b5998; } .post a.actor { font-weight: bold; } .post .meta { font-size: 11px; } .post a.permalink { color: #777777; } #body { max-width: 700px; margin: auto; } python-cyclone-1.1/demos/helloworld/0000755000175000017500000000000012124336260016633 5ustar lunarlunarpython-cyclone-1.1/demos/helloworld/helloworld.py0000755000175000017500000000210212124336260021356 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cyclone.web import sys from twisted.internet import reactor from twisted.python import log class MainHandler(cyclone.web.RequestHandler): def get(self): self.write("Hello, world") if __name__ == "__main__": application = cyclone.web.Application([ (r"/", MainHandler) ]) log.startLogging(sys.stdout) reactor.listenTCP(8888, application, interface="127.0.0.1") reactor.run() python-cyclone-1.1/demos/helloworld/nginx.conf0000644000175000017500000000102112124336260020617 0ustar lunarlunarupstream backend { server localhost:9901; server localhost:9902; server localhost:9903; server localhost:9904; # server unix:/tmp/cyclone.1; # server unix:/tmp/cyclone.2; } server { listen 80; server_name localhost; #access_log /var/log/nginx/access.log; location / { proxy_pass http://backend; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } python-cyclone-1.1/demos/helloworld/helloworld_bottle.py0000755000175000017500000000146712124336260022744 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys from cyclone.bottle import run, route @route("/") def index(web): web.write("Hello, world") run(host="127.0.0.1", port=8888, log=sys.stdout) python-cyclone-1.1/demos/helloworld/helloworld_twistd.py0000755000175000017500000000210412124336260022756 0ustar lunarlunar#!/usr/bin/env twistd -ny # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cyclone.web from twisted.application import internet from twisted.application import service class MainHandler(cyclone.web.RequestHandler): def get(self): self.write("Hello, world") webapp = cyclone.web.Application([ (r"/", MainHandler) ]) application = service.Application("helloworld_twistd") server = internet.TCPServer(8888, webapp, interface="127.0.0.1") server.setServiceParent(application) python-cyclone-1.1/demos/helloworld/helloworld_simple.py0000755000175000017500000000235012124336260022734 0ustar lunarlunar# coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # # Start the server: # cyclone run helloworld_simple.py # # For more info: # cyclone run --help # # If this server is reverse proxied by nginx, some headers must be added to # the request. Check our sample nginx.conf for details, and set xheaders=True # to make cyclone use those headers. import cyclone.web class MainHandler(cyclone.web.RequestHandler): def get(self): self.write("Hello, world") class Application(cyclone.web.Application): def __init__(self): cyclone.web.Application.__init__(self, [(r"/", MainHandler)], xheaders=False) python-cyclone-1.1/demos/digest_auth/0000755000175000017500000000000012124336260016760 5ustar lunarlunarpython-cyclone-1.1/demos/digest_auth/digest.py0000644000175000017500000001447212124336260020621 0ustar lunarlunarfrom cyclone.web import * from hashlib import md5 class DigestAuthMixin(object): def apply_checksum(self, data): return md5(data).hexdigest() def apply_digest(self, secret, data): return self.apply_checksum(secret + ":" + data) def A1(self, algorithm, auth_pass): """ If 'algorithm' is "MD5" or unset, A1 is: A1 = unq(username-value) ":" unq(realm-value) ":" passwd if 'algorithm' is 'MD5-Sess', A1 is: A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) ":" unq(nonce-value) ":" unq(cnonce-value) """ username = self.params["username"] if algorithm == 'MD5' or not algorithm: return "%s:%s:%s" % (username, self.realm, auth_pass) elif algorithm == 'MD5-Sess': return self.apply_checksum('%s:%s:%s:%s:%s' % \ (username, self.realm, auth_pass, self.params['nonce'], self.params['cnonce'])) def A2(self): """ If the "qop" directive's value is "auth" or is unspecified, then A2 is: A2 = Method ":" digest-uri-value Else, A2 = Method ":" digest-uri-value ":" H(entity-body) """ if self.params['qop'] == 'auth' or not self.params['qop']: return self.request.method + ":" + self.request.uri elif self.params['qop'] == 'auth-int': #print "UNSUPPORTED 'qop' METHOD\n" return ":".join([self.request.method, self.request.uri, self.apply_checksum(self.request.body)]) else: print "A2 GOT BAD VALUE FOR 'qop': %s\n" % self.params['qop'] def response(self, auth_pass): if 'qop' in self.params: auth_comps = [self.params['nonce'], self.params['nc'], self.params['cnonce'], self.params['qop'], self.apply_checksum(self.A2())] return self.apply_digest(self.apply_checksum( \ self.A1(self.params.get('algorithm'), auth_pass)), ':'.join(auth_comps)) else: return self.apply_digest(self.apply_checksum( \ self.A1(self.params.get('algorithm'), auth_pass)), ':'.join([self.params["nonce"], self.apply_checksum(self.A2())])) def _parse_header(self, authheader): try: n = len("Digest ") authheader = authheader[n:].strip() items = authheader.split(", ") keyvalues = [i.split("=", 1) for i in items] keyvalues = [(k.strip(), v.strip().replace('"', '')) for k, v in keyvalues] self.params = dict(keyvalues) except: self.params = [] def _create_nonce(self): return md5("%d:%s" % (time.time(), self.realm)).hexdigest() def createAuthHeader(self): self.set_status(401) nonce = self._create_nonce() self.set_header("WWW-Authenticate", "Digest algorithm=MD5, realm=%s, qop=auth, nonce=%s" % (self.realm, nonce)) self.finish() return False def get_authenticated_user(self, get_creds_callback, realm): creds = None expected_response = None actual_response = None auth = None if not hasattr(self,'realm'): self.realm = realm try: auth = self.request.headers.get('Authorization') if not auth or not auth.startswith('Digest '): return self.createAuthHeader() else: self._parse_header(auth) required_params = ['username', 'realm', 'nonce', 'uri', 'response', 'qop', 'nc', 'cnonce'] for k in required_params: if k not in self.params: print "REQUIRED PARAM %s MISSING\n" % k return self.createAuthHeader() elif not self.params[k]: print "REQUIRED PARAM %s IS NONE OR EMPTY\n" % k return self.createAuthHeader() else: continue creds = get_creds_callback(self.params['username']) if not creds: # the username passed to get_creds_callback didn't # match any valid users. self.createAuthHeader() else: expected_response = self.response(creds['auth_password']) actual_response = self.params['response'] print "Expected: %s" % expected_response print "Actual: %s" % actual_response if expected_response and actual_response: if expected_response == actual_response: self._current_user = self.params['username'] print "Digest Auth user '%s' successful for realm '%s'. URI: '%s', IP: '%s'" % (self.params['username'], self.realm, self.request.uri, self.request.remote_ip) return True else: self.createAuthHeader() except Exception as out: print "FELL THROUGH: %s\n" % out print "AUTH HEADERS: %s" % auth print "SELF.PARAMS: ",self.params,"\n" print "CREDS: ", creds print "EXPECTED RESPONSE: %s" % expected_response print "ACTUAL RESPONSE: %s" % actual_response return self.createAuthHeader() def digest_auth(realm, auth_func): """A decorator used to protect methods with HTTP Digest authentication. """ def digest_auth_decorator(func): def func_replacement(self, *args, **kwargs): # 'self' here is the RequestHandler object, which is inheriting # from DigestAuthMixin to get 'get_authenticated_user' if self.get_authenticated_user(auth_func, realm): return func(self, *args, **kwargs) return func_replacement return digest_auth_decorator python-cyclone-1.1/demos/digest_auth/README.md0000644000175000017500000000166312124336260020245 0ustar lunarlunar# Cyclone auth digest example This is a port of https://github.com/bkjones/curtain (Apache License) ## Basic usage ### import digest.py at the top of your views file import digest ### subclass digest.DigestAuthMixin in authenticated views class MainHandler(digest.DigestAuthMixin, cyclone.web.RequestHandler): ### define a password store. This function is expected to return a hash containing auth\_username and auth\_password. def passwordz(username): creds = { 'auth_username': 'test', 'auth_password': 'foobar' } if username == creds['auth_username']: return creds ### decorate views (get/post) with digest.digest_auth. Passing in authentication realm and password store. If authenticated, `self.current_user` will be properly set. @digest.digest_auth('Cyclone', passwordz) def get(self): self.write("Hello %s" % (self.current_user)) python-cyclone-1.1/demos/digest_auth/authdemo.py0000755000175000017500000000373612124336260021154 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import hashlib import time import cyclone.escape import cyclone.web import digest from twisted.python import log from twisted.internet import reactor class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", MainHandler), ] settings = dict( cookie_secret="32oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", debug=True, login_url="/auth/login", ) cyclone.web.Application.__init__(self, handlers, **settings) class MainHandler(digest.DigestAuthMixin, cyclone.web.RequestHandler): # forces this handler to parse only GET / PROPFIND methods SUPPORTED_METHODS = ("GET", "PROPFIND") def passwordz(username): creds = { 'auth_username': 'test', 'auth_password': 'foobar' } if username == creds['auth_username']: return creds @digest.digest_auth('Cyclone', passwordz) def get(self): self.write("Hello %s" % (self.current_user)) @digest.digest_auth('Cyclone', passwordz) def propfind(self): self.write("Hello %s" % (self.current_user)) def main(): log.startLogging(sys.stdout) reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/fbgraphapi/0000755000175000017500000000000012124336260016563 5ustar lunarlunarpython-cyclone-1.1/demos/fbgraphapi/templates/0000755000175000017500000000000012124336260020561 5ustar lunarlunarpython-cyclone-1.1/demos/fbgraphapi/templates/stream.html0000644000175000017500000000144512124336260022746 0ustar lunarlunar Tornado Facebook Stream Demo
{{ escape(current_user["name"]) }} - {{ _("Sign out") }}
{% for post in stream["data"] %} {{ modules.Post(post) }} {% end %}
python-cyclone-1.1/demos/fbgraphapi/templates/modules/0000755000175000017500000000000012124336260022231 5ustar lunarlunarpython-cyclone-1.1/demos/fbgraphapi/templates/modules/post.html0000644000175000017500000000137512124336260024112 0ustar lunarlunar
{% set author_url="http://www.facebook.com/profile.php?id=" + escape(post["from"]["id"]) %}
{{ escape(post["from"]["name"]) }} {% if "message" in post %} {{ escape(post["message"]) }} {% end %}
python-cyclone-1.1/demos/fbgraphapi/README0000644000175000017500000000052712124336260017447 0ustar lunarlunarRunning the Tornado Facebook example ===================================== To work with the provided Facebook api key, this example must be accessed at http://localhost:8888/ to match the Connect URL set in the example application. To use any other domain, a new Facebook application must be registered with a Connect URL set to that domain. python-cyclone-1.1/demos/fbgraphapi/uimodules.py0000644000175000017500000000141012124336260021137 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cyclone.web class Entry(cyclone.web.UIModule): def render(self): return '
ENTRY
' python-cyclone-1.1/demos/fbgraphapi/facebook.py0000755000175000017500000000757212124336260020724 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import sys import os.path import cyclone.auth import cyclone.escape import cyclone.web from twisted.python import log from twisted.internet import reactor class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", MainHandler), (r"/auth/login", AuthLoginHandler), (r"/auth/logout", AuthLogoutHandler), ] settings = dict( cookie_secret="12oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", login_url="/auth/login", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, facebook_api_key="501833353168262", facebook_secret="56d788a22ee09499ea57b5044da123a1", ui_modules={"Post": PostModule}, debug=True, ) cyclone.web.Application.__init__(self, handlers, **settings) class BaseHandler(cyclone.web.RequestHandler): def get_current_user(self): user_json = self.get_secure_cookie("user") if not user_json: return None return cyclone.escape.json_decode(user_json) class MainHandler(BaseHandler, cyclone.auth.FacebookGraphMixin): @cyclone.web.authenticated @cyclone.web.asynchronous def get(self): self.facebook_request("/me/friends", self._on_stream, access_token=self.current_user["access_token"]) def _on_stream(self, stream): if stream is None: # Session may have expired self.redirect("/auth/login") return self.render("stream.html", stream=stream) class AuthLoginHandler(BaseHandler, cyclone.auth.FacebookGraphMixin): @cyclone.web.asynchronous def get(self): my_url = (self.request.protocol + "://" + self.request.host + "/auth/login?next=" + cyclone.escape.url_escape(self.get_argument("next", "/"))) if self.get_argument("code", False): self.get_authenticated_user( redirect_uri=my_url, client_id=self.settings["facebook_api_key"], client_secret=self.settings["facebook_secret"], code=self.get_argument("code"), callback=self._on_auth) return self.authorize_redirect(redirect_uri=my_url, client_id=self.settings["facebook_api_key"], extra_params={"scope": "read_stream"}) def _on_auth(self, user): if not user: raise cyclone.web.HTTPError(500, "Facebook auth failed") self.set_secure_cookie("user", cyclone.escape.json_encode(user)) self.redirect(self.get_argument("next", "/")) class AuthLogoutHandler(BaseHandler, cyclone.auth.FacebookGraphMixin): def get(self): self.clear_cookie("user") self.redirect(self.get_argument("next", "/")) class PostModule(cyclone.web.UIModule): def render(self, post, actor): return self.render_string("modules/post.html", post=post, actor=actor) def main(): reactor.listenTCP(8888, Application()) reactor.run() if __name__ == "__main__": log.startLogging(sys.stdout) main() python-cyclone-1.1/demos/fbgraphapi/static/0000755000175000017500000000000012124336260020052 5ustar lunarlunarpython-cyclone-1.1/demos/fbgraphapi/static/facebook.js0000644000175000017500000000000012124336260022147 0ustar lunarlunarpython-cyclone-1.1/demos/fbgraphapi/static/facebook.css0000644000175000017500000000272412124336260022342 0ustar lunarlunar/* * Copyright 2009 Facebook * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ body { background: white; color: black; margin: 15px; } body, input, textarea { font-family: "Lucida Grande", Tahoma, Verdana, sans-serif; font-size: 10pt; } table { border-collapse: collapse; border: 0; } td { border: 0; padding: 0; } img { border: 0; } a { text-decoration: none; color: #3b5998; } a:hover { text-decoration: underline; } .post { border-bottom: 1px solid #eeeeee; min-height: 50px; padding-bottom: 10px; margin-top: 10px; } .post .picture { float: left; } .post .picture img { height: 50px; width: 50px; } .post .body { margin-left: 60px; } .post .media img { border: 1px solid #cccccc; padding: 3px; } .post .media:hover img { border: 1px solid #3b5998; } .post a.actor { font-weight: bold; } .post .meta { font-size: 11px; } .post a.permalink { color: #777777; } #body { max-width: 700px; margin: auto; } python-cyclone-1.1/demos/email/0000755000175000017500000000000012124336260015547 5ustar lunarlunarpython-cyclone-1.1/demos/email/emaildemo.py0000755000175000017500000000626412124336260020070 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os.path import sys import cyclone.mail import cyclone.web from twisted.python import log from twisted.internet import defer, reactor class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", cyclone.web.RedirectHandler, {"url": "/static/index.html"}), (r"/sendmail", SendmailHandler), ] settings = dict( debug=True, static_path="./static", template_path="./template", email_settings=dict( host="smtp.gmail.com", # mandatory port=587, # optional. default=25 or 587 for TLS tls=True, # optional. default=False username="foo", # optional. no default password="bar", # optional. no default ) ) cyclone.web.Application.__init__(self, handlers, **settings) class SendmailHandler(cyclone.web.RequestHandler): @defer.inlineCallbacks def post(self): to_addrs = self.get_argument("to_addrs").split(",") subject = self.get_argument("subject") message = self.get_argument("message") content_type = self.get_argument("content_type") # message may also be an html template: # message = self.render_string("email.html", name="foobar") msg = cyclone.mail.Message( from_addr="you@domain.com", to_addrs=to_addrs, subject=subject, message=message, mime=content_type, # optional. default=text/plain charset="utf-8") # optional. default=utf-8 img_path = os.path.join(self.settings.static_path, "me.png") msg.attach(img_path, mime="image/png") txt_path = os.path.join(self.settings.static_path, "info.txt") msg.attach(txt_path, mime="text/plain", charset="utf-8") msg.attach("fake.txt", mime="text/plain", charset="utf-8", content="this file is fake!") msg.add_header('X-MailTag', 'sampleUpload') # custom email header try: response = yield cyclone.mail.sendmail( self.settings.email_settings, msg) self.render("response.html", title="Success", response=response) except Exception, e: self.render("response.html", title="Failure", response=str(e)) def main(): log.startLogging(sys.stdout) reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/demos/email/template/0000755000175000017500000000000012124336260017362 5ustar lunarlunarpython-cyclone-1.1/demos/email/template/response.html0000644000175000017500000000156712124336260022117 0ustar lunarlunar {{title}}

{{title}}


«

Response:

{{response}}
python-cyclone-1.1/demos/email/static/0000755000175000017500000000000012124336260017036 5ustar lunarlunarpython-cyclone-1.1/demos/email/static/index.html0000644000175000017500000000356512124336260021044 0ustar lunarlunar cyclone email demo

cyclone email demo


List of destination addresses, separated by comma
python-cyclone-1.1/demos/email/static/info.txt0000644000175000017500000000002312124336260020525 0ustar lunarlunarcyclone email demo python-cyclone-1.1/demos/email/static/me.png0000644000175000017500000011536212124336260020155 0ustar lunarlunar‰PNG  IHDR¢pfýszúôÌ™³p`\H0À×B¶2ÐÁØ;8bèÞÀ vƒ]`Ÿ+!(@7E(ÿ¥-öˆ:Õ'EµtºFT\ƒ¥}&¤{(8 ý¥?bV2âHpznc-*vÛÆÖTJ   /*&x¹º#ø‚%ÉÖV8"˜ÕsWQ±Û6n¦â‚'U÷-´;üܽý ›B°†;1ˆ€LSýº»|| XÇ××±Ïþ ‘‹Ȉ.û‚…¨û‚ôHs±@Q±sêY !%D0ýÿÈ„•Ø ÀÝ®dsV[{quyÈÊl™ƒ˜u ÞÜœAl¦°ž²¹¹š»¹¹žê=õ$B09d‹‹ÜÔÀÿ6Þ¾çß($8ÔE4 ßGÝB·Óî Ëd°d:È¢ÆFâ¨çäåŠážåÑÝw‰ÿ±@‹`…pœ¨ŠØ q}‰'RRØ\iîCdÖä¼ä{••òTЪ.jõ¼šZ/t¸pºúhÃt£!>S 3+ó‹X|¦e¹UÇáÏÖ¶\vX{œƒ£ï‘“G“²8?t©r}ìö„ðÔý)±Î£Ú³Ì«Øûáñ"ŸBR¾o®ßMÿ›×¯“ó‚ò)w‚KBjCŸ‡u†FLF®E±œä;%­sZïŒvŒZ¬RœüYés’çEãø¹/p^dKbL¦½_ÚHù•:wyæÊ·´ñô±«22û³zÿêÉî¾ÖwýÝ7'rær7n±æóß–)0,tºt7¾(ç^ÍýÞ“Åp O©L™Aù‘ —JÇG6UÖÕø“ZƒÇÚuªOžÊÔcŸI4?Ç4ò¼ØÝ´ó%k3S }+ܺڶÐþ£cªó[×h÷ûžŽÞg}¥¯r__ìyãüVÿÝÆïƒï‹†â‡=?è}FFGŸÊ‹úŒû¼ò¥ð«õ8¿3a9±<™õMõÛàTÈ4ëtþw¥ï-3¶3£?¼üüIù¹<>»2>·4~³A°¡a¿Q²±¥É^“¦¹fîæ‚æ-²ñö–l–-V1‡UÏZÙxÚblßÛeÙ;8ð:|p¼uÄç(öèg§ŒcÆÇÖK\ˆ®<®½nñ-Â’ûC¢§¿ÇÏd/C¯ ï²ãÇ}øóç«è;î—éoä¿p?Ð…ÌE¨S–‚ËBC¥C†•…‡D(GlF6¸esrßɯ§Š£COãÎ0ŸéÉ ŽÓ;Ë}vú\ãùìøÐÛD… ÜV/%=O.¼”’BI=zwE41m:½çjuFefqÖƒ¿ ²ó¯å]ϹqãfVÎõÜy7nÝÌ¿{»¸ ¢ðéÖ»ýEŸîÍ>@=Ü],^¢^jUæS~¶"¯²áÑçjö•ZÏÇuÝOéëqÏb^42¾À7e¾jk j«î í4ëºÒ=Ø+Úúª¹ŸÿMèÛ×ZƒåC²ÃõG>~ºôÙì+×øÊäÔÔÒ ßO·¹êE™åžÕ¬ ejü·s5'Ð*pÕ» h€¨8\Hų`­ à÷b¶ŽÐUFð'°1  lÀq ®€BP ºÀ(X€è! iCÖ' %CyPÔBË0, kÂö0¾ß…›á¯(” EÝDu ÖÑX´+:ÝAƒ¦Q£ ¦yHóVŒÖ“¶€v‚N’Ο®‚nÞ€>…þ=ƒ8CC#cc †)œ©ùsó4‹K9«(ë 6N¶dvföó4q;èv$ìdۙΉἿKiW ×®Ÿ»¸E¹÷xìeÞ[ÎãÌËÂ[·/ˆOŒoˆ?ƒ`hÛŸ(h,Ä*Ô#œ*b'ºOtTìö?qñu‰’¥l°ìÔÁJéó‡šíTt¬Ö¹Ãåƒë,É]˜ˆó÷¬öš?Žõ $•ù.úkœ ìÚKq. E‡Ù„F¬œ°Šºsr#úÈéÚÞØ¨¸ásjç ØÏ]XK M^J‰¾ÌvåFºòÕÁ̈¿ög÷^¾)“ó5/;_€*¬º›pïöƒ±’ƒe±_ªœjFêbëUŸs4¡Zv·ëuëë7~×7ä2Â4öbüì”ÃÁ¹ïK÷V}¶Þ\@˜O$úWÁð € qC’Hìm ãÐIè2T=^A“0sÇ`˜Ÿ†¯Áuð¼@¢Q×P]h4Z†®DÏÓÈÒÑ”Ó,ÒªÒž¦}IÇAçHw‹n–^›þýƒÉ ©alÃÁ$Ò§dNÈFʅʇ)„*žP:¡£’¨š¢v]½P£J³Ië½ö"ŽEWBÏHßÏ Í°Æ¨ÛxØdÎŒÖ|³´·"Ž³Î°¹o[g×i?ì0í¸r”Ɖí·3ÆEÄU KPpW%mEy?©Ø÷‡¿|@Dàcò&E;8&¤9Œ)Ü,"9²+jçIü©äè¾3»cc³ãFÏ©žÏM@'^x“¤™\˜²#5æòDšuzC†t敬lÒµ¾j7oå2çÝzw[£ çÃݳ÷Äî>Ì-ñ.“/߬|[UQsùqÔßz—†#ÎM„frkT{ZgawSïøëŠ·©ƒþæ#Šc‚_'QSk3ó³Ë óËk;¶âंbÐdâ… sä™?e@¥P;ô†a~Xv‚£á[p¼€Ú²A%¡ÚÑlèÃè¿Ð_hdhNÓtÓ ÑFÐöÒaéâé&éÍéKö0Ä1üd$0¾b2dz¬Ì\Êrˆ¥‚Õ†uí.» 7ÇàŽÜœ†»„¹è‘gy„»oOëÞFžzÞª}¹|çø)O×ý®‚žBÁÂ1"¢%bÍ>KI)9¬ÅA/éøC/e™å,å3>*‰+‡ª4©íV'i¼ÐâÓŽÑ×µÕ{i møÔgÒdfdÞŠ7µìüUb_ZQÎPá\YQE_mW“[;]§ü$úéógŒ fÏS_7q½tnÎiùÔ&ÖNî(ïÜè6îIïýô ûšÒ_ýz§37Ø4+~ðø˜:òhtðÓÆç=_~Õ7™°œ´üf>e<­ü]d†efáGßÏâÙØ9»yùÉ…âÅ %¹¥éåÛ¿ìVhWÊWV7ÖòÖõÖÇ7©ñß®—¨ù0âüIþdŒ)Nwkøÿwñ%#5ÙVÛ\™ýÜÌ-žŠÇ(xk¤çB~+A!‡õž)‡Ø=¼õ~cŒ»«® ‚y¹t„ÎÁÌ6õ ë#9Žª Ùw5Æ#˜Á~D?›ÃFìC'H[5.'Pt¨|nß$éýáTExYÛýÖm%[!u òUÔ–>þ&T>Õך;Q÷÷Ú`z?’¹)"GüÂÜÞ#êú9ŒúÀy–<HSä»L÷÷ƒÈ1ÈØ™%‚ „7¶ÅûòÝ{ÿ-)à±e/dKÇ|At|½Ï[ÿn€X$„ ÈÒEÒÒks¨^I[žÿh™ü‡dÛÚö ·¹ÞÀaý‘SíoÉ©Þ}K=B2üÃUl½Ð"h´J¹3¯qjkÇÓn㺮㴘QǺ~èZ¥ckl£ ϵӖÆwt£¹óÂøî:§ªxMÙã¶lŠ®îZÛY§ñ:l„i5V§XZ¹MË~7º,³õi^¦‹ó³ùñQ[×Uµ0Öoew7«UV§]Ù¨xÀåøÑpà‡~Õ¹ãÖ-´^Wui­Ü_ÇåáãsVââfú;úa>Ymú‡óçÆ³k1|c°Ì¿1 ëq|9¥ç8ò|î¯m]ó“ÿÚ¯þ½òÖ7î=¼{|ú(Y“ñÔéýÿÉöÿGÿöë_úz¾X.6‘_ÿÜËÏí_ºRäI—]ÛÔE[Z=6«ºÃUÚ®*»¶HTÝTwã¸Zײ[+ã]ií6U™Ž­¹àd‹pY,]~hTGLpX–?PZUºqµåÆ•ü•iuÅþcÿËרÖ‚«Ûºõ­Í*¼Z,ÍXG{8‹c:_uÄ G¯—K¯1i[xNà†«.t[VuìëÃuV-ÊÊsZ¯Zmºhgì,Î;íVå:ËšÜDu¾|ëÆ¶È„üµè:" ;ØiÙªNáÚÛÖ9Ü: ‚e¶Z7„2~oum?÷‰K?ÿÒµû÷Þù§_õ}¹Èòû“Q{c:;˜„ÓË¡ûÜÞÏÆþèG0ßn¦_.ÕüößX§«BW®ß\ší>õô%§ÜtYÑTUWVb¥IÆÒvu£ÙÑOc‡|vƒkv»¦ââqÉÚx]Ñ:>;§rüœ ÑØÜªm¸-l„Ýc1Jç5|ík¿ä™ˆ]:mW·M[Tòì½5%wç`>X}irb¢5]ëé¼m=Â…D–?Œå1~™"/Už¬Q© Ú²uÚ Á–p›´Ì ’ÊÝ'Úä›Ê¯ù>ßNŸœÛl“Ue®šÅñ»oã÷Â;ŸúÔ­ƒé·¯Ï‡ùš=fex¤Gnâ_% bóâpO¥üVs[ŽÂMŒgÚ†5«»Þß}ß¿õÜ‹·žÿµ&Œ¯µ%a¿uûµßzóÎk¿÷ÚºÙµa»3›/E[—†Ÿº?åûæš¾ë1¿ñ_©ó·o7yʆt7š46vˤ®òµíœ¤Qåzt]Q¶¡Ø îÏ+ºå×ÜFÑæ%Á§ªÜÎtMa’¬{üÎi;óª |¯hÔ¨k½óèŸýÑë7ùåoEàïZªïûíÅ_8tCZÃs•æõŒñ°´J<¸nÃZ·0X@ Fá->$[œÏÿà·ÿÙ«ïžÎoÌt5Ü\ßol}ôŠÿ‘øÏßõ¯œ¨¿ú+¿vxü¡ÍÕ`î§>zí©Y¶X›¤s¢$=Ç[J2ŠÚ´:dK|\(ƒÈXvÒ$q\c›d#•ò>Êec¬CJ&]±â†ONÓµµÆ¨ [‘/ISìæ[¹PãàÆ@*ÖÄò 2> f$@ŽÏümIÄ Ý’4!‹Tp|? Œ) †ò­vP®!ü·Í&_ñL]–ùjÝð'ÇGg‹´ž«¬Xh/p´Ú3ñd˜=^{Sã96ºþùÿâ¿ûõ_ØÛ’gÿa>$"þÓ–¿“\×½ø²h2e\ÙnîœÆ}ˆKˆ)س$MWÉÁdzæ×dºõKŸþ4ÿËSã"óúÛï~á•/þƒß Ýλ97.û×nM,þ¸ý¿ù¯‹Ó¯«Ì²6ô=ÝŒf’ò:sðSNãçÍ×a¥»:qÁ»&dÙÀ-€ºlŒëÄ.ÞÒ‚2<;Iª2–¹ª ó4•Ëó¶¦è6ž 6ŽrË·‘ÄÂ8nâM\Ç9ù–GÝ‚K5]Ö¬Ö¸ÀÂ7«âU;œ¡Âàxpçò0BHt8jÙ¹nhÚ¬fÁÙS.uX%+/ JV^WEŠ#R*•c%y‰¨Ixˆë®öª±ÖI–4nÝ_©~›yI u<%ûر߲Á¼È€FºŠ&›.[$wÃÀû¥/}íÿú¿¿Ú4ûÆÆÓéö§®|êéíO]íì÷ßuêõâ­gþó[Ïô».ŸNþñ7¿þ›¿—géγ[Îåýí£§o^{ÿü¶ùæ+{±H UŒqÇãkÃÎke;«ji: Œ.-ɤk³*bØÝºÍuk#¥=_Õ#Ÿ$ŠEm^w~`*Kˆ“ñµ£Edv vÃ%‹ÎˆïbCÜy5JÙÄÊ´Æ ˆê®ïæ)´¬v<â²5¤¯®t(¾º7yÍž¯¥J©X§¶òb¯©ñ~ªcÉßUíDÙ^ŸÅtÉ\ªcýDå`4ê²2ß𳬳²®WeYuyY¬,¿³.kÊ„6}ðÇøö¯~úÚ ø!¿à9ßGéz4 Ì:Äj¾q:`ˆ¦è(œŽüp¹Î\Ïp׃øQUÝ(æ¹ÿè÷£Û_ £Új× 'ƒë{7_ÜÿÔÑ'öG[¾÷.egï×öá×~þý‡¬×éÿô·þÖß7î¹sg}6¿óÈk`Áê-g»Û”µn˼Ö%E+F×¶~š®µ)Ýhˆ3ó›®±ÛÕ ãºfŸÚ®5:°ZQúP‰¶ðÃ%‚ò%žÉåIÌÂK[îoTµ±]©]¯69¾Nj'„»NÌèÌ3åÔœñ=0vP¨psÙc¥ñ)¬[06©#òT–wTé¾k”¼ /c!¤óQV\ ÖÙ-Re £Ä‚–‹táð<]'ž7,×ÚZ)®ÀÑ7§æj¬Õ:ä©ÕðÉZwa^”Ö=“KÄó-ÆàÀl4¡KÉ6´v¦t ºÜ ›ÀÛ*ê¤nm›RÈÎâ”× €§­‹U‰áYlˆ«£Ìq¥Æ/ ]`PIÍþ{àªæa<ªañ’LO–ó9éyGþö‡ù ]<'û®†ê!báÚú³ S¸„ñK ;‹QOö®\ÿøOæY¦gEzvD *Ûþ×Þ:­CSÜ#gHh ñ…r ÕB€*Gïªæ¢Ðç¯T_üêäwÔÀɯ©`ºóÂÞG?sýŸ žŠÞbIÙ&§_´¾×åÚõáe_‘óüª-q*ÜÇé .¬Ó°DT(€e <“Vt–ì P³Ø vØp›‹¨U‘ÀÅ¿[̱«C0“M"W.ñªôñ¸„S¨&§Îå™=¡E˜RД#°¹¤ü„Ô¤ž®ØÂ#,#JP(Ugω¢±{#¯Njì¬êuÛÖ ¼XÕD|^ç' ×в |*wJËܼɘ7„ âT#q zŽ(´\¯¿|\¼°÷awø"nó«l mCàãÅùˆ% |{Â. (“ˆÝb´c_¸þì‹Ï=Ïc‹nU§ùÃåÉÉéÑz-Ò6?LÒ6i%ÌMå’¡F>%MÀõƒWNŒ¥IžuMœÚzYù_ážOߪû›Î?,†ApÕw÷FÛÃÝhÝl@&ZÏ)š.ÏTxЛ9p›W xG¢dÁú¨w!»¸R`‘©„™•„CñŠAéµò"ðME²& ¡ÁC,4÷¦©C/®ªŒUf1Ý2 °_ @"„ÁNƒ¡¥ìÔæH­Rrÿº †š@'ص-êŠÝa‡”QØ&yætþÄ/'¾>Å…áy¤C牒0֯˲!—/Üd ù „3ÙÒÕƒT ¤t²xÀ…hªo•Åêèp®>våCîs‘¤_ÉêÂÊÔa2UGúr[C^ÆG)”{ ÔVN ž¤p´«²æÿ-Ïî·üñáUu—I…°xðàdóè|¾Ìô0Oõ/ý•u]¿ù0}ü¤xüÚÎ*|˜VœÔ'U³Hʼ˒ª5ÄH?ØŒ‡wªúMè¯<¬ÖÙã>ŒR–“@ä(è ÿb¸7 ÇAÐÀn˜°ÆsÉÆ |GÛyl¨O..[IeÃkîB¸f’k/ŽI b-IJ%@ââu©‚À_‚Ø(&ÁTU†¯Sû´!~ÚwˆOûj]êÐ66t¢²,<7ÏtÀ54›¼ó1mZàìŽè0°~®Ö䄦¬€Søh¥)G¡ÌKÌNŠƒ³›Ìɰ¸Œºµª±)?s*›ûQÛ® aieò{IóB |øø1Å¿‘"J†(‹Ã"ø›ðõàQÊB ëC‡µ¡@€ ‚ȵ6O7Ël³'ilûfâÒµò}o´ç}lo«ù´fßMdåjÈZ?q}¤®+õ2ßa‰WVEýÕwV·¿:}´:ÌôUÉ£‘¿vÇ6“ÔoÜäÖ%=Sf\ @hS熽l)Õk+QH°3}H':Qª„§«`‘ÉnÊm= UH­ Q-¨”ˆ`±$CãÂ~´È&},謁lÅŸ`‰0ÔEnÕðîÒÛ¨Š¬}9¡Ê ©7öJ‘Ó(Õ,mn Ô ÔR! ~ ‰¸I­CJ.‹p¸Uo”7Œ4ÿÐe-.†A*B…©:z ›à›ÖsÃZeÔsÀnœ½2íÛ©³Æ‹æÜÞë¯Ñ/q÷êo­±ËlvÕÔ.IFŠEUt˜hOüõ 3$Î.pÇ"$¸×”V˜bãWºöóë¼êިׅ—€UèY†v6÷U|±ÓßuQ#ß~þÅéç_äÇûJýrVÖ¯>8{|¼uvvvg½*ÿ­8ËS(Lx(ö¨ð¨9(‚®®«0HnTÜZ9«­@Y•DfÊ¢Ô^%וCX‚j¶øh%\Ö,\KB‚è#FHºÄ­7äà„k#R*H«H"«TççuÚ6ÛêA7E4T"• h Lªtrt¢¸r½&k’\9u„j2²éP€àÐ8Ã[<±ÒiM—š  k¸[©ä…&à?»Áé)³‹"£>#¬Ъ6„t[­HJ€ýïZÒïù-Þ|ñsÉ8X#{Iº§èlJî`Zˆ±µ‰b”}?|ˆ<¦íVìGñ8)»n”VmGÞ7têpB¯Éd´‡¹yhã°›Æcs=´ßs¿/."ôìO<½§žæ»þÿÝßü;]õ3 ˲𫼙ø\›W¡×eƳ®¥ˆ‘0H¦…Únàž%ÉI;èX„ëBÉ‚–¨„„ ‰•x<×Íer;‰\3™˜¦‰@0MffEEKµÎïðP ãE$ðÁ¢ÄàœBç@Ίf$I£Kª*QkÕðÒgxf4Ê$²eÁ`¶×„ƒ"; -H·&2±ó¤-§S…µ~FXÔ…´^Gü_çÖói¬®—·Åêä´bÇÿ¼]æ>Y Y‹¸/Ð\ì*ž£0Ää™ñ*òl0$&˜X:Ÿ~³†[ÛA˜¶ôÞ dJz>•)Ûy-—Oâ‚ÜQÉó;[ÃHˆá Ò’„VјÑ?8µüÍ¿vç—~=‡¤iLü kÒý ! J¤+.f €S5}0PVÒ”ÖE£‹2`Þ&t@Ô8Žä!¶]*í㆔ BÇS!(èh± ,÷ UDv† Ò¤Æë`»(Z¡…©T ­–c1‚Ì©çˆa짆³IkâQpH`VYWFûDþMéÅñ0?/6¶h«u{[#<ÚÙŠÒåy«(Tª. ÓWY¦|ãQmƒö:`ÇVfBÔâJ ù+$•Ep~zŸŽáÅÎ\8É÷üÌósa‹¸ll÷d)ûd/Kú4 aKÈWbQÂAqŠeñ(È;8°UMØ(T°5:¢Y³¥Æj"pŽZ€¤—çyÖ´Ã(¾¸ŽÂl¾“ ém ñ;Œóþ¼p¢¦šµ*O{¤bO„áy-¼´/mc?ît^Ѭ«IÚlL…‚k)uεÃH ¦À`h­¨WásXÌ*Ç7Ù] 7Å©Š;!•5Ñ‚£æY02w•;)Z1u½ƒ.`Ì’Ó°ö³âE©+!9;ê$cl ŒWÃn’ZðšUÂv"gªU½ð˜/º"ËdÈ1P°eÀÏRÅ`WQP€1• ËÍß#_ p/œjVªS'ßœŸfuóƒýä•ù,¥0ëѰ…|EáVƒOØ]Ð*[/„ƒˆ Hl¼°?<†Å:³_x›Àæ\ >¢»ö=fâIÈ^Äqñ?à3F÷]iæWþ@½ù?ý㲈êúÜ-Lâž^ݤ}‚¶h©r|Zòb K_g-µ„>ef:+n„„Y‚Ÿ ¸ ¦îê<'ÌÓ¼rI½Ü2#°2æ€ ‰±w$ñÐA£Ž’[EŠ h pHÞìû¦]• .¾Á‚Š¢Ìò"U%]+0B¡Nó™ƒpÇ¡- x¨'ðKº wü¨Ž*êÆ’çòxß„ •ŸÚÀæ`]^šh ×»¯mE ÁUI“ßáR‚„ ­—P¯SŸ'R{|ÿ‹X}áÐì »#*NÖ'f¨¸bHZžW™‚(Ø?"˜åa$:Jkûx½9=Ÿ‡FmAD1$àw(‰úqaJßÿþœß|á§Ô Õn[l@*¬[À…XúÈ>+ %"7ÎXâJÚˆxß:Ç£Uj‚—¢B`‡¹JRÞïP% z€ÛÄ/ˆÍÔÔTDü>6XIÏäÿŽ%¿´ŽÜ0­èN¹¬JïÓps¼ÔŒë”åº,Jº_ôŠe^HMBøfuN¸!h ©—–¦ùl'‘EJ8Ê©²[H>¨Ê³„o/ ?ØVÍ’¶AÚp<‹­zî0*—«ÂAh­èê€ë憤­ú¥õévü9‹HˆþVV¾ÈÇÀ+–…»?Äaß$OK¿ÙHÁE­ÀsJÁ(OM~¢½A/¯ÎV«ÖÙ$‹cð’%ÇÐÅñØ5ÛA L—W’'ýá?¶¯Õ÷ß-‰€Ø¶#L‘” cX'¬]¹^u]'›\F0@ƒSx„Cé™Ü§3€wèH:°àüÂëhVµ«Ö§}¤S î.’ Œ˜ÍæÒ¦$Ó[ð:i»!)#ÜwÁè‚Û¹ÉU…%ñ¤b[¬e!—“+Ťh=À+Q–“¼)–à9K<«üt}œ­÷f»v:ì'Õ©”ÕÈÒüh\)h´‰2p°© b ¸ÙyE?p­‹/øÌcˆÃÉXø¶~ÿpR¢ƒG·àÜs‚8Ü@Ÿ›åBßf¡{¤ã燘<€@"aÂ!Êe†šçX…”±¡Š}žÏua´MðÙ÷_¾Ð~’¦…šÿ‰tñ%˜PªÉ*aÍ9]WKè¶Íz½>¡Y³®l1I†¤L®eC,z@ÓIëE®‰¿…~DpÏk”ÐzˆèVcŸBí;…g†ùâÉ\üL :ÿ`ˆÀ@c$0ÈLä:MWä¤W©Ÿ¥pãSæw¸ÓKżìBÞ[„N%žxÜšQ¯Ó ¥‡áv±ãu±7]çIîJã $·àCDÍ_Í—ª´DáîùyVZÚÄv€¾íãƒ=î/çý­JX@#YFÊHPŠ\&—=â%¨à°â650_HÐi2®Ó·;ÃÉÎÖÖº.@-ý cìr{`}ídxwFhÌ Õbi¸&`ù•\ŸþÝÀÕ{”ßË׿š¨ÿõ·î>¾_]ðn•¶ð?‘KFõü1í© h›l"ú¹ÒP$e[!¥ ÂB(hX”šÐÀžÞJýMXÚ‰ˆÊ6;‰«­à i[`ú@i»BÌC”‰ÓKbÍ‘Jœø–sÃPVÝH¦Â‹^8HKxlANÆ ¡l„o¢m曵~ÄÐÔ¢s Vx™j¼5²t25ІnQfá_t´æÒd£Žaç$ŸÔÍЯÚ&}Û6×J²ëøÇZºîX*]v«sR®5„”çpe4HÎÚs¤¶ Œ¸µ€Dã#6¸CîPø3V5mR¤wXzR§bû¢j‡Lƒ\«é:ÐÊBŒAT„¯:—€r´ Fß·p/ýÇÙâ°(Öç´«5ñ·µ©ïE‚ ÙŒž{ÝlÖÄÏ ×V5)P f¶ò“ö€Ê*³Í©kDäÉ âF^Ú-œ\ª3ªøŽ¢€¡Þà`˜59)-T‰Ô€ôô‰ D4–¡î2”)ˆQª(èAÔE\6E‘™º›—Ë‹”$gKXP&òI HwM`ú/šÖ¥‚ßw*Ö¹Š|Ïmtk°Ô0$¡Ý@8uør?‚ 4ËB\Ú€bF‚g£AøÞ|±t|f×û_ËÚ÷$¶Ä4j¢äV À'û,•“ì: -¯/`„’>4®*(†‰K26eYï9ªÙá;Xªm®ˆœXâ_]³µÐ*…»$Ô ²iŠ„&®™6HZ9‡Kupð~gm^íÔöeBEŠ9¢êBô.¬F Íªj͋ӓ5<$h¸ÌéM‘·g¹¡1QÓù•ÊZZ½!,0D­à¦1Ph“'NCÀÙ’¦Ðê®±t†J·Ùà‡ÒŒ"¡ E£“ãs †D„y‚§]Â&á ‰ÚšV$òP a šï`7„¤W6«Ë°‚®¥„,|ÓIãwAŸk íˆè ¢ûN½u¼  @¥ÙÉÐÁþ«0må‹MZçŽH-±<ÝFðÀÑ`³³{ƒjöƒMý_…?`QÀ£BIõ$e­å‹@ÝÒƒžŸîE`bUòÁòè$éùì´Uøy‘ü/2q__Ùa/šëÿ´Bû”ÈØdUN‚Ã)^-å’mô Î}â™êˆƒJ˜–ja KãdAs…à–¡-(.Á±ÏÃö .`[I˜`ÿB@ú.´üÛÐûL–p™uÑúcÛæh¾hE¿¡†Ðý@s³KÙ€+° ƒù@æ.¸AZÐŒ.3ogFvMd Eß,ų ’ê¨8\iŃQ„H‹š—s<ÑVFD¸.ƒ¾s©(ýYN’±djýÓ×nì}üÞü1/'õh‹ÐÂÑžž¯:±,Y¡IÜt®@=qRäs.R!lWÆþ¤L€µ"1>?–ê63õ0Ó9@Aª`bFî:IíFÔD”o¢5c—…sª \+D‡ÀL({lÚ]¨hšt*j Að$:4/±Ú#ÊâðÆMœ* i ÏAÌÅ€rxjcDG—þŠÓS¨ÇAÕÆÚÙðæ‡a£ô/HWà/D' 9TmŽã^Ê IDATV‹ñÃ/·jçöõßßþâh2üøêÖlo|uzëúî_™Í|Ïw‘¢ ÍAzˆ±_ÄÜšT|OúÄ2t#jm¡À„=€œg)ç‹E±ôÖ+s(~l¨©`¼0øî Š'>¨C]°%Ûùý>s)¨ýþ㜗-ÒñîÇ._½÷†„ Ù¥7†‹uâÓµ~á7yž©ŸØ¨È1>8~0æ–t—‘“)sØn<€(B†'ô`rŠsÖê1™–†>;J9åÒßm JXN4 Q\,äš’º[°ø&ä-\½ÀÈ~IIä‚5ÿìÚ2Í! !ße¾ªoCÞÌa(Á%Ø"{è$0½¸,È!ôq½âß6Ú``Å^ ÿÊÙD‹óD+$w‚ýqg\¤Ð@„ÅÛ$ËÕÑcx.Hà¢ò¦ÃÙö•½Ýß ÷v&³hgª¦—n  ÝÝŽ¦´¬¹¿]à2B ýwÌO|“•Ód1:ot°BJD,PšUøµT0A'ç‚s¾Û*]ùÇ‚7aà5‘ŃÐß "‚\OvöûùŸ.¬ÝWZ°åöi‘6o³}4ÉŽZ3mv^o(‹êY|7†¥!ˆ›áLlÜcE9ÂêÈx‹DÊbëB1Hi($48D$ùħÇ•ÎW»ìUGgHS–tÊî#Jm²U"„h{A·“/ØÌ'x-'«ìÈôš8Ó…–.=¶Å<¿ƒ»‚š –+¸”¡7ð¼Â£l×B €¥KNÊO2³å#@%œà[ÈSÈÐ:´²âÄdrx¹Z-ÏÉ2!+l™më"å@2Q°x”|äÆÕ¿ùŸþ*B¥óù›ÿÉþdýGoà»Óƒ‰1“‘¿5 OmOo l8 :骟ÆkJ šN|G²÷ïŽÇ³­Á¦.Öþ2ÏñÉ]²ET#–€GÑO—‰:"¬ÒäêÅÀ®Ó¬šDîÄ2‰†m_Øh—‡½“z@zÙc‘] ŸLœiÖ:U)™ƒüƒ Ù„Rr GX'À‘nBZËâßD/B%‚PÆ,œøºì£J]Z’õzޏ´Í)!á­]jZž=©–3&"u“ÕûäEû“2Vûââ†ìƒs7%ÏÆ«HVb2t˜¡œœ¨"ƒ2£¡7aZ¦²ó3‚ÇÄQ¢QG !£ˆ¾²l0©f Ün8Š»ÈBŽêÉ1Ö"×^N ‹ñ¢ì@ã ‚õ™D!P¨Ï¾|ýæÙÁõ³fšÉpÿoýõ‹ôð‹¯ÜêšxçìµûçzûùéŽOºÝŽb7ÜìN§7§Ãq< ÁD bûÉ/ÖÉ^ï>–M•‚†5U•Â9æ]/TÀk 摆…°I|B” UÐùrmîH"@é¥?}ý)¾‚•\mòOŽEæ4ß"EH— =úÀëhÆæ!M Bt€¿õC·_Ré“^ûƒ C• äŽC¼“à×”€•Ê…Ò6³:™!=z ‰î³¨2i³GXÉ&@¶›‰R¦”®>CÈPšÙô¤•-œ¥éAÏhcº$%s$OWúyqÍÚ#ÄÁ‡C[8ƒp]d› 5?5ÈWAKÇÚÀ|{2˜D³ÏÂ~I¥.mi&¾àçQžÅcçlE/®æš .ŽÕU›4YðákOª” ¢ûÔKÿæ_ýË?vçñèዱ·­¼í¿üoI#ùJ÷‹@Ÿîœ]8M¿wòð1 þ;G¯Þ ÖÊÝC7Œ&ƒ`4 ÜÉֵɈFu¦Ô.ȾH†hQXdK¨QöLèrK§’ –·Ù*W“SYÿ¼'ÊÄ“TÉì§R²ÍìGv¾™^º øV±)Êcj=ì´ƒÿOé¥ãÇ< w¤,j ±£\{›eµ2ô–vi+ˆÔ ¯ÝhÛöÆDž§ÅAµÛÙL#LI_ pT¹pýÑÖz$a¢b™K™%:³ÉÄb!ŒlTW O„ÇqM.U'†šŠLÃË¡jÅz†(ಈ¶¸ÛNµ‡ñŠÐ¶HýN–'fÞð©ØõE^‰Š¿ Ó ±iþF·ƒ`àcݬä:gTÖGÊ'KŒœœ¶ DÞ.õ íëgÕlpùIPðêý«?~ãUD¦ÛûÞ¬•ƒ]zjs8݉^øôÓmýYâÇ*Ëß;=}rxrxÿáý7ôYe^5 „aER–T/„BêB²_Ï ÁJMŽŠUÊ2ÕEÚ¡òwzX r‘ÝTÐPÅ2KŽp¼dþ¾íQlTä-Ⓤ¯¤óÃ.—öp4Ü¥ïì –—ZšTGæ@hºõjQí²@òa|$}|¶1¡èK“ºÒ™SQÅó ååŒx¢´"‰ 8¾j¼Ú#©¹Pñ0ˆê1IYÒ!Ä[¯ª'¯ !T½øµ0°mKÃQ&$¡¸qN&/xZ‚‹ÕÉy¸˜M%FÑd±J¦c 0–¹ˆ˜ª!;I ´P°mò7áá[PâʇòoïúÁÇo^z³Ãÿªxxéô$…ªölT>¼‡mGû¯.–[1#¤ê™›gÛ—Š“'ï`8ÿW¯…¿óªlŽnãʴή{°ûÌþÎǯ®ij]Ñ(qöÖöîÖÍçPäùr¾8=ÅTQâÁ/¸Ž= ¢À bÈ;Ÿô®æU%‘~ÿÂãÕñŒß#,9l´ß¤|åNÇ#=ÚvÖIàWã!)cA”ôØÑËœž"] |0Ke‰,{“Ê2)‚Ý`n<è9ɼ”4¥ï æ&½¢À- ›*óÊ’*H1MÉÃÔòZ ‡Fnkv›€Ÿ5çÌê¡á°@>¤ºDÉsà"²„™\¾é†3«“£‡Kø-x^,•Ù,Н!?ã­ƒ®=-VɣãY¸EªF«Èœk@RF{ÔÆseàfé÷ƒ1VB $ð‚ÀcÝú¯ü¿±í&]:ýßÿŸÛ²<[#‰ì®]™SJ·-Šn29ȼՒÙÖtáwUDG2lŠ„Çó±Eª©²$0—Eà‡Ã°ž¯hÍP¢˜£÷Îý˜Uzöª{~/F)¸syêšA:ˆ±)bcW±Íò››?ÿÓÿú»wNŽ7›lÎ0žœç z!F:`8¸b,H³>*ô©j^¯´w…w¸õvdA¼¿µÃá–4TŤÒ@ª6#®âH„*ø®KÎ*w¹â’é»RB /££(öâYàî !Nõîÿuœl`.®o;rŸú‰§¾ö;:ö}(w` ›tÈ4 ºŒ`2 Wy•ž³Kt…Ì £8Kòt~$é˜ÔÁ"ù )W³áeØ€ lŽ”*[L¶¡¡B£‘K àgaÅU¨‰6“XDŇßn1 ÝSÄý€rŒò2÷a*ù“a,LEð°– ÇÓaöÓç÷_~¶ì6T[§É!ª¹Åyz¾wŽ!Ìýhz0¢MuÁ2Ž.M§q¸HWÛQH“ÀH…uèhåEÄ>5¼äïÀ™=Â!CÌÓ¢ÊW‡Çir~¶<;m³5m£“²Þèr#$4+Ì[¢Ýhðñ˜7Q—¤sA̦b-˜3-³UÕ¡ë 3 4ö±d ^ÎÑÇš‡É v\/Œ.¾#¯^r‚x{„­ÓKX­ —Yé$À÷Pª2ÖÒŽà ŸÚ›iwê¤<Þ¬ô›¹w°­üýr¾P!ŽEãj¸ÁžÊPR'‚ˆ » ïï† ›Š•yHiâÓ&·ùáª+8~ƒéf$—™Ñ«p¶…/FôÚ€Ly»®×´S"vŽ•)"B”°hî 2~åŸQB0äΞRvª_JêFM°^.€ÞD‡H-ä‚Ððð/2¤;ò˜bÇ76nÙ.ïn_º²\œm¹O{6Er—Ûð£—Xº7í3ÛjË™ã Dàbˆ¤ùá9Y›ª@f-غ˜DÃÈ.÷éŒÆ~7ØÙ™ÂÒ_Ã2¦G ôZž%Gx!;ÌF ›NBK‰ÅGÏúËÞÍ€Aä3¯}a9¤Ð"âåYá̘lNGËŸõ»¼œ¢Ý‹oùüÙYþOÀDT”r: Téw#tÜq-ÊVB³ „ÄŠîT°h·t„& n*ê´$5€ÑÈAaB7re øÑ¾CxZÚÇP’ô@Ö2¦Æ: ®àð HNJ$ô›Ï/LĬ|@³BÆOÔFÄVãºð£‚5ð Êov‡ÞùFˆ23 Ï©WÓé„ITrþ囈Aˆô„‘Æ@{Þѩ¶’¨»¾' .- O†D$é0ùz/G³‰{úD¨?׉p)±ç¯º|¶½Žä„÷æT§‘“ÈØžÚê¸í—Žâ[b·€ÚN(5˜Œ$Y±_>DZÐxÇG®…”“«ѽÔär¶Õvé€âß`F|qà%ØZéHlçæÞïdÄÆÛö†÷T{’ @ £’Ÿ =ùöâãGC„ð˜9s«8t9 ´Ë\½ ? â0SÃÁ$ æ‡O¶'„ŽHÁ"·I‘VÒ˜‚Ö ²fä‹#]‚IÍE¹…ZŽS¤oŠIàãÀ¸ŒatŽéÃ|}ˆ"Tå°ÚìsÄSú¾‰š|å‹èH°Œ2ø .l<VE ßÂibȰ5C}Ióƒñ¢g]¦ëuç†L33¶‚²Øå#c¸¹¾e&„dʈT%%à }ƒÝõº7Øò¶\žqö]8ŠÐÿÊ»x÷²ûñI`œppíúC'˶,I†”ÝGëjêÓà’R(n·ž=ˆÐ–PÀPøÄs™Ú!f´êá÷i™êÂȺÑ&€”v˜´·ì㳓‡‹cÊ6êøaÈyuþV`8Qëål3awHo‚®À·Ÿ‰G½DöbqJü«s&Á€€WžZºÎöñèkw8eE3€àºëv˜,•ƒõ| ¢ÀoFÀ׊*®5ñÔ­ŠdƒOFç ÂQZœ@‚Â3 F£Ä…I·Ø¥éÜEÀAsr‘èIðXâ+|L({O*â0Ÿu~MÕɨ}©.<¼’“û™H¤¥:$•ˆB¢j–„ ª·J’4Ý#¢vÏG£Ä:Y"ŒðQ‘Ù‹lŒÀ 2a CÌÑ…æ¨"z¦E¦àrv‚9±j"<æ"x>죋¨,rs;ÙßÞÅ#ˆæ6›èQ¸s&Å‚“©Cá2W¬;äîE› ˜ç3aNO)Å–W™ðÃ4¿E÷ÈôhJ/Š Œsr)¶0É–ufÏX¨#9>O ƒÉ$4Ho˜€!3ßzŒÒˆv1ð­­dW.zSiÓ½€1DÆÐ9¬€ç‘`¦êv$,Ò ¦êƒU'ýqBŒTâ³ ©Âä…G/ðDß<)“$³€2>‹£âRm¡F… ד´:Å…À)r>}&Ÿ8.£ e\Ys ¶·x¬2†™ô‘ ð߀QÇì NÈ€,a‰u²— èãÉ€6,$ÐmÇ<²ëÌðÒlãÎ‚Ø V;tÖ6Ìl+FÕpá0Ç—/!é!mBûP<åp­|Þ(É€™ -HZ˜ÚG"VQóR¾ àø<R3@AËC'ßÕ%,ãÒ_"B%1`Ä,õyRš"ô!ïÉaÑM0Äc¡°Aœ“AÉ‹‚ñâ2É…ÓGÈ„|΀‚À¶Áx3Å´§ÌÞ +­e¦J&Ð ¸#<-倵«`ÅÃêÒf½PáÚŠï>!W2žJ«ûŠà&8~ “â âÇ;ÐX'”Þ·.9p‡ƒÑ¶'œì¶‡õ€©ð~6…†& ‡l˜r³"^Râõ(>dÕä´‡¢†>Q~r¶Â¬1 ^ÅüìG>ÎW <¥ËúêŒÑÈ÷?^)õúñ†Ó'`mÐbl°™2i±Ðh?:= w!ò`éîrf–œÔJÛ›„Õ 6ÀF“ˆ¸$¨&I†€êP2‰¨¸Ùroü¡óVqŽr>Ú\;ó¾(‘Ø%Ô–°©º!Ù¡ø "ÁªQÃâe*œ!b¢Kkd¹:Ù™1^`¦W`ZºwÅ€€8ÒöºÞT‚5Ø…V43B™´â‰Þ @0Úã}2'<,( ç |îê>l,Š*ãøÞ2+[•ãÞ|>MOVg§ÌxPÌå„e¸tYÒu-§r¢IOëå9xrͦˆ‚ƒß_wGë;\êb½ÙžàhžE·EõÌøêž‡5çeÀw8h5¨®Án¨„(°Ùx=DF‹EÉúÊÄ·àQqû5sJ8ælúÇ5Ñ cÚ‘2‘”öBW„r”sèª0»%jNhCU4l¶DLNcømyDwÆò —!³\lB`pYE®…=€n@1 f(è-¡^Åja²˜m7dc2;~ŒfŽVÛÍÝrÉÀäªèÍ·ïó ¯*¦&ñ`H”˜Äa¯ˆcŸæØé$B› ÉHVJ™b°Q(Q8+Úª“òÂð…ïÐgÅãŒA7â«6’ù çÄø ~+×cŽ®Ï$Órü뀹,tut›9ÇÒGä§àÝ&þÐdD(6 'óýi¼š7îÉq3ˆ1œl LQƾ÷¶œhѳ)Rd£èo‚·NŠ[SŽ®óó2—*®z¶=ÛeNŒÁQœ¾iSE€RŽ‚gjf9d/U”KZkapkï=œ?8^yƒh8±;#=`°˜{,N6“B gg3Xböübš0ÖŽ¿DwÜ}”ôh0ˆ–Þ¯ÿÌGBû>7Â6{«ˆ±C$Xy2ŠJÂôhÂâÒä8Ñ^ú>ÇïÂNÓN§™OÈuŸQè;E¶!å—tƒ!J µð 6©!zxm"б¨¨I·¸Ó,p t*lï 5 \g´I`§ÕÅKô9ñ¬±P$sä!V[dePx·ð ŽÙ6œˆÌ,ÛIÅ1N׎•@ áɵõA^#ÈÝ9U„^­@g¨NYVÌTÁf'ðà ÄB;›ÌÔ7Q€ çÔ|ó>QØoÞ;¤ÝAGš€A$&†P›?XCE\ö­û0kÆrò4”ÃìO4c™iÛú þ}‡DÞf­SV#ÙTE £,óUbÃòÀ&Ëvñæjp÷=oäå ÖÙĹ¼åŽBœR2e Þ,“-ò”@yR}ñc|ͰH é¿ÿþŸdþ¨âädÒ><+'4‘%Ô!¶§7à’=È,ÒFž§ hQ¸5¬ ÇhsQÞ„¡_…ˆà[ìXÓ37ÂZ“•à£Y'Á[Óà´ à”QÖ!ÞÆ¬û¢E†Êú.¨(Ñ!r¡Øâ<%F"{½¾¤>XÒþÊoyŠ1`†'ר?Ïc¶’ …o\(s‰4'ÑKá~Ì%!§‰)žàÊ i¥s:LâvÔ9´ÂˆÉ‘V°“äNqÈÏ×=Ï.2¾ÎŒ€¢VG£t®W@+ì† "_H`•%àÿ~¶ƒ`$Òb, ÿ¯ã„$¦ÂøHs≗+Øvûädñúý»Æ†at>ÆÁ5®`¾ÔOÎë·ŠºXlÍÖ­3ަ晲³ë ª¾\Ѿ¬5nÆÂ÷_~û.«×÷=Š0óñœr°qüµFM^†fìò{ÚÁ`g‹åœB\¿2(ÚÀ€’"ÈÑ0œ¨Šƒ ¸àv ·°"˜œÉE»#' ˆ`‰ .¯c°§¢¶&WÉ E°^„:‚¯sŠhÅZˆŠ`8„ÐGüh…S|‡¿ch0+TÚ(ñù{¢™C’ƒc‡2AÄ%$t1'p¶Ç -S¹KÍIÐŽYnhCð‚/M+N Ì…2†ûäŒX8d–ÍÔDünÞä;«³¾äÛï’·ê­m~îëpM] ·À¡—œØsÜ@€øQRlû4xýKQD+•«™Àx[x³'O@… LYààDJ Î1°NNoûžÚY¼Ž»X Ç=­ß{”ûáœ3õl¸îït{>%;ò `¤ÀÛoßçÛoý]ŽVÍTÁ ëÉÉéôÓÜ«mŠ(ËçäQäLÍ ¹Ì?²CÊ?ÉÁ²èL!s@† XÚ“œ†Œ% …†ˆEr–#S¨v’ 4b ûIÓQ lœK‰`#Ú1<›ÑWLl@¨ážxü]^ÔÁ&Ód|zœl-æÀö†ÈѯR¹È‘E%d(ÜI¹,#= pŒ´Æáœ±G0…È ÙIé¶r€\rWØw9Ná']ÿ²H±"÷šd«e£¦.¬”´M(¼›–æ=PÛ SÀ‡Ðà¢lI×·>À:y>V/ð8®:Œ16Ë/Á‡tf¹{ßg: îXÁïC RµFQÎ)`è–hé1\nOÖBÜ4sÅt©ûFŒã¥8KI Vþ»8æ|ŸA;w6Ã9gõtÅÿðä8øòÑ_ÿüg®]»r±ÓÍï~-§§&G°”' ÷À Ÿ›Æ£Ò!~ЫϠ˄ԉº™uã<8Fj^Ô¨¥÷ÃÕëáéµ²U,,ÄsVˆ ø3zòw€-È^ÎxE.äoèT2Ýc)§!KÙ~jEÖ†X IDAT²LPíòälP½FsŠtâDZ(¼L;s–ĤG˜1é'3æNLÆ3.¥7À¤Ù‚ ¦WÇfÓ› ‡ln„ª@UQþbtÐåÈå8¥²õÀµ Ï# áyÂÝ‹;ÊÛR¬’by Ý4 g ÛFœØ É @VJpÒ€dò~zìPWáÞ2Y­0‘‘c 1!Q0ÇPó"TçþPÞûÎÖØ"Ãጓ­ñÿ  ê xÿ•ö°ž @Å*ÁŒBjPí¯}û W2ÃÜ'`–gËìµÎ¼ð7¿ÂSœ­Ð…‰r°õÞ{ûø3/³gXyÆyó]wÒé‘£–¢t$Q¡)—Lϳ"À?ÚïpAø ª„3u9«‹ØGí%"P 2§ƒ)-H ‚EÃ8Ë)3Šãqdð¤EJ†~”v©‰[•ÍŸ•RVH¾ãáÙÙP¼„LÊ1&x‹ø8LœkdRdø#ª¼‘ËB|¨ƒD‹û †€v¢v£XgªÊí6Gs.³Œƒ0¢ÈccN«"AQäx#÷|M×—‹Ña0ÿÞnZg'Ñ,DUTÎr´.vÙ>Ü›‹EÔŸ\0æêÎå3¬_8z 9×\0›õh»¥¸ê§4—y)š }ù¶ƒñG JL»ô‡#(°§KÒÜÕÝ.ú]^ÇØ6º‡ªÎ’ÆcãódhõkÚT§~äÉ7©wç¤ÉñŸ5-îpf}B·Ø$v»ä= –ÙþÄÍjÊk-o ²À髤Ð)  WärÕJ‡¢¨Ó„.â€/ºLå5ÍæaíëöD…!GVgòžÞPH²ÑAP® …(_1]p-Å6SÔÔ“¶.$ Ïž Ì!ÇBjÊ1Þd89N·¤¤Ç%dÈ>1=Nt)PK° "_¡CyÆqMâÎ<´À¨¼ïJã™D §vJ½ÄF3¥ÏQ¯nãÇí(žIßœÏÌ9±zdÎ èGáèupZ£µGýaô^Š€ad£Ò*cJ6|±™ `ùŽ” épH·φ#„ÈÐ>¼›`b‚´í€µŽK„£ðC] ñÄ3Â7R–‘¦-Ë}è°Ð±K@À¨€X¾LíPÓÁyͶ3^õgÉrM¹½X-¹ñMr³ï¾[üÔ-"-Ó~N{íï§/ívôæeÁ{Aa°ÝLA°Ág²ˆ aŽýxŒ3k‘(ë²^Ëi=^'›²Í„–¡5ð¢Ñeµ]¡Ü#ÇHç—â]_p“ó,YTiòÈy„Šº^FõªRÒBQñÎŒX¹þpÊ(óg~ü/üŸ¿ù0%ð­]VÇ_‡TÇKøoÆ‘šóãSý¾ 1&'çœò5DµZNâHg‰“ÀõÁó\ÌCÈ¡}D^JYÓ0½Á$iî0:‹<˜zOÄÏP|žfþª $áË÷ ³œ¶"dŒSðœ–ù‰Íàÿr>‹®ÇtC}Ѷ–Nʳ±€TKÐn:h6”æPç!ÚqÚ€°PL… EÔ"ý%l“óÉd™°¢kÐ ä}–r/×%³‡ M¤”HºYÓ€$É–›Û§‡5’dI ”^0í7îßsºg|š¨°DŠÀ‘Òö ù‡ºNÏèÑ2»<äì^ŒÖ™$(Õ7ÝJ’`£hÁq58Ä‚ôWñífR õÚáœBާ$«äÔ¢ ±‡¶8 N£8; ¯â¼[ÍK<7ՙЬ!ŠÌät³š¶C6¹¿¸wfvöa3>ØæWÿ®2žÅýÈTãÙéy*JŒ¸O¨$Œ:\Á[|{¦êë‘4ç2˜°½Ø‰á8™÷äùØ9þƒyõ›ú$:f«G“— ø&µ8~ˆEÆVBôõèCЋÇ`‘o°D´êˆ‘•åÍäm†Ð@žwA¦ªÝŒˆç0§ “†—€æ¡Ú¦~¥WD™Â±zwlŠ*·µ¡ì—3 8ø¡Ârü•¨ÞÂ%mãë*Ü ¼Aö!§`V'(c÷ÝG¯Öæç`ðA¿ \ËñStW't¼c³UOØJvyƲZ¿rÿ..ˆÓó`òJ…È‘!rúxŠ U‡&¼±OÞ=z 2# ¸¿Çç{Ìës0:¾ÅÁ M¼Q*P™3¿dl+ö&£é¾XG2Øë÷“¥¼ÇË·> ºÞyóïÊ“Sø£¾®§ÝÁJ1_ÁÁTUI¼IËÈp¨Š=CH„¤¥Ä(9ç^“Ûst'phBÂÛMìØ~8/V›Š*1(Ú¤œÚDƒÈ¦[IFÀÁÐBy‚Å1ž‹’Wθî ZduFÜOµË‰ž¼gML:î=pÄÀ4 ÌÜsöͰæü?fÜaÑÈH_™%Q©4Ïp :!h`<<í%Þ€*àŠ - d|„V1Âq÷µ$v§¼Àj-” 1V`#l7ïÞ¥‹`Ãm ÆN!2i±À•p f”"˜¹Ä=*7Îæ·r •00ˆÿøûÇ,/Óñ¬–YÈ8H"yÌØ‡ìàób§¼¿[N;:Eðš{ê\îž¼wƒZ’vØrà ÐhxÞÄ@[¢s†¦I%ˆŒUæ'«óK[—¿µËjÕ6ï=~/Äúmæ7œŽaŽ7å.­W9¼5IŒÈŒ}AÉxíTj, Pj!²šëÆoºŽÞ'Ÿ€¸» ÕeZ`9Н˜ d.©”bÊ^g)ÍM‘[Y4¤dSF¼›ÇèÑ-u;ªy—Ž?á W݂ˌ‚&§K Ã,4?&M—LÕ›v X µJ‚&PQ¼€ )²8rGÓ/¹Bá X Qÿ0Õ/T˜¨‹ébrý-ÅôIp>ß̮޸÷Õo ‚Ú%²ŸN©\%ïJYÆ´,ŽE¼©-ç”ñ\›¶¹ÞšK„±"=ד­Ë¼Ã!qÍðW—ÚöŒ°!QŽ(ĉÓxmäc9üƒJ$Š£[!K¢‘ôÇ– †`•{ŒMQes’ê2cú 2âZvŒ`©o^¸ñ\F á˜ÕÑé'gÛZú{Æœ?>ÞËw,CßÛ Ð\Þ¹»ýô-N5ægpú$¶kx'—Æ0Áä) tâ¾2â°Ì8ñ†Ub¸2‡Füæéˆx0Ø"ôÇ (£‘w·{bážwa2âOÀOîE1 ¯OFtÃDNëF{ïmšáðÝG›m oC¯SSà¨å=qðÞ…O¸rî;@g…re ˜g×a¾k…l†9µ¯Að¢8—q7ÈLœ¥ïß‘ƒ,…Qâøž£Å™4ô‰‘¤2*9Â¥¶›ÓSè_R'g?ºÒFL6Ê9ŸHƒ©õÙ%Á9¼[Õ-ÇBÓw‰ŒôÐÌ…»P›ê,ÏHˆØj-Xî"_Q¨Xaþx˜„DGás’=rpD™À{}õï¿%›Çx I¯à¬™r…¯ínQspxÊúÉ“á³Ïöi˜ÕïÞ¤ÞÉaí¹<,‹@+»HÀ¼ûàÑ•›/І@B tEìÞèµÇüiÉ/(°IiXqh¢œËbGåp›TR=¡«n2"$õ„C”Cd%šJ+]û"Rç ?X ;$°! ’ãÍ ‡| "èçd0Džù ü4¤g–¨'¼r £îð3Þ+âM&£p¼"Ñ‚-ãFp'xzޏàh 1!Á[TÑŒzÉ!Ææ.8æ—SO$ˆšØJa( ®ùâ>çZ˃„.–EºØÕ|“¦—够Хˆæš«4Œ–É2ó‡³IOžÌÝëæúøíÓ³üÎÙ Ô3ã{*|2ïìM&£½ü`oÃûTR)orgq&‚¿ÉPk"¸³xC ×¾ÌH_VKã_§]sè"oa‰B 9¡H ÐÊA¬s(¯‘:“KìÞ=.¢·Írwt¨Ç“àÀ‹GïÐjG–bUžŽ( -tô„÷ÍL 69ª³¡‰*g R Ë«Š'rIEséö!ÄÁ‰9PÅ»¸™ÃF Þå]àü4ú¯¨—ØqT+ÁL΄B å0å’àI)Að|¢;ba<ïA 'è2–ºnƒhg±fó€O,y¸IH‘„x™+ÂÁ*ºl´¬º„Òh…މnœšœ?€’tŸØt.ŠP]QŒ«Ð‰—”Tòæ* SsØFðÆ[ïr4†DlþI‚Ò4@Íš1ƒÐÝNY V+{Tî©yò¥÷Þ@2´ªŽëôöùê‹ò“ÒdæoI 9zö5ŽÁÄn Ñæ³H'øñh²€´úØÇ(rû]@wïÂáù°j|¢¡¤Fsݸ;¼‰–ÃñP˜;(oKNÀ§Ñ=<›#h ÓÖMèÎiÞWŠ÷zÂt¥‹¼z“톬Àtµ‹A^4úÉý÷ž¾¾G{ÛʉÊð}©„ú…ºáƒº !%œÉ“Ib²1\Á³)iÑ3·ª§²Úæš±ŽÜ,à‚‹Çi…H†pB7€;Iµ-«+² u²4Á7L9ænBILJÉ'R9;€9cºê¯ yI&ÛIΨí<¹tìXŽúªˆ !K—EÚví#u[B78‰QIÒ%ð]úÝCÞÀ˜c+XtþžÓdö/Ç“û“ûãmµ{peó?ñN¶¸YfgLðq;¡Z _ÝY6wsçu˜a²ª UÈQÈY,޶#Œ™ž0;ÉÝíª%¯¬OšÀ øzž´t®à*QoP‰2ºw Æ&¨Ll‘¢1*WÊw"¶tˆ x¢A>ˆõZ˜=6¡Ø.=È,D Äs hd`ÿ9M/} *QXbŸ¨ xãoÉÜ2kÈ=¸Ðáõ ÚœHÇ»MVÆÐ·HQö“NPS­»vˆIàGœŠ‡½Èë‹h<,c#UŒëqŽ6$ô¸ä’1T2²áÙa–å\»}5þïÿÒ¿²šƒ'»Î(¤X¨@^?ÍBE{^¯Þ,ÕÛUucÌÁ£m4´D,JjÞNS`0‡žø!Gä`¾„@NŽ8ká†/áϸÍͲ^z›{o½úF1¦n>¿{r:¦&9™Ñ„ALŠÎNà [Bpbgx[Uâ–LtËíÂyCX´À•Ô‘ò6eR¤=ž—;AK’=«?|íwy8ÞˆõŸù$[…ORè®=šÐZ¦:€Ç+’/ùL²¾ Ø‚áa޽½!pò‚4Káµ8˜Í8WÎêʵáÉЙ$àF!²EÒ¹”1˜5"FÈrvA©¼±¸ÈX1%Þ§£ýD|é§&ÃÄ–œë¹m­ <« I³çO–¦z­Hï6êÃ`7‚!èƒ÷Àzä2:Ë2q•Ñ'û³)óMÔx!Ñ5#^%Þ&ä-A‘’ýØÈ½éØKY®9z­³Gçå½Þèof*;ZpìlêœÉY|èÈÜžMµ…€:@¢Esä¡(•÷þ¦!ÄjûPÞ"õ¥ìYœž]{vGþœ¼ªÕ=mONŽ¥\–}å±PcâOý×üDÝ~üöËO¿lN yÌ¢9컂¤™¦‘*Åk¶á𠼚„ /ÒZ¤¥ 7pÞUUíO€«29cAj(Ž6¡“ãÀïô8(Ÿ`©$mC’ \7 ·B¾Òpª\ZÈ0c$xÔúG„”A8®†I# ñ#ÿ`ºjLNš#€ynÞûQJnÑã;é IGÊÒÑBé$y–Ùgðëý‡ÇðågóÅ9ošâ¹ÓÑà›iñ‡Ìr»>ò&‘ù{Ò:Vy€xTÒð&E=ÌûÅo€L˜ÒÐI¡-P^²Ò@”KÞè™`öœ wLé•s[M˜K79÷œ­iRs$ºhPˆ¡Ržâ¥ÈÙz¦RžwI‡8¬õ*Å^å0"¸yÚÅÆQB[ýÔdtíòÓü„<ºXd/o¢¾c Ù¦¯ñ½´\‰pü@ö˜Ï|¼Àæ°[ärl‹—ôÝ÷^è/“4p 1ÄÚòv+U’b½,d±¢Áb¡Ñ‚ 3å  )µœ×ı0¨öy§]ŽÛ‡sÛçèzEØËd¤Ež»1ë”g‚¼y"ûÖñN=ð ¼ýñDFz€i.E÷D\‘ ÞZmƒ°Gi“#¾!?¨‡·Ø EاŸG£ FÈ e-?ƒC&îpyW§“×#á8ØŽ6&øã¦þÈfà1§¹ÆQ‘Pcph‰œ(˜Â ã"ƒ¹Ãuµ"C€Ÿ`ád}«è)}ÌÇaxXá‡ö!'’A(8Éâ²±¸4ÏxoØx0ÜÕ—i”̤êT-/¥Iö—ø*Bõ(åAµÓ"Ôd¼·##÷OÀb%ÖéÉd„ ¬ÉP˜~úõÛ¼DA.ð•/¶ùb¿ñ[oþèñÞî6Q¿'’òö#ýå&9'å®Í ÷¨Û% Ü’%1•Œ¼'Ý)š6 Ж. ~šP)ðX¼7 Á5#dƒÜïíå#lÖdj`Iºä ·¼‹öƒøHÙX…bCXF¾!)a{7Ò¨~˜èBèAÑ)L6S`hTp£<Î.eJ1™ãcÀxîže«Óè„ àÏ\›Õ7ÓêkÕâ‰L½I{šw »ê4+ZÔdô.e.V©%ˆ#‘¸]SÁ'¢úZäl¡ÐÏ6øV|á 9Ÿ «rSIœä˱³c»¦R#Z!Ñ€ú^¼ÍEs«ÜOÌ{ŒGô_Ê'rVsòàøò¥MµFtEc°ˆ÷¨ÍhY•CŽz R"±Í n#"zêp7!–ÿ@hã‘ùð^­ Î@OD|H,9|LSê¢##Ù û‹[Jº]ÆLÏ›DQ™&(üH4âj^¼)C+$F!ª@ÝèæøŠ3c ä0n½@_^g6í'zcQ3ʸ'QW6™ÊªçÿmêΞl=¯³€ïyÞ½»ûôÑ9ò‘ÐlËÛ•ãJ…"T™›P•«\ÂWüAü T ¸bâ‚r•ÛXCœcÉ–tæsº{ÏóÄïùZ6´e©{ß÷¾ïšžõ¬µ ¶mô}}»ÕoUÿ~eûÆôRíƒý~˜¾é\&m?ÓUŒtàåfŽ®޲ „é¶Lz‚ ¿[iüÎéî frÜ¥‹qÓê/%»râJ¹jýŽ EP„4@?©¹ž<™@å’¡c O‹‹á3ÓÎ=›A “‼QN%‹­™ž/’~«ÝJúóù‹'Í NÙõvùAsÛkqW‚¯p ¬ä~ø" ÍÎßÈñoÉçûÿ¯®‡_“ö ( ^¤ANЄëáhúòV&frýâí²~&<ªD,/BCQÆ¿uÙö/¾e]ŸÍç£lbcÐïþÂí󹿱C¤¸±:¬ÉøÝo—º+ú˜pg½Wä—Do;‡ªM?}JŒÏ…’¹c¢^EðTÞZ¤ïoUo÷7"míÑh+FÊŸ#Kÿ}BJV}W.?ož¡ø’!êå‘ÓfQšOý6Ïeb«&à”:Õîj¬Œz×_—¿Õk¿­>ŒXH‰“(YY¼ì“z"ø`hëK#ÕnkAž ½Ô‘'$Œ•ý,˜¾ðA_Oä êàÙý$ÖæAµ÷Ó¹5ÞÌf‹Í±Ã‡ƒm@hªNÅ”:¨"¾o#™É"öÔF§Ý^£92Bµ²yï¦ì…ÆŠ¼NÛ–\eªçÒ+€¨-–”¨»ïtŒPóõ¤H5.øA8N·óŠs\ÄÉ鏭ާ<§ ¸9XZ®§gA‡®ˆ÷°3.çz,J×H^›ô•Þñ«ÍÁ[*²×áa‚¢OFKŸs;Vdå‰P .ËQZ®26ëX™o%ç]ËÒ¡ §6èѾ3‰LXy.7í„éòäI¢nèY(›ªKý½Ø,ì3DéЂiX㛺*Q¸•e:Uäó“¤7¨T!—¾<_f<®}¾ÃèwÓ‰ÿe·Xû†¹\³+Í’Õù°î&lÖ׌êi³ò•fûÝJ颾ï2O’)Õò±ÛÒ ¦Pla5D7¢@Ëšª7ÖS0„N¸1^×F57›±9§—»õØhÁÒ\:»†‘:Ù̆qì¬ÃK9ȤÁ¥Ñ¥¡óŸó¹E?:?qtÜÅr4i~õÖƒùèÿ¯0yøí?þÓOÛiG­]±Ûi–ìçfÛ¿ø÷çî¼ñ*û¶?€‡SÅE€‘+‡/¦»»!3¯Ït‘ÛÀE€ïµÓŒÂ4Ù ä¨|:4˜j]ÿ‚U(_Õ Çz¯ª¤»Àµ+_¥ƒ®àKmh´is—]êP-(‘•¾%CcâÌZ›mÜ1Ì$}Ò’Ì×ÎÎqÀëT®J!pJ†åcZ[óÂC V£!‰WJ×Ogײä} ›„¡ˆÇ䪄;šg"°î¶DÔ}¯Úþríxï¨ÜWÑÒáùnÕ’Ó7Ûñ€# `èAx@¼'7  2ƒ,*À²$T¾¼Š­‹:x=.9µçW×?þÌò˲AíÕ+´ÕÕÀ­ÉJ·1·›¾¢h™DÁ‰g^˜CE;Âi:1o·ßš=¿úê×_œ¼ö#SáÊlo¶¹øw¶ùÿýò|NO§–Üe¦”ÉJY¨sTªœ®2«5Λó”2CŠdšJ·ÇWƒLÆÂA\¯åjtxZ@Ë©ç~zÏw–µ²ì“5Gúë°šBaQ¢ü»(DOÒ6£pQJµ”Å å´ÐAIÒy@¤Š('¾æ w•œ®/üŠx–5ä>€»"õ«\`s1—63\}$´ÃÞ@TËɪ-D_$E`z¿Î( ¬ï¾R;}«qøR"+7øÉþxF€"]HЉöú˳–.´²Y( ÎMò¸¡` À5vUS¯ À(D÷·Y¨á¸žñ…ÚËqzä:jñˆÑº2®—¦S´>²€†!NÏž$LD-øälÉnÑŽ¯¿ü BµÃ#Kº]Jæ<»|v*êY·üòÇv¬Pn¿ÝQ:Ë–øw4}!ÒÙo€Ñx>»Õî&Ó#_*)¥1‹XN'…£+­kØ ƒBÿ¤¹OÈ:,‰ÍJ ÌjqÜ G#‰Â5]¨ß…N°Ì–Ó×ü§´l’ÑÚЛÇóô”qêIw`ÆÎ¾2k±H;ôJTº1ZžC[Ò+úœ§¾ƒãfel¡Ú¢¤Çˆ­£ÒÛÆêhø;+î,„°š/ØlìB¦«´ÖùËÚ`Ë´IìVùívõKd«2]ºêê¾EÛ’(T•n&¶ó0Á°ös&ß IDATƒÔ4j³9>œaõ‹iÓÓ®öp…RGZ*M7׫¸™Lt•ZÍ_h™\íÃ\?ú¯Yi¸@àdTäa• Iù(µA,â&”c]b: $Ö¨ôæ½—˜YØÿ|ºÔ5&ª_®õálö•û3"žíÌO.Μ“ʲû=ÿxÖ•Y‹òõ§Ÿ]¼û®öN\­xªÊº—‹$p,Y~ñº–ÔOj;óÖ¤Ã2E“`®æÕs—‰mG–*h„¶Âôóq?Q;š˜hgüãívEkº*%§/R‰†b±S`ذÑé\ñUÄúv§ÕºeH”käøG‚7´‰rˆLàSg· [­H^|‘±Ÿ•2eT3Û„L°nKöÇŒf’æ–V“©ß  ëÍètÏßèa*læ·áÊ ª×· _”3b®/×N4AXoÆS”³jgsÐóœ>ás8dXäÖ8‘ÞnX«iNå÷*RN¥º¨ÐaÇYíâ|Ðè×- m“5 ŠD˜€Fßr›1Iv“¼§ZjòÂ.öóN³Ï目Ɵ¯Oúw.×£Í~X«¾Ú¿xx_s'»èû#6¿‘ã7ûæOÿö'ëVûôjø‹Å8®Wʬ†×Ÿè•6:óòBÕãZ&S ³YOðÙCAÂÜE|{Èps§rF”:8÷°Xº“Æ„ƒ6–»E§uÂQÉP¤Ì¢"­Ô\—Â+ÔUL=?DáêU.õ\am'òÊ ‚þœX@u†°ºø÷(¿¢ ŒÛ×"â©9ŒË'N€a§²~ABØN$Ô1ÇÔ¨õ:4½ÖÚs—jµ7*å÷ºZWVÖÓq©ôDCŠ®Vçû "4ïPíž øÝäŒtòW áÕg@†_ h®‹ê ×BDŽic²|6,Õ‡‡úue3¤¹ìù¿êv L­|;äg‰´@_ôgÌeÆ3ö°ÈNųÅĪhß³E{øq Qä7ím®æ“[wB^RÕ¬}Xmÿä“‹]´Ç…ÈF¤ýB¬ýí~^R*¶Y]! ¼|ï­[íú•iÛ•c·ßoÔÔ‘¼¬ÁX¨ÉÙ=W²œcØêzwÕ®Ÿ.73 b??¨–¹D©#)Ò©3ÝDŠ 7»U3ãOšˆ{lµ‘*êQM&]o;MCpÒOR2ÓÙ5«p²Øu[˜ô窜׋y7á›\©Ô$ç°Iþ)q¬.Ý®»åÍI÷F ìpBì³S üÖØùåÞ(ˆ1ÎÅÛÆW;ƒÚVÍ÷þr3ë–x[»fË4.µð-a´˜pÍ,ªÎE<Ý™qvTÒ>O—ny¢Í|)«ïÐZwÕ¶¤(ˆ¶z9¹üx»r(]Öµjln¦ºVâ¼Tnß¾ûOoŸÝÄÖˆŽI=tœÊ„‡–°Îã<#óQVÕ^Ù,N7Ï[YÅt¨Ø-/Ÿ]Þ¾ÓÙ]Ïû%]6k¯Öû>ÿ0‹d,}€ N˜SÝ«2%¢>Êßij¸g÷ ž–Déj©²FY&2Ìœ°1™¢ài†ç+ÔzµkŽÓʕУøL‚.ßÕì«8ßv×ÕŽõYH Uô‚–·~6ßüú8šé¤æT‡``=P*$Èå$àÎiuP«]T[âÃLNƒL§“ÇIO†®š2þ†Ù«ø©¥.¶[çŒFOÂV-¿Å äiLË›>AµøÉ_ýå;=>yöüª´ûpþÞ—ßùn©þÕÙ9²È¢à’td›XkYMÖ¤"¶Ûá²ô׳ùO^lï/Çï½ÔRêå,Ö+½‘qcδ ¥ÉÙò{ãÅmL´Ô‰0¤Ó©(6lŽxê#2‚µ wcðÅ–z•^”O”,÷y®$–“5»|2D";,𑱮R“õmcÓ¤-žµnÍÀq¥95ÁSeÛ>šlqN‡[h}^Ïlúй{ç<˜µi?Ò£é:"ñí(ú`ª‚/ÍÙî5óÊè)(_ùx¢mƒ­ÐLOP I^€&•{Ú(•ÓòY½£€ã=hÕ§;6àñfùp½®ŒÁáIéD)vC"‘8 ºD™+@+;fL–¤?¼@âÌs†šÃ ‘ ›ÅŽÂ&ü˜iu´&Z KY/ ËNR|¾\6þ—Ÿ>þl\~ Û¹X2£×ƵÊ |ë¬3ž;ìÒÿÒ”ëEõj5ºÕïÉÛãú¼z~1œ.ÿëhñ~ꯅžü#$ÑÅ{ƒ—¬ú­Scg©®qy…q9vôøYŒäᤗò¹iØBi6-uÌô‰¾$9ò‘ªW p9¦b„n÷D£wª÷°XÎýÄ› êMkðJ¡¦<ÎS>§Æ!Þ°mt…—ˆÐüW.‰À„ñE©*ÓPÝ3"Ñ@ÎZiG _4d€áøqÔ²OvµoÖ[.§ŽS YI´DZ@? T„­6^Žm/;Ö3 ê wËG"¨-?™­î/JOÖ3ðN¨À¼ÑôËãK` '£ßh^“nŒ X0Å‚ázã‘èN/ye´Ù±{ ôPÅŒnXÂjEqÒ]iKTQ™hoÞÖúáÃg—üÕÕ³Ï׫«tž‚¶ÄµX>/üë7_¹GÔ°ãg—«•S҆ϗ¿¾¾Ú¿P}úôòß¿Õíwd’iW:E…ã€?­t@Y£®<›?ùè<}þä³áåË”‘ËKÚˆ(Ùî ñ_Ø¿û‡íýøþ'÷­“TP Z›Iꦑ…ù¼í_^^ïóZïßÿê×cü«w^Ö½ÿz¥cØ‚}}«.·ú‡¾< øºÆÝ6In;¯;Û4º•’DxƵæ{º;†ÃarŽE¦»ãìx<MàL G@V°^E·ÉB”±÷f áR¸eØÚÙDCÁJÀô­TÈV~4HéVÎ$5AÙZdÇïÛî³ô·ÏzÝn¯D3ß.Ôbë®0+Ø”†íB2•ݪËÍè.¶‰sR‹“¦Â^X…mÞn5¬½d¥$^s ;ºž¯:÷»{ëGÿ’;V‹¿ ÐŽÎL0k6žHQøäWŸ ’"oÔ!ÒºÏKF¿Ù«n{õú½V÷»ÝÚ‹Çßÿ賟?m wfèîîv[çíö냣²rÝ× —úçƒRåßýâ>×ö÷ï``ª¾¸æ›ŠjÕ»»Mc2<œaÏòR]rù­—ÿÎ|u æjî6½9›)f-H"`Tòù«R7áTÆLÞ±˜ÒX3cüô'`ë…jíDDÌ”Òw±ª×‰¼»\b™ll5¦(¨j©ݵ÷ößÀÃ_„€Þ݆ì±Óê/·»Õö©L'BÆ©~/j;#<±äjD‚·¹#Q wWI”[J+G¢÷Ý›ZÅ­!óÕu÷&¯ 4'ÄÆM:V ±°Ù?ËÍÚ-(!óÖ´qˆ‘ ½Á¯ùkø.5¥[*=1 ÓÈÏy·ïÞ¥t<[ib¯~}µ˜Õ>úø³Ÿ~üÉ/¾7,O|»Y~íÍÞà;owÞ}ë‡?}üáÿúeïúÓ¯­æßÜ]«5‘î–$Ú™±Èîõ‹“–9¡ìaõÿ|tÿ?ýâi£[þÆ+çÀ‚݈šMóAN ÷¯Goö•RW~ðùTQâ¹sŸäÚ;Ù°’Ž«:Ÿ–O–ëj3Ÿr«ï;듚1ëñZ±©|OÌ!5}j®òzŽ„Þ+÷ƒ¹¨KXƒ0&ì7{¦IŠõd}´–†Ú1Þ¦©£ËA¢¦%ß ÒCqz,¯´Ûx{u›î¿Í¬ÞjÉ×Q:¾°¤§mOú¼bDW!´UÍÜ;~½&)LŠ8áìÁåÑ´¤9kí¶ŽjæPø‰Þ°¶mCÔ žM2! Ñ0f@³ã]ùò¸—|Þ„'*°³ i¨!7äÅÀèè/g¢Ù•ü7ÖUÀ½N¦ zb©¼ÍYï`Õ¤Ò‘§5JÚë“ùÿñϺ@£ûªo>©Öþó£Êùáïvÿl¦s½pÏ“”ä!­¸ Ü;=¾'³8þòÅèg4Ü|çþ]-_Å ÕGÓÒƒ1ÒÓþv§w§Û÷¥³‡Ó±1¿÷Z½ùxùë9?×rñÕ­ø0ß±?®FçÓµ„Vf+wм¨¢ ~ ¡.HÐF™e$Œo…ŒÅ,V7€nU¥t[h1’7O‹´u=ôW„9ð§éwç3¡â!²J¨¸×‡4»Ñ0ƒ >­®Z‹ùLä z’ ï¦ÀØÐ”&ƒ'.Ê[Pmñ±Ç\:•m ´¸ºøé=…:.’U#FIʯ´- ½CʉvoU_Í a ‰ ®Ž ÁÜ+D(iÉ„ÂO”µß„Á£¶ÑS³@Öƒ+Nðfu¿ð€20ÑÑ*¬×±Â ™‰¢—–Ô*¿Tk|ãXº½µp¶çû!w@›6ŽXeÝv|;›~3UZZJ÷›ktÊÅö£§/V|û^Wï´8¨š#•6ÝŽÈþSŽôvÞ“+[mNŒ¦Ÿ\þèzùd©¶ƒUO¶«\íœÔ´»hÀl¸ðóÝbz9=yé´¬°ŸÅ•:´Åµ3HAuœ0ÝôŠþn S„7ô× ÞX°àNž o ¼1°.¬Ù´”ñ^#üpŠÈݧ*ÆÁ(?DÛÅX·Œ­Içk—§³&Îu»|¿®ZU¼ÇF}¦A’íô+>g 8äA÷ÌØ«Ü‘÷\;¿±¦R]4 ¨b‘EÁÅÑØžKa˜W·U)_Q%]Z]žO¶6ÚUÙ>ê:A>¿ˆ·/=ÁãŒD»‚Úå&í?Ó¢!Ôz_³r¬œŽŽÝ:õ•!²WO/×µ¯×Ë=•Ÿô¸­l¹„÷£{Ó n‚—ųãž]­æó¡á¹í7Ï›ïrùÓç8e»7zú†V¡~ µäT§u9À4ýÖi®öj¥;O=m}«ÞÚjn"`¿W”™«Êü¤´û)•ž+&XU '‹Á9I²V5Dý¹ÿs¶[)¡¨j÷Hdª± MFËJƒ}ë--Z•3*{Å 8¼À{[N¤¯ð§´)—aÚî:W%ZÐNËÚ*t®j¹{(,½›CˆH’H[-xøKË"Ž>\ÄÕžZâTÉ,Bo\/;àk©‚&@¢Df@-o Ü|p‹¤³ˆŒ÷H›>%½N]À·°+ãû`ñ@r<ñ¨}CÌ3öJ¯$;4 ;ÒIC<•c‚Δ,zorŠ>ZGŒþæp5œÏ»tÊÕÛ•FQÙº˜­¡Šé¡"¤éõÛ‚&K Þ.WNúZÀ†Æ°©\œ¾½•+o‰ódi°ìÙëçŠ;TàñœwváDH®µÌ'_7ƒ3¥„BÞ÷Z»áâxŸ¶Ä›^DµN¢4ñŠ@DE¢o´*…-¼cJE I 9Á&‚ÉÚsZ/uÖ&õÚˆõræafù '!ôirÛçnõ?VÐ-Yl(“i‘eÆ[3â0Éw«¤7¥Ž³ dB¦e½|y15Rñ†}µ—üà|º¯fmëŤR¤DK®\j…1F ]³X+aYÅKBøNéG%)¼Bv?T®ö»úD"3ŸFEEJ€Òì1µÓ‰)O8çË‚áZ~çÇ+l2Z?¯¸Ò€É©:>|ïä)ûÀº²Ú¸ÎmʶâÃÚ|?Ó¹n?œ€´x† ¡ôéËÛ2.åFC7 vë;ý¦ñcÃ…ÙOGUÎÚZMv*T¶oõÚgƒº. ~hA¶Çƒëݺ«#HÒÃEéjýt¸Üë*LnãñôêKÂjþMVª‡n«3YÛwpêîá¬ÚŸ/ú½ž\’•zÂá„9wÌ7¨" ±¨C.å…Óº5®Óº¾ë¬ž`ä’T öÉ~h.C.¦ LçâYgRÞˆ!ËÀ€WTç*N¦†â­qåˆ_ÀUÅ2á êì­p¤Ü£“¹ R¾œØ BNTúkÂal„¶\ NߌCœ’=ˆø¹ÔýèPQ­5KgÏìov7ëøÐùŸ­J¬ßl ­ŒÈ¦ˆ@„÷¬·³Ót@ ŒõfÙ¸qL‚]F' «t4œ>>—®,—`•Úfãûäs¡2œo0ß©ÕyÝJ]‚<×h3y¶Ôs°ü“K9ù³n³$Û­ïvovû$PUe±H_hŒþᙥ%9#S¥yÑîζ‹góÊj2|±€·ÃŠZ)’©Nù´OÍ—¶µ'U}M“Ðzõÿ9ÂŒD ñ^ÌQ¢;7ºKþ¢ánecci³X)LÁkD=胦y²XUr¨Í)V·z„¢‚ËyDñ†Ë¶ÂÔco7VøW}×¥êYL¯ÒÉ/m\˜ìR‡kI )0DRÞbQ+,À÷ÀNåÞÂÊ3Öz¾0NÝ’ ®d™bnu%yÁ»a¿£s×xX®²圂€1¿QÇqØì7é´ýö8çÀï„UŒäe¤TâRx{¸Ì~pЂԲôŒ;J›4¸o·MX<^àEà‘énÿȼ¥Ýa~ýTät*P9©öî4gg‡š‚Þ‘RhÝñïö·#¡’ȎÂÔ"ǼYñ€YŠÔ5Y2íhG9)7«W¦­ÒÄÄ3yZÛp\›<$ ®Ÿ.Z0 ˜(zÖí›åùD†¨JgȉÎï\DUSþŽvÊ;˜Òr¡õ;€©Á~’ºLD åêÅ¥s»2œbqÊ ÇcÓã[c^¯Õ¢YÖÖ.H½AK¶CþÒ>[×´€È‚ÆÅ'~Î…WèPâ ¨v8`––„[Q ºN:ƵÅTéq…¸0F–º ÊÃÖ>’Jœ¸]Å–egˆf6³ØlƶÚÞâÇÝì±÷g˹Wn?°äN/žu³žâ;åóƒ:Øv‡Çõõà±õÚºß-'Í*E&09xk}FS‹ŽHñiôžïgW׌յ,ÁE÷Î7šm£ƒV§ŠV©kV K æ7ûv´Þþx¹-p椶ou ¬¨&°J·=·{ý~ yËNÖd‹ödÁu{ÒƒAmE«ffÈÁ¡ ËÛéz|¾Á”RcÁHA¹PíÐtÖÜ(ûM L@‰@"Ì'*rD¨‚*T »Y%]êï œxýNXÍ݀ݴŸðáù0ð@‹ÑGÖ–Ôr”êÕå¿GR†š6Ñb5;Éw6ì`w]^dz µ ›6#. S-à¢U#›~ü—æJF¡Ø°B”í•]ÌVE4,e£¼ØË¼ëæó“9©.ör˜€d©–OtÄNïAÙ´¼œ Úp8Z~Ô¶c*Qvðä·å—yb$…Ó¨H¯1Úƒ-¬/ÌaÐ!ˆÅàÌ$5W‹ØR|]êb E|£“rí¶Ç¿=å…ÜURn»û7u¢æ»Ø<ÿ¦¢±s3Å Œ†SÑ©J…öØOPú6»×¤J:ãiŽœR·oxŒÕo(ý[B#O1™Xˆ«ÇïNÇßψ(Å'°ú £æ+Õ´+•Ç>7˜Kê«#†”­[!Í…GÛ»%F‹ù÷àÍnyôf ]“}÷®ø"An¾ö|qñ+q'ÊY¼Å±²[yµÙîò„Xn§™fƒJ}¶UcÎ1%¼„ÓZq8 Š—ãÁ9Lloºí»€„($×ÛùPüY§*ñ‹a·IrD®g²ÚÙ9"û’ëIb¡H¸T”­+¶£žõšØáB]' °–ÝÌö²Ùx0@^ã÷|VÞ’ÇsJì¬ÙG·ÔU™ìº—;7ƒTJ+éøGåFŒ ± Ïn¤Ìk31² e‹©»0—®¥34ÅÎÉö£ýnø™P&Ì¥j+<[v¯¢Ý3èHþõ—¦‡N  (.º.=îY0mËÒÆ1Ÿî'U¡Ñ•‘°KF§pŲ>®~ÎZn5XA˜W—“—µVMúB£7GãÊÚÉRp,y¹|yjÕÙ l[rº›à~roŽ pÌ\S|•”<ÊŸG¡‚ê­d‰ ØŸïjÿÊ•›øY’oµ¢bSb¦Óz¤¶©MW»ÑLS†"(rwÅ~¹ì<’M¸ùûD¡ù 7eÿ e_Í/^àeÙż+{íî½½x¬ØØâ£¬Iž½(•â5ï¡ÃÓ,žF`E ¹4Z7œzñ=u`TˆZÎ SLF¾^ö­×`NwC9M-E³ìÍò°Ä£«í:Û„x•ɰàÜêÕúÐA-7n\XBÙÅA× ))ùUm¯1F†M¦Ô Ä/¢^n^ËH‰|ºÀNÜ‘Wc”È5?LjÒ*ôÌå|õšRcÈk,”±+:1~ù„4j1_".‚$q>8sÝ2–1)jÌtGêÍ, Û2ÈÖ-9yy’cÔµFC&–“ÅÀm%¦·kF  1€ã§mg6‰H$¦°Éܪ8o$3Ó+Æ%9Ò€õÞ¤]S§9/"&ƒ»y*”G©öJ³þ{ýîƒ5P5rÕ­¶Œ¡å$Ž^±\XÖøÙºUÊbk`f¸8*–"…D™8£~v3Ïð&šûí¬yD«—´PAË ‰†z+Þä´ ÝÔݱŸÁÛT‚­IKìz ‚Õœû'ÿ›;´ABcª@çïsåìEFK>U­…÷YótwôðªsجÙ"`CAÅEu×8ëd· w8‹PR¨p*­$<M¹v-UÓ¶3^·lSºÆ5Wë–³†±þï`‘z®Œ¼F #šÖÓOV•bµå~sÅÖúÓ~åy³å¶Ï[˜¹Ã©9Ì…‘ôZõ°~´΃çGEÇÇwæoü¹}i*i¡´éy©<Ñ$«ìx.''/–ùf9IµóZõVã5á‘ZM[‰£ÙS ÄCß—¯vu ù¿ç¢BE‡Uœ«'Øi:Õ€#Ý0@CÚæò{ë Œd[ V¯Aî‘–di%^q )@§Zhsu<çÀiN, N÷`é!z&p €Ðå%Ï*ž‘h¶8¾†FFVìJ³ÿÃ'ED;ü‘ÇÇ“ ¬ÿð‰æ;ñ¦Aø™D2‚ví‹}ƒMvÆ@äWÂ#mwìÜk 4Øú÷\àY?²ñgG¾î±;ßÛH‘$GSDŽØŒ¢‹$;îp9¢¸üŒÍŒØ†’#‘MÿÈuÃw×4ÒåH޲ßñƒ‚ý|Cv|¸#Ïô!îøñ ª" _àBÁà–Àþ¾ò!òPDæ Â@0r’ùhÿ´`Þc†1o1Ï0Ó˜dHÏß<|ü—®õGä |B´ú‚ˆ?£¡9ÐÚhM´%rÕEN´ZýOÛÀbÓâüÛV¤¯ôoÝú¿­F4nýáH!ÿÁ¿ûxýÝã¿m2³ˆüÿ0äêää6ÿôÿgÆX#¬!ÖkŒG¥£n¢zQ÷PPm¨&À‡ê@5£úQwwðo»þŒBD$;^Ùñp°@¼è ¢v…þï?¼õ7ã· e`ô AH[Àß#8ïZð_Z¢†2b µø;¿íB‹ ÞUF룵?#>F³ 9€4Z ñ¸Z‰2"ý'Šÿ9ià·ëíèݹ÷È=d·ô•â3 õ–‘âS“W;{ï€ïö»{*Ä2øŒ €š”úÿȵY_ˆ<gþ‘‰ Ï»:7ì½£ÈÑéCïÜ0€Ð"O;à‚@ ñˆPš@s`جaA,އÁ²@.(gA9¨5à*¸š@¸€Ç`</Á4x>‚%°6 ÂAˆb‡x!aHR€Ô mȲ„ì!7Èò‡B¡(è0tÊ‚NAg¡ P-tjîA aèôZ€¾Aë0 ¦†™`nX–…Õ`=Øv„÷ÁþðA8N…OÂÅp|n„ïÁágð4ü^FŠÅ’F©¡ P6(w”ŠŒJDe¢ŠP¨zT+²Ÿ¢¦Q‹¨54͈æCK#‘4E;¡½ÑщèlôYt º}ýý½„þ…!`¸0’ ŒÆã‰Á¤aŠ0Õ˜Û˜äy~‡YÁb±,XQ¬*²ÚݰØCØll¶Û‰ÆÎ`—q8;N§…³Áq‘¸4ÜÜ\n÷÷O…çÅ+àñîøP| ¾ߎÁÏá7(è(„)4(l(|(â(r(ª(Z))ÞQlPÒSŠRjQ:RR¡,¦¬§ì¡|EùŠŠJ€JÊŽ*€*™ª˜êÕCª7TkÔ ÔÔÔÔQÔ'©/QwR¿ þN DºwB$á$¡–ÐM˜"ü¤a¤‘¡1£ñ¡I¢)¡i¤¡ùLKA+L«G»Ÿ6ž¶ˆö&í í"‘.‘®„®…nœn™ž‘^žÞ†>„>›þ2ý#úyƒƒƒC*C%C7à #ŠQрћñ(ccã;&,“(“S SÓU¦¦%ff%fgæXææ»ÌÓ,(3–`––,c,ë¬Ü¬z¬¾¬¬õ¬#¬«lœlºl¾l™l lÏØÖÙùØØƒØóØ›Ø'9Ðv1ç8z89™859½939opNpÁ\\ö\‡¸*¹ú¹–¹y¸M¸Ã¹Ïpws/ò°ðèòòð´ó,ð2òjóððvð~àcæÓã æ+æ»Ï·ÄÏÅoÊÅ€C@TÀI E A`RRPMÐO°@°KpIˆWÈJè°PЄ0…°š0Iø´p¯ðªˆ¨ˆ‹Èq‘&‘yQ6Q3ÑxÑ:ÑWb1±ƒbb£âXq5ñ ñ2ñ! XBY‚$Q"1( KªHH–IKa¤Ô¥B¥*¤Æ¥©¥õ¤£¥ë¤ßȰÈXʤÈ4É|–’u—Í“í•ý%§,,W%÷RžAÞ\>E¾Uþ›‚„‚·B‰Â¨"AÑX1I±Yñ«’¤’¯Ò9¥çÊŒÊVÊÇ•»”·TTUÈ*õ* ªBªžª¥ªãjLj¶jÙjÕ1êúêIêmêk*‘74¾hJki^Öœß#ºÇwOÕž--¢Ö­im>mOíóÚÓ:ü:D ·º‚º>ºÕºszâzzWô>ëËé“õoë¯h$t¢ M 3 ŒŒœŒÎM û×/™(›2é4ŘZ˜æ™Ž›q›y›Õš-™«š'˜ß· ¶p°8kñÖRÂ’lÙj[™[å[½²¶µn²6f6ù6“¶¢¶mïØaílíJìÞÛËÛ¶ïu`t8àpÙaÅQß1Çñ¥“˜S”S—3­³‡s­óª‹¡Ë)—iWY××ÇnnnÍî8wg÷j÷å½F{ ÷¾óPöHóÛ'º/vߣýûƒ÷ß=@{€xà¦'ÆÓÅó²ç&цXA\ö2ó*õZò6ð>íýÑG×§ÀgÁWË÷”–ß)¿y-ÿ|ÿ’©ˆ´`p6àk i`yàjMÐ¥ í`—à†|ˆgHK(ChPèý0ž°Ø°ápÉð´ðéƒ .‘-ÈÕPľˆæH&ä#·?J,êXÔ›híè’èŸ1Î17cécCcûã$â2âæâã/Bò>Ôu˜ÿð‘Ãoô.$B‰^‰]I‚I©Iï’M’kŽP :ò$E.åTÊ£.G[S¹S“SgŽ™«K£I#§×<^žŽNHÈPÌ8“ñ+Ó'³/K.«(k3Û;»ï„ü‰âÛ'ýNä¨äœËÅæ†æŽåéäÕœ¢?j&ß*¿±€¯ ³àGáÂGEJEå§)OGž.¶,n>#t&÷ÌæYÒÙg%ú% ¥\¥¥«e>e#çtÏÕ—s—g•¯Ÿ8ÿü‚É…Æ ‘Š¢Jletåû*çªÞ‹jk«9ª³ª·.…^𮱝¹_«Z[{™ërN\U·pÅãÊÐUëÍõÒõX²®kQ×>\÷¼>vÃâF×Mµ›õ·„o•Þf¼Ù5Æ5.5‘š¦›Ýš‡[Ì[ºZ5[oß‘¹s©¿­ä.óÝœvÊöÔöíŽøŽåÎðÎÅ{þ÷fºt½ìví½ow Ç¢çáãݽz½µ¶=ÒxÔÒ§Ö×ôXåqc¿rÿí'ÊOn¨ 4ª6©µïnѹ÷ÔðéƒQ³ÑÇϬŸ 9=÷Ÿ~îó|þEð‹¯Ñ/“_a^eNÒMMqMU¼Ý0­2}÷á›þ·o_ÎxÏ|œ˜Ý|—úžð¾hŽw®v^a¾mÁxaèÃÞï>†ÜXLûDÿ©ô³Øç[_t¿ô/¹.½ûJþºý-û;û÷K?”~t-Û.O­„¬l¬fþdÿY³¦¶Ö»î²>·³‰Û,ÞßjýeñëÕvÈöv8‘LÜý@!WØÏ€o—¼È Æ!䛂æ¯Üh—|îBÁÎ ô¾:ŠvÀèbEqx6 ^J-*kê B.M í"½4ƒ/c%Ó ‹k[-§ W÷wÞ=|©üOé…ì…Oˆ<âŠ~’§¥ú¤WeÅäìä“êŸ)Ã*òªûÔ2Õ5Þì!h©i{êdè^×{e€7T1ò6Î5i62‡,„,M¬­slnÙ>·ûéÀâ¨èdãârµÞí±û›½K«û6OJ"»—´·ž½ï?_"É!`O _4Ür>ôh)Üö ™/ñ%r,ª=º&&?61.8ÞíÙa­ÕD•$õd½#).G}S#K+8^•~3£3³?k,ûõ‰¹“Ÿr¾å.ç­œZÎ_.X/BŸf.–:crÖ»$©´¸¬þ\Gùãó£&*¦+ª~T£.1×HÔê_ö¨‹¹RpõFýpÃ×ëô7o:ÜŠ¸ÛXÛÔÚ|¯¥»µóζÛwÚk;*;Ëîvev¾Øãð@¥—­wíáô£Á¾»ûï=ih,Š6!Œ<}Z2ê÷Ly 36>^ó<ú…îv¢Y_ʯæ&ó¦4§f^Ÿ˜Öœþø¦ü­ý j¦aÖiví]Á{©÷sös³óÇdf?Ô| ]T\\þÔðÙû ý—ÛK¶Kï¿þÆúíÁ÷œ¡ËÄ?dÍ®÷lÉloïÆ_º¢Póèë˜d¬+N /M!J)J%@-GР±£õ¦K¤/ghg\`¦cQc%²¥³ßâ˜â¢âVäÙË›Ìw¿Cà¥à²0•¯¨²˜™¸§Dœd¾Ôué~™y9´<¿ÂEw¥Hå,•*Õµ'êo5~ìÁjqjËëXéëåè_32üdŒ7á6U032w²ð¶ µŠµN´9j{Ì.Í>Ý!Ó1Û)Ó9Õ%Εäæèn¸WÇÃxŸûþ˜…ž×ˆ]^}Þ=>·}Kýù»ä¨‡‚ZƒkCJBsÂRÂÉ=Ⱥ¼‘Ï¢®F§ÅxÅÅÉÅ â>ÌžÀœH—„MZI~{¤/åúÑÂÔ˜cûÒ̦[f3d]Ì~pbêäçœåÜÕ¼åSßó— >.}>ýó ÝYõ’ÐÒê²s3å çß]x]ñ¢r¸êáÅöê¶K}5Ÿ.ó×í»RzõEÓ5ëëéÈîµv[¦Ñ§©¤y¤sG©íÀÝcíÕmí÷.wåv'ÜéI~Ó[ö°òѹ¾“£úžH &o e ŽØ=55zf7æ5õ<õÅñ‰„—~¯ &9&§Z^Ÿv}#ýÿöýL÷lÙ»ƒïuç¨çFç+’>|ôY$} ùþ%|)ü+ù[ô÷¸1Ë+&«´«7ý|¼æ¾öi}h“zkb7þ’à>d=‡}QXTZ=ˆ‰ÇÊbpñ$ YŠ5Ê>ªrê‚=- í Ý úN†ZÆ|¦f{V-6qvföMŽyήvîzžJÞ¾"þÁ4¡ha¢ˆ‘(ŸèO±~ñr‰IS)~iXzAf\ö¡\«üe…bÅd%Oeu¬Ê j¡š«:»ú 2MŸ= ZX­)íF]’ž¡¾ˆ!0ün4gÔ>¾—üú«ùo’:’uƒ@Pgð‘ƒPthOرp½ðŸëÈnÈ;»6Ò&òGTqôžè©˜äXîØ»qžñ,ñ‡êMpMK\IêNÎ?âŸbxT"•íUHûq|&ýIFCfv1[éîÄÄÉk9™¹Ay&§N=Èß›¿X_¨W¤:ý þlfÉlû9…rõóê”+d+Ūø/²WÓ_¢¬¡¨¥EV’ÖÏ«Çë¯6<½¶yCì¦û­S·‡›˜šÝZJ[ÇÛ0wÅÛM:¼:“îëjï~}û¯ÁCÿGÙ}×õo ˆî:=<õTaôijÏãÏ[&ø_Nʾ¦y3›5÷ÉúÛÊšÝNüÿª‘í¼°*ä#y¦ó ä\ ¯ ‘;°R`KÀQÀÇëlR  c¿? €x$çd¼@(!™¦%pGòíXd”W@;A²ãMˆ‡t‘ü0:äƒ=Ð Áü°>ìG²¼x%ˆ²BÅ£jPãhR~œÚÞþ.î[yÛÛÛÛ[•H²ñ €Îà¿þwÙ!c‘Z}éëÔ'ñtsçþïãþbÀtv>iTXtXML:com.adobe.xmp 674 364 C£é°@IDATxì `SUÚ÷O’›µIº¤@[(¥ )RdQ‹Š *à(ΰé¯â8 "è7¸ë¨à«¸ÕÁWpD6ÙªP…²”¥@ìB[šÒ&mö=ùžso’¦m--Ðå9ƒÉ¹çžõw3ýŸç9çÞËóz½@H $Ð ðÛã pLH $€%€2¿$€@í–Ê|»½´80$€@(óø@H $Ðn  Ì·ÛK‹CH $€2¿$€@í–Ê|»½´80$€@(óø@H $Ðn 0!Gæñxl6›Ãáy‘@H kH@$Éd²Æt ´5ït:Qãƒó $€¸ú@£ív{cÚ -óV«µ1…1@H $pM4R©C;í=Öét`Ù1‚@H \CB¡0&&¦ñ¸„̃Æ÷ìÙ³ñÕaN$€@HàÊ(((hRå—y¨ _a×$ ˜ $€@ë!€2ßz®ö $€@ ½¯…Áê@H kA­ùkAÛDH $pU Ì_ÌØ@H kAöׂ:¶‰@Hàª@kþª`ÆF@H \ (óׂ:¶‰@Hàª@§ýUÁŒ $€¸К¿Ô±M$€@W…ZóW36‚@HàZ@kþZPÇ6‘@H \-'óÎ ÷廢{¦÷ëz¹=×ü=¿sZz’°,;ç\Âàô$éåÖdmv —hÙpô÷\Eê½jßd:y਎ˆDµEò¸¸¾ÝãjšÓøõ€ŽÄÜ~ÏÍò¦l£¹gÿغö·=:!ò˜ÛnyÏ=êÜ¡9pà, OÔ}À€îõO6bئ“GOšHÌ€×]¦F”kVÓÑ?ŽšD1nî+'-xM[°ªf  #$ÐÊ \Zæ9€â?¬ùµŒ®?œÕ«‘eêe³Ÿ_·aÝ Q7þÍ›¹nÃñ"o|¢“þ”—}ÿÞ7®;¦L½#Ézæòj¨×¡ð‡e{Wlȼ·÷½ˆÇ—Éqþ×ß,nXbäë[_ºçrDÅ¥?w>È|â°ŸRÃ^%ǯ<5÷@ÌëŸ~pO÷Ëi¤a¯MŠãÜ‚‰S×V×6~ð·msG~½á¥ë‚~šìÏ^~{?ÍÔýÕ­ËîmÄ€ëòq•øÂËÅ$iÑÖo{7¢pmošs•ö&ü0’¾Úþm iÌ5 ×X½±4§ªpM`:@í@X Œµqo¨3î͇Pvਡgš"P¼ ¡H†š˜/ï?õõ„jy·(¯µ)¯ºçÛ «Î2tX~óeÕÐ辪 $mx7¯×îõâK™W ="†oToßtHKȶ¹Û'Ü6êr48ö¹g'ªI¿®b¯×®gR^e ©.qˆø»Láê¹ÆéÌ÷iü ‰¯üctJÕ¦¯_[™ ìþñæ_>¾Û¯È¦}ËX‡Îž[qÚ8üÆ @˜Ôã#f¯QB”ô"HÃÔtùÉ\£±x×#iÌ5 ×R½±4§ªpM`:@í@ Éü¹½‡­$åž{,¿þzxÏññýÓ•óü‹×äÜé_öT»ˆ<ñ–)Oí. ›NüŠé,9¸jE΀qS‡Ä ‰îè7_ÿ¨†ò„é9èIãn µžÛ÷ÿ¨«mßsÐØÉc»KÏ/û+´z諯ÿxúfCP yË¿[s²œÍÜ÷ÉSn§Žöód|—ëý½s7þR`r1òž{æé¬þÜk–ÿrÈDŒ¾mÒ¥vnpÙ‡•éwHlnGƒsÿxùåG’@˜x¼g§¾vÛ_·²ëhÙƒ‰=º“k~óÍ®\x×Ý“§M»;Ž“/ÓÉ…s?\—cJ|÷䑉÷ä’ÆÌz4MD<¶*›Ûæõ ˆéÜž…Ÿelσ7zðˆ Óf[v¶Àà$Ö¼÷?X©®9np²°àÐÆÿûå!~½`½ºZ”>rÜ=ƒË ö/øú§ 2.^ÉV ìU[ƒ3ÿý¾;Y΃FŽ+?¹ñƒ÷7@ï¦ åÕêõ+7–GõNOKv™ V.\O]ç6,ØxˆŸ8xܸ{R„Õ{¾ûø—ó < Nv‘ô»z7Ôx¨Àf®1Р×WhªØ¥ôN$¦ÜŽ›I5BuÉ®uó|¥âŽsóž¹.§„¦æ¬›7ï³í»vmßrŽ] 0ïXa³ŽGˆîàO¼¯R© gÎöEÿ|x‘Æmúþ›µ‡ÁaáÜÖ³fO¸VÜÎsÛsr¶3ïÅ·Á4Áéò/4@AÍž‡Ÿxe{.í@5íØ¢™ã>×°UÖ~0®ýÛwÑâó•@®êê]ßÌû«OÒ ¡Ç#)Úµ+g×÷¹çÇÑo`L9ßü—Š™&ó?»víÊ—t¨­¸FŠ‚$=õH½Áh¶XŒ}òÏ bóxù¾™há4!ý…y/ÐIäÎeGaÂqVoÏj¿›ÝôÐyn?4G6äCÏÓ°fɺÜêjñ®oÞš¹–.ö× a˜° QŒ¯Ìû†åP²nÞÌ®¤.rªNÍ$èšÍÂaf‰VçîúæÅÇ_Í…!…l=ÄXBTåk«:÷³Ÿ¿‡½Œ¹vu{†GH ´W—–yð_*ï:neRG'› CF õž#eP,Z ¦JŸ>片úûœç†©ˆ-wÛiK¸tZ‚/ÝÇÆãóþÌÌYJŸþÞ {bÎ{ƒrþÐ ‡›Ï'Ì=/ÍÿûCÃ}bÎH*|F‡7úÑÙÓ ªLŸ6ùö®Š·mƒF¾ž5<ôÄk/ŒL&Õ{¶ýéà‰hß”ƒ§d¼öìߟž3-MJŒètɉ"ø£=râÆ=:;ãõ4•Jè®?úâ£Y„¤ÞVvp ]‡ðéÔÑcýcŸ= Ǫ‰nžÉü¦'dÐ rrr6,qÝ7¿Ÿ1–gÿøĉê­¿íÙúíÄ$z@ˆD"fÄÄë2VS!Tþü§ {¶.=hPRL‚ú¬ÍùíZyvñÖ©7*µ“HØš ?_üí}IŠ@¿-ÕT Té¯ü–“³ç§·ØlÌtÝ#(ø{2zÞO99¿Í˜É¹í½Þ0-Ú>:òÿ^f·Wœa+%‡^°ÿÜGÇ1iX/GPõ< ½$iL2c $›=]G±C;\¬aËw|NqŽ}Çð;h夸ó#åô áŠC'yôP"á°ID¢ú|ü?02‘Èhv [NÁ@‚C8&†F¼òíž=[çqÅ¿Yqnü}hx*Ð(kÍ×^Óò]+ÖÑa¨æýôÛž Ÿ³Wþèþ"mèÖy Æâ¿(ðó(ÿƒ«jÐG?Áeüíóg)¸ío­ xüÙ^»à!c 6D€þåhJh§½ñ8»,_~|Û.½¡²Z/ú5ÇrëXh‡pÏ}7Ôè©7ýú{G2Y?x¾¿{¡ÒáýcèË ßP…Ôûnpë àr>±àË©žÍîž3oæÆÍ ?,©¬¬,7Á¥Pä²*õ`w;z½Ñ¨Á¬‡Òn‹Õë ÔÖK¸®ÙVèp;½ìô&õ–¾ºšHg¤ÐYÅî8$•ì.ûñ½£âSRn9g^‰É` 6è­9{uÌàGb,.h9dû¬6]Þ]L î9BS}2÷ÿ’ ‡ØÓ{NiJ* š4ñ½zð­î”ñÿ½òµMB°0hF¯—'e…P»é±»7‘˜î÷øë¿ç=’"2ét„a³ Žɮ•Ñ·Òj@ä¿ýlv¾Óå°Y¾Ú#Mýiq·µ÷¾ôÄ÷ŽMO«—\”ýôå4áöxÎÙëÖ²=›ÄJ"¼ö0-jÆß sºMÚÃÔê¨8ÓpB]tæ\ 2b@'˜¹†G}Ù4VÛ½ÜB=t˜ªŠè·ªS' á8wr ="7%ŠÍâ. cÀpãÑ‚ÛF&ûŠÃ9ö‡¨×ë¬ÇÇÎVéܯ‡ L!›¸ÔÖ§/ÁDõìK§8,îá³ßúdÓL-qòÿ/6ä)«\÷|G^¯ÓA/=ñÊð8žÕ;dÙÖ F‰PD1âPWÄÛ`,µãô:-´*ÕèÉ·Åñtz2äÁɪ/a[H…¦k¾i|ÃkWgȾ^á@í‘€ïÏ@3†æÜ·›Õ0ÝñÍ›7ÿ~ ˆVe±'þeñ¼ç†¥†–;°þ×þ¹à÷²à<Ę»†ÞÞ7œÆƒa †ñ°çM¤‹wš…_*—Áh¨–Œ=zDzzJ'™˜GMÏØ8±ÁlwÚÌD&÷e¬û%JûÓâ·F$Që“èÎíXõÉÿÜÿÐ.ú·=ðǺ¶e^ž¹Æh2[>^þÛ}úµ•›6Ðw›8‘:Ãï zHnnäUÐvØí±}Y›?ûÓw?)ûhñ+âÛ¥ öAí£ïíÊ ž6A•\››OW5®Ë]È®GÄËhoÏlÿ;÷Áä‡?8›'þ8“N꺺Á|‰v‹ üÁ œ¢‘K0éíÖ›N›ÁÙ£N9B.rª^NðØ°—>éúH³Åá´y²è˜˜†8÷r,´^®*y’Ìhq·ÃèezpÍÕþŸ»áµãrà'@íŸ@³­yã °å¥iS2f åhYòWÌþ$ë}g§A‚a_nurªbÕ‡þ€Eq!²®B¥ûÿŽù¿#úõVîÎ>™ïx¨7 8~ç•̰©)Ç‘N™Ÿ‘®ðx<î^ÙçÀžâ¶£ …Ô¶ò× fkPx½½ ôAg„O¥Têõ õó²gد·`û—ÿ-¾ñõ³' ø¼ªƒï¾¶T_åØ)§2o¿‹$ß–ìñšƒ Âiÿ‘PÌ×ÖØ¡¿qC†%‘•ÅäpA•·[‚ŠÖª&¾·ø…°7!gÛ.­ƒ?°‹—ê9ôégGo~'-F—µ vìÑ@{å¯þܾxåÁŠ>ÿXüÞÈø¢C»þ÷éבê :‹7Úç‰`;>®ê°`CÑÒ”³{vÁgҳ߮ÚÏSº}%ÝßNìõ²õ„Fýó(¯W®E—…61iå—Åç@‡“nêŸ2fÙGŠ¡ôÄûúÚÀe1^?¼ðàÄø÷s_-\òt/˜ê˜JÍy=Ÿ>°»Üë<»y - &+Ýœ@ƒVK'kŽžûû]RNصFè·Øtb‡šÍÀϾvX>uÖHpwÂ3árún_ùͷĉ4¿oæf€Þ7ž§¸ÞÀ§?M q;|/XWøÐ¬D¢Ë6ñgBžÈXÛ3ì ?@ñ‚çî2QD4Gv³ÓŽÑÆØÀÅk¯]íüFH }h®ÌŸÛ·Äûž»ûWUUq„DÝoK!YꃫÓdr|ÅWÉO?ÙÝt`ùšS„¤ÜÝ_LNÒ?1 Ó½T‚!ÀYîo+ùæ4’ýûН~œ2þΚ½ë˜H·‘Cb<„”ÿco§ž’ÛVü Œþ¼Å›)U¦’Ãêb“ü5tHkXþÅS¥(ÏújÍI"<¢ßyškþø±öœsEù‰S_n’þe`¼¡è,\GÆR§1×3˜júµˆ¤NîlrÀƒ:û³M»nóºD` :xÒø;NßËï;a Ùô‰våÓ#òîì©Û}¨”ýßU™w ›¢"ÙZrdÎÄ1µµÅ‚ Õ¶i'ÞÁÉÌ-û2·lÞpZº¨¼ˆÍª¤·…yíTíÈϾ&þôß·‡i¥«Kmµl¤K²üÅ¿_âTl^ BC,÷ÖŽŽý£÷÷z^w¸qu²&ßô(!Ÿ@iÕÐ$‘W™žD²©R' »AáòÀO¦68, Ï,ž¸éé•D·nÆ£ëºuëVZÊËÞË=‰­<ÿàNš]õцmwSÎÿ{ðC03X¹íÌ}O_?ˆúð‹Ÿ›údYé1_Qèžä/ˆÏC;seýjBsB8&~ÅoNÓ¿·c\K#&^/‚_.WW¨STÍi†ñënHÈaB~™6í ï!V˜“÷‰ž {EÂåºaSXºk¦Ù{ç Âý¼T§\/ôÂB\»:CöeÂ/$€Ú#Z¿ÞeÎz<§Œ0i·õ¨õ;Ìï¬$®ã‡ªèß>©Ò¸añG_®È2帞îìÿÃ2Œ`†Â?(Ÿžä /ŒKu•ù&#císÒä{ž“Øý¾QÉ 9¼aùG‹³Š: NUש¯~ôì†7nO¹?\äTp+4nUݾañ ´`É!vÚ—4ï§ei{øÖCª217.ûiK¯”ûy ‚M†³o4Ö®Ô2½vè(êz­ý£®a7¦ÁQEE,ú“~‹¢c#žÛT© þã­ˆŽ•0<ñoÿµàäs_|ØÛ ÑXI\b¯Õ 79Ü¥ßúêTÃt"TÄFIÜ6½É%e\6=ì —Gª¤.£Æ`e¤ªXÏTSm(T‘]‰Æ%UÆÅ‚iÈs D!6k-‘ªžÍB$|³ Ô ˆRIœÝàÊz&v¶™jŒ ˆòè@•<:4ÞVSe„ç¬FJ«+J`ÛP¦Jì"·t¦Àßq¡cõYª( Lš¸nÓ³þX­ª6Ê#c¤"WU•Öá©b6SµÑê¨8ök¾!R=tä]±bÏžþöüªâ¤g~;*ÙζÅ#^›¾ÊèF«"Üf“ÖhiW¨b#¶šj£›"c¢E.‡fE^»ÎhÙ 7X7m×hƒþôÓ> ¤Êh9ÑVÅb…öNðùàÖõ8-Úš ] ž'°RI>l¢ÓWU…-ÒªEŠØHºÝã°hõfeL¬X£qªª¡· C„2Z*vkÏkY²,"‡¥ZO»©ê$²uËú*$K¥–?¯®‰H!z¼|¸Ã×½º|l"øQ¤¡èRX&›xl>ôÖ_qšªìD+—Yô:ºuD÷[ØSzp½ô$"2Z浞×Â5%ª„ÍPm´‡m½ÆVçZÆBD,RH==ø}¨"øfm ý¿d õ‹90vŒ $Ðú ètº.]ºpýŒŠŠºd‡/-óÑÑÑ—¬%dwé¦/>6ù߯¦Êe°»Í {Ø|áÒCV‰ŒH"bø^Ëj ˆ«H!äyܫ͂7Œ¹lˆAnÆó8¸x BF$‹<¯Çf¶°¹gBG¤²Xš‡Í–p;íB¼D*#’‰„<ž×i·øz¨ÝóàßçÑRÑ7Þqü8ëpñîO³ÑõŽzA$‘ TîÜNK- ʇŠ`í ¶R¯¢z‡4?So½—:lj‹aëÉ"Dt¢áõ8ˆÂf®s~$ðsûÁçêó >.’‰n׃Ï'Is~^t—R*ãà Ãêÿy\äT¸&¸t‘$‚ÝÈè´›k´![gó_t,@nCõÂ4ÍøÊśdzH ´=p3W“dž¹rCd=á2ð,;¬A~M%ê!‘®'p˜«þ_-‡Õ\›ä°ùÿÚB[Áo.‡µA þs¡¾­– [6T†ËLs9,uû!ê1zÅGÖ/¿Xž]|ü8½ oÐ+‹ßz¤TU5<'¥~ ÷ÁÕO£Ç®zCnÐJ¨BAiMÍTÔm~ ¾Š– ËÚ°‹¤ÐI˜Óõù„ÉV'9ôˆ*Øû— †}6k½u›‹œªSqý‡-ĈC·N‹^t,—O¯~¯ð vDàÒÖ|c|!ˆ1Ñ2ÆVsA_×K.=d%í=‘‰ŒŽ‹0Âìv»X,rÙÁaBãÛ;‡60>öw+$^gÍ]Ý_4,ÀO=ô©600ì"@mЬª7Éš¿´ÌGFF¶)m²³ ÃÀŠr­ã½M;@Wœ€^¯o’Ì_A§ýk;jÀå çrnGƒÄ¡ $€ÀU'pi™¯{õUï 6ˆ@H \.Ø£ $€hŸКoŸ×G…@H ÌãÏ $€@»%€Nûv{iq`H $€КÇß@H vKe¾Ý^Z@H tÚão $€@»%€Ö|»½´80$€@(óø@H $Ðn \Z櫪ªÚíèq`H $€ÚÀ3íÓñKËü AƒSæAH $€®4üüü&5[ðš„ 3#$€hKPæÛÒÕ¾"$€h”ù&áÂÌH $€Ú”ù¶tµ°¯H $€šDe¾I¸03@H ¶De¾-]-ì+@H &@™o.ÌŒ@H -@™oKW ûŠ@H IP曄 3#$€hKPæÛÒÕ¾"$€h”ù&áÂÌH $€Ú”ù¶tµ°¯H $€šDe¾I¸03@H ¶De¾-]-ì+@H &@™o.ÌŒ@H -@™oKW ûŠ@H IP曄 3#$€hKPæÛÒÕ¾"$€h¦I¹/’¹ sÉŠƒZâT>òâŒ~²‹d J³ûã%YNÕÐg '%™ï¾Õšxÿë3‡7©2‹Eï$²H™Ðy¹5Ô`ÉÇ+4NÙÐÇfï)„³Î‚Ì—f*‡O›1¼gýÌxŒ@H 5h)k¾*s]Niaaiiî¶lÍå ТQ«KK ÕRm~©Á ÍËw6©.çÑWgÏ™3û_-—[CÃæœ5 «T½fÑè§¥¤P«UæÅ$€@­Š@ Yó%Y9.߸ԙ»ÃÇS³·©á áK–:vÚ¤TgdÏÈ&U"”‚éo%2ø YƒÅJ-”ÉšÒ;!ñå6d-;8rÆX®GM©"ü`Ó2…oÏ $€@&Ð22tkv-Cmö^ýø;#©sûýE™‘©ƒUÚì,µH»›6sx¿Èpéµ5€Å¬ÉÏÊÎ"*׉ ƒ–‚ÝŸ/ZSh€©“œ6ü±§ÆBj~æ’Ekr¬PŒ‘¦ 3mjÊú÷—ji-Ú¥ï.™ñxJ½>þø‡Rn."M™òòÌ¡qÔ±ÿñ²½D¦R’Ò\5U¦Oš>õΰ®øÜe?h†ÌTu´A†ËX—¾,e°R›­60ÊÔ©Ó†]³,§ÔʨRŸziævæ¢9¸úóe¿ji¤i#§Î; ¨VŒ"$€h-â´/ÉÌ¡ìÔq“Ò¤ðmÍÊ,€/pn—‚ß={[–Ú"…鄵tͯfj¦È©-W¹:¯„úÉ«vÿëƒ@㥒!®ÂÜms?ßm9ºäªñL·”d%±ª³×Ì]–oÐøéVÁX·†W? Ï(U*è¡U½ü­·ŽRÇ~øã Õ¹¹j'M'†ìAzˆÀ°]yË25B¿3 TN8 Ô¥£Î.p ]†¼¥Ÿ,ÊÑ׿-ú`5ð–ËÞZJ5^ÕMÅkî¶Eï®ÎÑ(&!$€hyç‰,5íD·ÑÃï“Þ b¥Y™úÚn%?÷傌óÒ©ìÚºñ¨ÿL¸tö¼ß˦ü‰M›X+~dƇf¼0R)e¤陞–š6ì±éÓÆ¥r›ôœ±33žcMmås ^¼Aâk‡«Œ~&eÜ‚ß{/cþ0ÐV¢]±æá– ˜”¾üð½ŒyìÅpPJç#ÓfÎ 5®Y¶×¿÷À¢0•`Û•¦Í[ñá´Áô€I}{AFÆt6n±ÂìbëzêüP¦Mzéå·gŽKxivv¨Vii H $€.›€_N/»Bþz-­Y³p ),¥qëÑœ’Φ*ßß.?Ǧ§ª²³XŸúEÓÙ“õ>hy—0Ž~õûaÆXzºê ¶°@mÈËÍ¢G¾`áœò´´n E““ÙÄÈÁ©‘YЧ/38íéÂìχOºj۟Ūê9~RjÖy…Ë—²õÈ8eˆ>pç`,»Ki½D¦T§oç¬Å; ¬ÓÁûÜgà²kÌ.4é¦_IüBH $–@óe>?3j#qanŽ¿×ÖÌé¬ùjPÕº]ª©£½áÒý5Ô~»œTw9Å$U{?þd“%nð`K¦Úàê–>åå©Có—½úE6;`U•Êk]½äjÐhÀ §ëî¥%A¾8%ëµÍs1gBîœ>mÛ³_¦*™K—…è—?¸Îà8{–ëf·{ž{c|?}ɉ£Z"T¥Ôísýà1@H ¦h®Ìëf²ö{·éó^N•Á-ë¼o-ÊÒþZ1˜±°Þý꫚ÞRM^) ¤O%Öo.½ÁR†¤’œlkÞò÷æuN!T£ŒÊJ¥ÕP’µnÉFŸÆ3` ³’ª]ÿùºÇï©­(ux:ÉÙfÍ]þêÇG»9 s é"Àýc*ß B!®-['&ì7s\ê[kò|‰!ûP§@ÈÙðûS²Ö¨K³–/´¦ŽfÓ©FÞ9´_Èܘˆ@Hಠ4wm>'‹jž4uØ€X!{§špÀðát7›+ï€Æߌª›L_Èi|ê¸ƲO˜ ™.¤wÒ)N?ƒã²S§£þvð€"2ÝÒß~yøà‘Ã`†¢ÍûuͶ£qÉtE&%²ÔûShã¥yYOPm=Ǿ>i0Í»í Ác.6ííáqµ­°Y¡ 2¿w;¬÷7|Ú0n«½„ìCw‡7–à¸ot´¾¸á3§¤'Ãö¼ÜlªñŒjðKo³+õÃC$€@Í#Àóz½ k¨©©á+**z÷îÝ0CcR`?ùì/²•éÏ}8µwU•^K¿Ù}æ!Ó/Q§¥ªDk%Beb\à^z‹Fcªâ¸jŬ“ß7Q¤BÄ©‡üNð'Ƶœƒ¨SáÒƒ²` $€hWÐiß½Ã*@H MuÚ7w ^PÓEH $€Z”ùÖu=°7H $€ZÊ| ÂĪ@H ´.(ó­ëz`o@H ´ ”ù„‰U!$€h]Pæ[×õÀÞ $€hA(ó-«BH $к´Àãqàɸðò7³ÙܺF†½AH °"""ºvíªP(耚+ó6› 4^¥R±Û!â‘hÍ\.WYYY=$Ikîg3ûvþüyçñx¥"&§°âåo7IE0^xk $>†Fà,Ä|¾b^8ç…xµÉúü_†N¸-µÆbgøíßÅ orQ*•|>žŒöáèͼ­©xseþÌ™3 "‘µ¦qa_@> ÃtêÔ©¨¨¨OŸ>í 4ÏãÅœ^&ÂÈ@æáÐåñÖ˜m>O*9]n«Ý¥”‰%"Æívó㌌–GƈåFÐnÿ’ƒº³Ó Ï… är9ÌŠd2xzÚ½Ò7Wæh<Ê|ûþÛ£Cmš€ÇC_ŠÝîAè¹ÅÊZñá#Õe•ëöäWêcäÒ;ú%-~æ/Ÿþ7kÇÑ¢NÊ/øïƒB{ýmÀ)6pcLJJª¬¬‹Å(óíõ¢ã¸@퇩ÔNõ²¾z˜|úÔY'ŠóŠ+Nœ+HŠ+kD§ù%U†½«Ò°-¿¤ Z9¶†öƒ¤þHX6uÆiX¹¨Ÿ¯}·€5ß¾€àh@mžx¥Ù½u¶—½ãÌyí×[øt:l»ƒÁ§ˆaf/Ùòù³_|øö©kYëßçÇnó#oâ`A‡³ò›X®-eo·Ë0mé"`_‘@-LÀ r[îÓzÆÿÛÑ{ú'Ã!˜±°T/„=wïø;nÜ}ìÏÈñuñ*›ÃÅï *ßÂÐ[gu(ó­óº`¯@—O|÷nW)Þ»R­±_.à—w:!.Äÿ^q1°ñ¬¼ÚÔ)R{ïý›Ò.¿E,Ùj  Ì·ÚKƒCH \&ðÃrÛn—Û3 9îX¡Ly±ˆ®Ò‚ÞÃ]sçªjTñÑr³Í &>ñ¢9™¨[1”ùÖ°‡H ¦ðƒnj,Ö’ªêžq1íÿþ¸+ëDÜt`¶9~ÌÊ}nñ&Cõ½°B' =t{>†öI ˼öpÆßÙ¯wû¾ïÞøn_ù•kkFH \^ÂÝ– …ÿÉ<`võ»fì`ýò rÖaj,Ú1ËWÏ„UûŠàœ½|÷,¨iówØôYßíã¼ûvpõÏEÓFÍXO[¬ ¡Yìù72Vï(&Ü+«– ÔÏæ…FOì[ÿ.ÛÔ¨õ'´\¥ {U¿1ÿõæY_o~úËŸŸølÝ7;GË%B†>²Á³q¯z7±Á«Dà ʼ½|Lj)o?øúÊu+_°xî3³Oت¤â-sÿSÄŽ®|ßê-Å:•J*gíøéÙ‰ož¡õ¬ûôù¸ÏfOÌ8¬%vÝþ#[æÎÝ÷ä‚%ŸÎypÿ¢ÙëÕvb<1{ô3«Eã¬\ùþßEóŸûÝáº>y¶Kû>¿`ù‚ë‹í'$Ú YÊWÿá9K¼þ ùlöS³W+>]°`νdþSoÀÌ$t¯j{1$€À5 Ài¼Ëí®2XtF+ü«4XÀ¬·Ø\‡Î–ï8rv_~‰Ö`ÝwZ£•ËŸà̇¾¢M .Ø•oò :íË÷}CÈøïÞ¥"¤Ç ´Úÿ˜tö[ï›E½¹ïÄœýDûm!÷¾?PAŠv†È;WϪ7FåÝcÂïì»ëÍùYO¾OÏ¿³òÓûzˆÉÀ¤éó·üœW|kÞÂ#ä¦%ïNîG³¾;ççó¿Û7~à}ÔûÏõnÚÐöw'ÀùëŠÓÇæh=D©GfÑï¯Ë¸3žùßçn™ûΧ³n…xÌ´ù;WÃŽ½š¾ê‘@[\‹ø‰¸šÀ~µ†'ÕO>P,äþÂÓÛë `Ùƒ•ðÑC}-g´9zw… hÓ_Í+uÕÚº‚2Ÿ>ñ¤ñrßPzLÎxƒÞú| ùlç™ Id~1yþà€™Ó^Ä•dÏÞû¤ ]|ý}7‘…v=½))žSU‘*‰Ák/†\ûŸ‘îk¾b‚÷ðó~.&7ýÝ_UÌÀ$²f Wê¦xjì;qж|q®@¸^Ñna@H \+¬{“:G¿5鞦öœ÷M-‚ù[?+(óñ)1dG€@ùúŒÕIã§ŒWÜ9ëÞÏÞünUÍïL»š„Ì™ê+*¦g÷ÔÚ~fÇ’4JD¥W¨ "‡]u®ÛõF¢›¿hw‘ÝnÜ÷Ýìù«÷‰b¨æÆÙÿÙ¢ý7Í^pár²'IÔSLëÃòÃëßÜOÆO¾Õo‘sYè',ÂÇ|˜-«³Ôv±¸üÄΉϼ¼4O[›õ£ÆsUÁÚú‰ÍóW³kó—,\C ²WASŠ@FŒ $€®ÐiNªA¶ÿïšk;75Lì,Å7Aá)óòDPSÓ?]דAq¿Éãcެ&“‡¥p Cæ´S_:5ùU§/x¾(PÏ-ÓL¿UEŠ|g¹à3I,R¤LXùŽfâ›SV¿I““Æ¿ÿé¨4æ ¨jºÚ_­\¤SªVíY7½¯lýxè^ùÂo$€À5$À)}S;py¥šÚJ½üœxCÓ ~',¼>Ðx½€Çƒîd†"\b NÈqHgªnÁùy:Z„’m8æšš.±¢¢¢wïÞ 3R:Ô§O‘¨¡uí˦¼Ãjª¶t÷½›>»üõ] G'†ÌhÈnÔêL‘=hР«ÜîÕl®¼¼¼K—. sœø]ͦ›ß§D]‡ ë¥Ô;¼Œ9, ü kñññçÏŸOHH())Q©T2™ì2*¼VEòóóáBs­GEE]²WКçڋ먭½üð¢¥KWo!Ï/VO„ëå¬×u±BO×ñ k½ªë }>tjÝ’ ŽšÐ«e1 $ÐÁ pâÍA_½zå¦Í¿À £G?0~üD8 ¡²òÂï¿ÿñè£B<''çXî‘ÉSþŒõüy¦K\ÂîÝ»££¢a~S¥Õ½õfF$6MùËCP-¢êÿ<Áç :,ê+¸6†©VGÆ¿¾d»ù.LLFH öO€3ßA¿a¨“§L0aXñð"pèv{ ý¿ÿÝ0mÚ“:Ýs½nÝúéÿ|ÆårB|íÚu³fÍ>}:ïÞ½ï¼ûÎÿû׋{öüqæÌÙ…_~1oÞýê;íÛ=R @H 4ÜÑlj4ûÀ>zè½\ñÅ— Óúßøñ§óOä8~üxD±HÌí c=8°÷óíàkZ'Úin”ùvzaqXH ¶@€Sè±c)..]µj5Ãàß?¬,;¯¹®W¯ÿÝpìøÉÇ&N†¡,Z´@!«ªªt9à“/)='àxù[ØùôŽ;6›mðà!K—~óõ×_O™ò?‹…ù:p@§}¾ø8t$€€ŸxÂA#áÂE ÝŸ½YßÁm;ìøÑ£GÃûĉV¯Z íÿüߟ_yååÓ§NÙ–={öÈе?­3ç_7=?ô–[ûõïÿÃÊ•o¾ù†€@mV«Åh¬áª5C0™À‡Ê šÞ¬î¶ñÂ(ómüb÷‘@Í&À $|‚ rRÎ)==öNã!%0 àÎÔ;ôg÷͸R\bpÙz5ƒÒ/\¸hô¨QëÞ@¼žÍ›6=8jÔÏë×3ºÿþP|æó3Uª¨¾}ûeýžõÅ jª«W|ÿŸI=¡‰™Ï>«××p]Ÿ3g˜ûÜbüøqë×ï0ô½ê€‘+þxœÈ‡Œ@k#€Ç¹äát—Såú ÒΖç&ÁSH†ÛÕ¸™Ì¸lœð2Cž`/@  .{–U"h%HA¿ýò¨˜íDÐGpŸ¹dHáêÊå‹r§ ÚŽöxœæÎqàùw°¢!PLAH ´àˆˆh=ýiU=ýƒ]Ì;WH +ÒÀëè¹CnùÛ¿γؕzsà[‡ZJªô?9«ÄérktFˆŸ.­¬¨1A„(ðj,¶ã…å°Îá¤/ãóè>»âŠêGÎT—V›¬åZ=¤j´Z½Ig0”Wqùó¼ ¶ê§–}^‘Æjw”Vé7î?}ê\Å™Ò fkàe­ üUêLsö=zô(++ëÖ­›8Ì“e¯Ò8°$€@pÏ<¶k×®aÎwôd*´¬½îpºöœ,ŽŠ*¤"x)myµñTÉ…DR®5½¡»Ý麠7wU)ÿÔèºDEì9yî¦^ r‰ðDqE¤L •ÇÓ¥Ñòˆ.Qò ]ÉÖ/© œ•Š„½»Åj±v×ñuF‹ÞlËÎ/Q)¤ ôCûtï±aÿI(UReˆ…=:Gç”÷Œ1‚Ü"MzŸî¯æšó¡?ÏGËe:¡ ü‹½s”NÁ\Ag²ÞÔ3!Z.q¹Ý{O—DÈÄ:ƒ%V);We°Ú]£•ù7Wæ E\\(½ÉxWlGæ‰cGH ÕËå ñðǪÕõ¬utˆ³æÁC~àL(ññBMd„$*Brª´>áQ4ðŠ˜=§ŠÁžÝ=]z>Á2p »¦1ÊTóóZc„D«jªAÔ…Œàl¹®«J¡.«êÞ9ª¸¢f¿º$B"º>^e²9 ˜I€Ÿs¦ìÁ!½až«éÐ:T’[¨1Û¦àe€Ê3þ }àóˆˆÎx¥ZƒX(¯@~Y%ÔPc¶u‹„žŒ¹¥Onr¬P¥Îœ×ÝvC÷2­úÙ:H_›^4Wæ¡×ðèüÆ<=ÿÚŒ[EH K[´þÝÞ·‡¦Únö¼’ `¸Ã N]Ÿ¬çD™oß×G‡@G!`²J­znQœ[ðî(#oÜ8ÁX` žÉª#¤Kã µ‡\(óíá*â@Vg…ÕnC™÷KȼÕIŸ¦×qÊ|ǹÖ8R$€Ú3>áóE(óá®1'óp3€ —§]¦w¬Ñ¶ËKˆƒBH 1ö4nK9.Î×ÿQ¸“€ET{¿@ýLíñe¾=^U@½iœ 0tøîx.=b §C?ðÒ? Ì@H -@™oËWûŽ@Hà¢Pæ/ŠO"$€hËPæÛòÕþ#$€¸(”ù‹âÁ“H $€Ú2Üiß–¯ö $p% °7¡ÑW»²ïµ¡-±{ø!vòû^.×üö¹[ÝàÕtì o¾»Ýâ]Í$Œ2ßL€X $Ð> øÕ—¸=ÜÛiá­ðþow…×À‚úú'—ó^ ˆ¸¡j¯^NÏçÓY…ÇKàý äiŸ”¯ü¨š-ó΂%¯Ð …NgÜÔ—§&Ò—,ûx…†§S9îŽkßD\;gIæûŸoµ&ÞÿúÌá²Úäð1¶ ‰÷âS\…™KVìÕ¨†>6ca; ãÚq:B™jÀ°1ÃÄ©Ç^|ªg¨n„oÏ $€::ÎŒöx¼r‰€/à»Ýðúyún«Ãm¶¹ÝÈðe‹}@¼!ârÓ©´" Ü^¯Ýéå è{ãMV¼WLüŽ~%š7þæË¼A]Xj (T—LMרª ]4EipJ_ÚüRƒäå;Épšñ’ÁɵbTh)Q—–´’NüÔR¨ÎË)˜6ÿ©T_)­“ Ìè` $Ь…í”1[U|µ¾ÐBH×qÿžQ÷öWݘ)bx5f'(4#àöxqÎö‹K2u˳MûþPæ *¥Èáò/0ìÌ­:VPSVíx=ÓJuk7½Ù  7çhL·1O=Í–ya­Žç©5Ãã4ùy¬ÆCCÂPO; K;mRª3²gd½î„;ô·T!rÿq‰©ÃÆ¥Æ}U~Ö¯¹VBrÖlzì½Tî÷®nLGH Xc÷Þš‚ÇˉBLÜÞ-ý·Œ¹=%úÉ’þ’/fxÕ&'ûX9ê`‡'Æ×Ó@ùA°iVêiöh¹Ðîò¬Ù}þ›mEY§«ÝVõ׋b°éÊ‹î¿ù1Öš®ãM&Ðl™j1?¯€ +€OÂ0ŒËå[ž†üÌ%‹Öä€ôFš2dÌ´©Ãešü¬ì,¢r HJ2?^¶—ÈTJRš«Ö‚ }Òô©wöd‹6þCuÏcÃûÑìÇߟ»­8-¾æ_æDH öñø€Üõ&¯Äp¬XÏï$# ¹¬’»½dwžv÷ÑÊ!}¢_™”2zHœÓåé¦>|Öì¦_^ºq|”ßëñzù|ؾß ù›jÞ_™¿ÿ¤Ž‘ Jm\¦™õø©s' *ûöêd¶8>‡^›Ë!ÐB2¯JI!ju~ž… ÈS[‰*µ7ÉÏÉ&ÄrtÉ'kr@ụ$ ÕÙkæ’.o(O;Ñ”€H¨-(,-%þ)UR¢µ²X4`ȇµh³¥"¿*QEœÄp°6&±Ñ®‚@AH ¿‡ÜC`1žðŒÞevð"%ì9sØ$ÇãËPꃆ±oüë݉Ïéåñ¸6—ÍIì·Íá ;º!¿D(î:øk/â‹ymM-fêtXõ?Z›€1$€hpÈŸB†6_y ­žaÿvƒ)N÷Ü{ø"jI߸ë:—_oCŸmtªUš< …dggmÌ~XZ"ãSy§Ô¨ y¹Y­M¦J¤r/‹¤Þˆ„ô¸û—èC,û3©Ã†u“ †Â¬lµ‹¸²j† ½h‹x $€Â1|½Ù‘WlU¦ºn·—ƒ¸ÝÄí …è=oÂçñB ´þ.3pƒ=o¾ÀæQ·½›Æ©ãÞëôzD·Ûƒ«–æàKù°ßvâµ»‘+ˆ@DøBâ0yÀücø9ÇΗUdð‡¼~D04‰@ ɼӥJNU’lu®|ï©=ã`=2—.S\ÝÒ§¼T<ú™;—N†E_mxmšN …džšáɉ ¡›ì™”™ÐoÌÃüŽvÊj(ÉZ·d#§ñà· .&ÆÂÔÁ*ò«–d/}­$+…ÀÝtÔâ'†¥ N+¬ó³Ëù*|Yƒvù–Î~z)› >”éÓ?œ:À„ßH $ЀlƒãóŒVON¬s¯DFd á3¬XSÜ ¾t0²Ù8§êìoÔrÓŸÎ @¶ù2Éæz¼0'óŸ[Ú§OE¦°SP}zJMPíÞKd¢ãçjʵæî]V»K@3ah2:k‰›ÖS UISSYµ…(UøÁ#‡ªkó~]³íh\² R ­àD˜ýúãì|ÎÓ ãì¹ú)ÿÞÛÃ’iý¥jNã™ô)o?ÖÛßTðÍ{°º­äÁ~@ H $Ðd`˜ ø|‹ÍQcs‘¹é ÁpóXÛ ÃTàÙíó>[œµÈéfxN†yTÂáÐÍt—Ç$œðX•Q§…ºŽÕ $€:ix¶]µÑé´¸‰ì30ßAwYö¸©-î“ä wÚ#N¤ah¼]xKÒé±ÉÇä}<¶ Ô…ÃøR»¤Vžy»)h?.°úM‹Ã¬«€f£Áëò–ë,°‚@{ÐìéKG»ŽÜx›59ê˜ÈpÔH vL€TpºWÔÀ†8ð¢ÃÖzØHï7µ©TsqV’áƒZù¬@ûš•c‘»Lóö¾;wžÿñì<™‹¾þöÜAqjdz~~xnò@·ìÑMú>#žƒëqSO½—”WÙ‡ñA–Ðîçv|-Zdh(ó-‚+AH ´ðò9øÐTæé2:«ÊT’A‡!P5öÅ9µfSÙªùl0è=¥Öø„Ëû9éÔÀ 9-Ë-ϳ•P=«î ´6zžZçº÷–WY¨÷€ÕxøäÎãgã  Ì7žæDH ´¬ÊÃMp¼óÕfoö-ðœñÍYÛSÞ§ø~ùˆ4lƒâEÄ…Ý¥û‘dŸJ7ÌàÞ9¸9ž>HH‚f³»íhôžÝvOÕ­>`u€ÏÓUœì›p!8 Úÿhé¢Ì·4Q¬ $Ц På¥ý…jXAñ¦†6»{Žc:4Τf7`âûìr6‘Úå“Sø».•'E˜ì äðyÎ=|N€Xß´B-õ€Šs•Ó¦.èLðÌ|ؼy?««³ïòú†¥@Hàj Kë„çt³2OMkÎæö‹;UbÖò§rÎI¾O¡<þ^6ã…çäðac;÷gØ÷`ÇCm´Zö,L5 -FP¥·[mNx=ÊüåýPæ/–BH ´O`ʃÚÚÞ z¸í}ü ”zÝÙÀ©»PÌÞZV>õ¿Ã£íyp#ìÖ«ÒY \Õð$|x¾”¯Œ„˜ÐI¹@Do–‡[ìèsuàÙ84•{¶i¡°¢Æj²8"åbjÓûžÀãë ~5†Ê|c(a$€@G!j ÛìÍ6×£,O—“veÐi¡”X -rŸ;á©*'v3ˆ:¸ßá³D,y÷£Œ¢²Ò?öì`â_yr$?צ-tI²Á?7¼ÌÆMŸo­¢J¯£ƒGîˆ$2š¯êî‘ÇÒWãÀÃnaýŠ@£tÀÓ[ÕF›*Rêõ PcCã  Ì7žæDH ´àgø‚*‹UkqRs›Ê-ÀÔGšrÞ‘Mî gURÁÍnLéÞ;V#ñ &Óm>9üžÛXøÙœÿ}6¥ßxWÍ¿g­ÊKd ÃãIDL¤2*R!KeR™ ¡k´»uÚšSÅ¥ÙGò ù}©.ÜÞwAg¶Ûݰݯ¡À‡BÝuø£ùÏ $€Ú`E„8wÈE`wý‹æt<<Ç£aŸCG·°Ãn=(u'…Äår{œöŒ—w».ïÐá§ÿ«KÏÞú ËŸ™¦)>'í”ࡺ¡+èð~@ ô±zTèé_ *+åðF Š•‘•§NØ-A„ÊÀô¢sÝ)Ƀûéa®^(Â.T[ÍVúè!ÔÉ~rçªg[b›b[£‰šë´T„$€hµ@ Á†OØQ³®Á> Ìuj'ƒƒÒ‚‰ q>¿Jï§=+Ð`O3„‡ »¶£&EDwÎ?¼È£¥±¾Bóýì%Ç\7v}À-[†Úî ÈtÏ4ê“x*Ú<¬¡Ø,6¾€£Ÿž…yƒÝNø°‰ž®ÃÓB çô‹ºë!‘:í6·ÇÛY!±9\ô>NìéX¸© ´IµJ³?&ë; íb@™Çß@H ý`åÖ+dø!}*­Ëí…gÇÚ›Ëcµ»mN¸1ÎcwQ{Õdu—mð<ªŸ ÍŒˆ'öê,­ö‘'Ÿã‰å§²6÷:4¾¦üüÊ9³ÊÏä+»ÄKJ¹¦¬ºRG=‡À5 ˆ!J»× ‹÷|FètØ¥²haD„­Ô 3Ðf½ÅJ/ûãÛs íK#:õ¾AÑ¥‹þp.žZ&8«´à¡§:Ïn„jb;=ŽêÊ®·Üvvûf»¾Æi³ô~èQx¶®Ûf›ç EçÊ1_ªôðÄ É¡}ðX&“M"Yì6›Ãm09À¬w8];Ý(@§|žB*TÈ¥=»Fv‰’‹kÙ¹Š=ý½¢Ì·Ùÿ×bÇ‘@&Š2_er:\Þëâd1 QT#ƒÌ×7,ƃdÂsì V·Ëã÷¼Ûé¡«‡À¾NÚ9yù§¯ÞØ?íÈöõýîo3TŸ),ì>òa7XåREêø)vM‘×éà1Bj…Ã?>vê ψ„>¥‡ß3Œ]sV™6üæg^,ر©SÊ ½FŒ±žÍˤï¹ê?i£ŠXØH+¡2¯Äq›Ì˜…H$2e„P.Êe"1¸&`· Çm´8ôF»Ö`=Wa„ÇâÆ«" ?-…%€2?$€@û'¾nÐò›’#¥bÆîpÁ;Ü +̽³Æä0ÛÝàà x¡¤³S¤¸´Êæ…‡Ô‰ÀÉ:Ëóõ wNêÓwÆô~ñùçTãá8¿¬4[¬ÜÓùÙ—!“£¢ØVz rªÍlï}ÎÏ;`r‘>n4µï9ï=Ÿ!v‹9ÿ`—~Câoº2Z óœº2°Ýe"áÙ²Š½¹ùè›æÝðD}ªÕ å ÿç+5FÛx‰Ã ‹ ·<b‘ F)‰Wɺ¨ýzuÔ»›ÅaÕàs\ØKÀ}\A™·—ï›ûúÉ&÷5Q˜ß‰ýIÁñº¹Big,,þû¨à4Ÿ‘ôŸŒ¿Þ •ÐåçBml=ñº0Ý_´þ™°ƒ²È+ÖÅŠ¤Ô=üÞlfbýÆñ $Ð(%ÃðŠ+­‡ ô&› ôS!eb•¢¾ÝäÑ q„˜/bø œn^.SpÞLà­¯TåÁç¯ÿYR5ù­7<¯sB7xj <¯Þ¤» I Gvð*¯Ûå5VÑ[ï@˜©ÊÓ÷ÒÂS‰âR’áµô%ôðòYH¥u/#ôè+LGwò"¢a3ž×ªw=ôG&-ùi«ÝáàuMõJìcí!™5çÝ^©Tx÷ žF«ûn·œö°÷¾Úh¯¬6矫Î9U³ E„𦔸¸X9ûÊZhÎ0\A™'Æ¢û·Œ'“ëP¶«'Ü5åÁåÛ'§(Hp¼N¦°Eû–®Þwßtö<r¼zõÎ-÷¾^·•°„=a×úë Ùí°åœ;(ûŽYw½¹ßŸ?füºÍ³âI3ýµá7@HàR@ôœnOW•¤g™RÊÀ2¼½ð{úÞY”·Ï]tUžª5ôˆ•j(È'´ð <Cõu À!Øô‹So´ÁVü™p`ßøîqJÖ”G¯ý5\I™§­ÿ¼*cT: ³¾Ûg'å&!dþÄ);ŠŠƒâv{ùî7fe¬_•ÁæMÏØq‚ëcù¾UoЂl0žùLóÈÀxzçÎBZÑŽY\ùô «öqÉFõ޾ě—s‰å‡7Ï⺕>ÚòÕﯧA·é‰P•¯×_w†Ú¹‚Hk­í¿CKHÒßé'&â”û¾‰3å&ÒÌDÚ/ H K]´»<&+«±à(çÃ?SV6©sœÞnë ð<öe14D•}nŽBv¦¬â˜|0ÿºô·e=>gîÿ­ÝzAW!•€ÞG*r™|{ú x©|ß;»,VÃÙgØ-pÇ/#„áÄ-*R%—É4•º¥?n~|μu™{yq)Þ¾÷FLŒC×i»P5Ï|p΃¿ú -ñØQ°>L3Ì'Ì 7.~^I§=Kwõgêw,W•¯~fîìõ·nxÖó«gvïóOöSÅ]_­nçþÕ;÷ß»`å:rdá3o>e­{ãÎx{yÖÎ-bÎ%_¾o=‰™–Ê®iׯA=Ù`/ß1bâ›1¾¾rÚ@ÍîfÏž¨Y°izüáS¸ÄÔ¼¥ÏÌ}fvÒ®Uý‡Ç>37iü;+Ç÷ÓX m%]¿ëA_5¾¯ºÝÞõˆ"«aåOŠ6?3Ëœ+oÑ­~æ™)#âve?4@êtô_¬JI"Åo¾»þõ¿§–ÿœq„ļÓ/F,jVbÝ.ã@H ,PDNé©z‚„rú l€³ð  ûV‡·´Ê[åh€Äùr‚Þzy°OO¤ð |˜ßcPEþÞ/Wÿ²tÝÖþ=nHîžÒ³[—ΰîÿ·w€QUiß?Óî$SÒ&eÒHh‰ÞYŠHYvVXW‘EÁ¥½€`Á^—¥¼ˆ¾Š~šEvuXP ² „F2)Óû|ϹwZ’ †“ÉsÅ™sÏ=ç9ÏùÝ$ÿÓî¹R‰$˜.ßçYÅAA=†õ«ðœÕO³ÃëiÌ^¥5”–—ç–^º^˜u­Z$"‰ßûWö(b1Øÿzóì ? °òQy±Vc“Á†¸ ·Wg…ï©ËPM®:ô{ð@Ý[ßlM»ßKg½öíß³U¿xh?ò—´¡£båb2Ô&&V­ß:ðjX\—üê[ÿ³æcKGÿ>ùá­s7Êtöãÿô[ú;bï »;â¥'?&dÖß^xšÉ¿û•“c^|çøC³vAäg/¬À§ºZMÇXaòÃJ{X4(cèúy¶ÍáCÈÝu£®>ª×1£Úº7ß/–ê L÷$rØd&&Rgp£AMfO˜¦IŠ•Ñ/8’ú÷#'Åu”W“½µ¬êŸL/{‡é9¦ìÃ…äÁ'\YÄÝÇ÷#[uÿ…ȤY.«É}ð›–hÎ}6eÍ3lFŸõÜÖfohÜ1þéU'—½óÌìwÀFD¿?¯Z•’L› u+å´_°wÙ.’´ñ›CcÅ&ÕÅצ,XðñÙô¤wZ™±ZÊx $€Z̬COÛ3½Wo:ò}̶G}%qÔ‡U äð»]ä¹µ›&žØÓøI}atÕ#tÜM°¶ž…³ÚlnO$žOD €ñ|F ažTÎ×Õ:Êóáõ²v¼ØÞ¹ ¬  ;ÝP1gw´5Ym3Œáßß#îí5“"CƒéÃrtU> yîx[Ë<•<îag‡ÛÙ3n wÁ+ì^[YufðÇ;s²_ªìV‘E´¯OÒy…]iı)ä¤ÖujºzøIz("»âHéÞv%ÍZqñíw™7¦ê— C ¿6Z!îDl žÛŒOãæRS¿'wf¼@J /þýƒ׬é3œ!¯Jy,GÌêK#ÄŠ^£†ÿÎÕ= íƒæGj–ö÷ õƒ!$€@s€Æ‚M¶‰òýo>ðã¿·í;Ss°øþ c˜Þʳު%VØ’áÃÓô°Ÿ¾žª2_@ßÁnèCó<ªâðPh#’ ªÇ´ÿMGÒáì¹C 5DkqTÚv ßa†Uù0hSî´FŒ&h0œ8H(!.ltß„©£SÑU*ÕjM°[›×œêuÔ<ìTƽª¼ÆUPÙÕR÷`»W¸ðµmÇ40}öŸ/ì"C–öI„%l¯Ò%x¦³û6iÕ(VÕ¼Ã.‹00j<)|gÛ±ˆ*=»–µÏzlhOˆ¬¢‘&“æägËÞÙu’‰-4'™œ!šÒï>Sƒ ZwûÀcз}W{föôÕgU$69¥{¿PlwcÁ])—ÿ0²¯OvQOL¥¹‡?þÖãõ—¶,5Þ}0€@Ë €ü6Ì|Ã#éZ{èè©Ë¶L/<5~ãËÝ¿ê)/}ú½'?48¥_¯Ðî©ü¨.DÞÉÎÄÚxV«Ì΄i݈^($v«ÃlrX-“‘þƒ0üƒH»Õù œàc†Z«ô¶*½½ZG4ž–Ê3Ý;+@ÑÿôØ¿¾2íø'ó~þ⟾þÛ‡Ç÷„%€5Z·¸ÏÙnhym;Œ…6í̓ê9‡ì9žI0 /V<:„¼öâl{hi¯Xwx;°^¸k͘L‡l“_yu|2L¥ÿv×o—ÎRîý™<±šNòC”'LÏ¥(ú/Úòç‚gÖÌf !‹¶,ª“¥[V©Ü‘‹6~+ÝɃOyç™SÆ@æ¤É‹&÷Û¶í™Çzö´Ë[nû6ž¶qòçóža퀩ɯìL¡}uO¥–ö’;ýlhòøW_É^ýâšÙÛ ”ôàªÏ~Ÿ¢ -Šd-á@H u@ßœUzØß†åõUæðÐNëÞûÛÖ¿\›hÔ¦I‚·¼Ø§R­­T›Ujs¥ÆRZe-«1êmöŒ¬›?fñ‚ƒéö7tÝœkPºå0é§t‹yx‹|Àkèl“yø¤a ]äR&F! —FGHáEh0ˆ=lÆsf‹“«¬¡o²}°umƒ¸-·N;„v}bƒšÖÔÔpqååå©©© ®{"233ï»ï>xˆÂu!èÑŠ]áqaSÁÞ1³§ÝkVi‰L£éÍ:LU•ÖÌÈ"¼-@Wž®±“{5©TZºô4Τ1¹ËŸÛëÓ¸¦Œ#²º¦Ð}xWÐ “FS#2Y¬ÜÓoa¤·} #$à“€Ùl¾råÊ€|^ ŒÈìÜFE‡A_¼ä5‡ËÄ.~fSObsTh–ÏïÿÊRá±{:ÆN¥þ‡}ï…¿åß»þu•"‚·ÆƒÎ9zmxQ=¼ù6#á iZÈ%àÃkæm%åüM·_™c¶é`ÞÆnÑÏ»Ã?ö€„ôñ?ç3òPø…±­f+=‡ìWܪIKy¨¤¤$..®¨¨H¡PH$’vô3““Ã9vGÏÛ´7ßhén‡®0Œâ‚^ŠåÐoþÙc=ê´#7q±ÂS\¾«}—Ç6,Ð]©úfÅr¹·Às—[Y¿ 3o\Ü–^»\P+ˆ }ÿóóGÏ•<>©óýÉ!°7~­Ör!¿zÿÏ·¾¿¢%ñ‰v˜Ø¯(}sŠD›™}ìͤ£Á‹%v#°¡À`؃DÄtôlqiU%h6ì§ÃÊ94¨‚ÃÿBh9Ð%öôp6Y\Úîúæ.âç]øedÞ‡kâäÉ“Ÿô(¯…@÷‚íxÃ9}-½],à-™ÒyÑ{™µ ó\­ØN;í¶;ÕíÁß‹ tþ"óâØ¡/°»4m¬@H }àúÍ0#®Ò˜çëtäôÕ¯ß$(…AV‡DÃì°% Î ”ÑÐ ·›Mðšyb4ØmršñrÊñcê>Sož 2VCg½\ö}â¯~ÝÿÔ¯{(«l ?œ*%ŒâaߺzSíîκ;Ð>`ù·—þ"óþM ½CH t8\ßWk²íX>:Ø~ø³—Ih$‘Ix"Œ­ÃÞõ6£Žèµ$HNGì#BÓ¨øS÷Ý£¢¯$eæ>^ž_³óvÂXHÝ…„éDV!Øsb:¬„"Fm„˜" ‚Ѻ!}HÎÙ¡ïp”Û¾Â(ómÏK@H ´C ½ Àì9ì¿ã…É“‡fôÕ¹sò`y=}ÀÝaƒpBBE6iW…aäâ<•ý_÷\Ûíš1R{ä–Tï°X̶ $TB‚¢ßöÎ/Ö)‚ŒW/ —.…ôìv7íNûqe¾ýÜ+ô $po pJ»ÌÂÖ5ÕjÓÃzO—–_¤º^T¥Ñ[`kœPYpjRX¥žüzÕ÷Õ•v" ºnN߯üDEŸWøxæê ¾ Š [ÒõÀÈn—ŸÎ #ºr㛊0òʲ :ƒ™}øûñm{SQæÛ–/ZGH ´_ÜR8Ú§‡ž=ŸTÕ Þ-) FÙa°Ýb±È‚Âneœè¸9dð µñü«ööÛVöÖ`z|ò}E§_åì(›JNò{ñ³"ÃbïïßùÖµlCE¥¼s¬^¥b·Òs>ÚC÷mñ£‚2ßTÑ&@H pºËõé¡>Ü5°kÞh§ê¡—±û›Ã[7ýmÃÆ?ߊÃn^ñ¿§Þý¦»P)%¦ü®aѶ`+1¨51ïž½lzä·+§Cƒ!óÔÅçVoýóʹ¿}htEm5SûÎÇãqž¾•rPæ[(šCH 0VòaVÞ¡ˆgáÖ¯ß~>óò‡­Kì[VY)QžÈ" ðæY[µÚb‘2BX˜o7ªjíB¾õfym¿!½¶þïê•Ë>øï¹++×< è×Ôha]’‡ÒµÞÏPK÷´‡ýï !Özþ %$€@ë°Z­R©´õív‹¬è:w™…Gà¬V›DöÓñ¬Çf­3ÿï«7b£kk´b±6©5™- ÓôM6ô9;3tûésð|ƒ;Ü„$fª«Ô‰c¿øúu­Ú0çwÏÿôã"ÂÁ,‡"X‰‡ {(™ê=-$ÐÒÞ|rrrqqqBB‚k3»úƒÙ‘@­L€¾/ª´4>>¾•í´9Xö7:lÛÒÛít›‰P <6çÿvü£JU»lÙÜÑ£Ujª-f+À0Œj"Òl°ã-Uh»¹ ʼn^+ÚMt&›Z«7L"F¨ÑèFôÎ;K¿;rrÛ¦Ý{výçñÓúõK5[-Z­žË§/ªñÚ5‡›>hämU¹–ʼ\.W*• ôÚ۾䭭ÜG»H ;Éd ñðÇêN ;úuVÚ=ëÞAÝáݱ%(ˆ‘KŒfÓdz÷ÿýÈ­²ÊÉÓ~5mÚh«Ãv9/6 ‡.8lwãpØõb‘Zo$"¡¶²·Yª5F Ÿ¾šÔ§v[•Út«²Ö¨3À»ká9:¢¿u«ªï û>úô…ì=úÅ(3~7vа^b¡H«7˜L ïºõ8E(ùMúIm©ÌCa°uþÝìžß$·01@H ´5èr{+(Gµš 6}\^,f$ÁA0Ÿ½øÈáÌȆ—Ã?p̸þ"‘¨°¨ÔúܬèÂìNσîc„:¢S Är›A›.2JíDWÓ ˆ¶*!DÄ :ªÛþAA…%°™Þ„ÉÃÆŒxäÐéO?ÞÿÉÇÿ9ªß˜úuJRÂ|½Þ`4™ÌНê1iX…z ð´d!"$€€ÿMe%Ùí)–™dÿQi‡« # 3|¡Éj-((9}2ûçŒ U*u÷”ĹþºGZ’ÝAjª5Ðy ¡GN—ÇÃyá:èV¯üè?B¯–J¢âËò.É,ÑâÊraæßûˋÊ/”Ù¿a¥š–ä#½¬¬Þ]7nâà'ξ˜ÿÃÑs/úY:tXÚÀ¡½’’bÅB¡Ùf1L‹ š"\v0ÀÚ¨£ý\EX¿ðƒ@™ÇŸ$€@G!Àê1UeNÚA#AeaéœÞñNV»µ¼¼úìéËYçr/_º S±’¢šò«~S„"¡ºV«R©AÎ…Táë/ß¶ÛìŠÈð÷ßýìlæee´ÂV}Ãa7*¢»ÃjªVÊ%ÌO?žÿ¿÷>õôÌ*U ·¨žãš Ãà´' ¦[JBÿ©f“5ëìÕ~Èü×?3‚%’´´¤>ýSR{$GE† ùB˜/€!}3¬õ‡i|vL‚~ׄfGíwÅwÄo”ùŽx×±ÎH 0ªá®ƒ :íiÓBèIƒFÃÚ7mxrpÖù 5jÍŸþ4ç7ÓÇÀÚ½ëy7v~²†ÿéwùRñ¾½Ç&MåÖSz°ÉÙ×iêZ,=®ÿ¯=¼²ºæò¥üs§¯üí‹CZ1,\Úµk|jÏä.]ãcã¢BBdÐâ°‡ÕbÕ‡µúܤyªüôƒó’uÛÅ¥#}£Ìw¤»uEH p @¿DŽÏ§Ï-pB·—ô ôzÀF£f+-S•ç_+¾Y\QS¥µX¬Ñʰ®]g>2¾{jbTt#­f½ÞTQUUYQ óâÐ2-¯+™ùeD¢8óÝ‘ ˜M·ÛÍk_xê‰ëtèT¯\ûGETÄ_ßÿ,çJÞÅ‹Wà)ìi¿ŵ¼ ‚ÛÜ)4! 8𪠠4R&  I9¢ŸÅn)/«Ê¹Rx%»à?‡~®ªPCú°HyçNÊ„äØÄN1ÊXExxHP0#$”‚^­c&p€ïpÐeðÕÑG™Ü_z¬@‰@xDHTdŸð-vìA«×T•:•ª¶âVMY‰ª´´¢²²V£ÖÙ,6q°¦½âcÆO’Ü5.N)‘À ½Ùnvô³ÙŽ/í[Cw¤‘O½{õàA\áp£…0ôËuzý’¥sBùÛéãîë‘TS£¡ÉôA»…‹fŒ3pïžCµµêE‹gët´+_ÏôÖ~(&àýu\Ò:ôÐb “Óo܃á9jµ®´¤¢àz ü;óóÅøÑl´ „üÐpyddˆR¥ŒŒŽ‹ˆC_&“ÀS|Bžž „V‹Ûùލ3ðâ®pMM .//OMMuÇc $€ü“À†ž‡ëÚZmm­Æh0ÃÜ6H1$”‡H££"bb"âbb££¡¡² g‹Ã‹Øa¬Ûl¶‚ÄR•¥Â ½`˜R§ÓÁð€Ng¬7Vï-Ï€rA ô’ae¾T*%}ƒÑ 3ò‹ði¸ »ßK‚á4Šñæ2zÃäbà“‹„\r¹æØ&m@ÓVçƒP"ÄC§_ ÿ*Þ°6åå•¥ÅÕ%ÅeeÐÄfÙ`…l?8X Ïý‡†ÉE"fŲ×JJJââ⊊Š …D"ñvÃÏÃ999111œ“wó˜öæýü†¢{H »"P[£‰‰OJŠƒÙëˆÈðp¹L&qeÄ"úNw»Þ kdE–µÃvsœš‚»&±A±é3ê´—Í}Âê¼`1”  Cÿð’aÈEÕ"áâA„¡Å+á!ôž‘ƒH8à˜Õ‡KÐb€0¤¬§ñl2§A. Ø„†L ÎCœÉwc¿¸¢Á@†y}ºæ@˜Ü9.%5 f %íØ·G¯7h´zØÌ§ªRMÿUÕ””ª¨Oæ@™ï0·+Š@@˜ûؤȨ0Ð=n6>¡W­Õé¡cõæÄ• °€ÚÉðİœÔT"Aé¬lCbø¦yX™†î/(,˜ ?6\wvÇ!ÄÀ'Uon¿;öÔm³Ä:ài(pYÜŸ¬^ÓFw€°K¥Aaá!\C|c¢Ùá|øá§ÒOÇh²èuV­Fkð¨HȾk ÊqqQÉÉñ0¤++œÃÕÎÂý e>Ðï0Ö ŽAúʉ:µ °lßwÒà €ê7pzPt…é){°Wê|@–E¨N+Ò€‚ÚlœAÎ&|B&P'gƒHìNæÎș⠬ ¨BxhˆDÌžÒnK41k 5ñvåu_§ÎÅÐO»¶ÙKv«  ì–T'K`Ÿ ÌöýÅÚ!$ÐQÀhâ :Í fnýóÖ¼»áì5sÙ9;ð Ò)“ÃÔzAoÔëìÊu(ÂÙˆ³îâÜE¸,Ð.ecÉ8¹ÆŒºK‚`¢ùA›¹,^nP §]G½SW´Ç°C#yp–³f FØC·(óèfcU‘`ì6h=«fC=†Äu#½»þNik`œ/àÁ‚|©,Øb¶Ð9~Xóf¡OªqÅqF¼>ÁoÃô”Kéú«´h8`Ì#À›ëDŒþißÛõðžÛ=Hæ®”;ÒCm9kM[*®°ë:;pÁåËp¸.tˆo”ùq›±’H <V1éÔÔ[o_ñÛ§„«œdrt°ÆˆaØ@LÇ¿aÛ<îÁtúrynzÐPª²pÔ-—öþá‡'å`Èžæ§ô …"Œ¾ÓX.d‡dÞåÖµCÏ|úìÙhƒÒpGC›ƒ2À7«†h)ÐE0ÁÉ6„91æNA§A¤ ¡«ñÝñn™§ýw¶p.Àé+]¥]w^„öÍëêaÃEÎZêz# ‚–›m_PæÛ×ýBo‘@¿NtA€Ýe»ô"<‘Ürz—@ƒNs—œ.7+êTÜݦØö€ó®¶ŠÀ{ïèA”ùŽþ€õGH Ü%oæôØýÉY€S¸n!wê”Ã5ÜŸu®áIë@™o=–h $Ðap’ïýy—qïdÞÙ; ¹{]Ñú/¼×åcyH $8;cUî2Ùí`‚»$Ð:½ùÚœc;wÈ)ÓC©’ľóž\Ð+²ÊŽmØqÜ¢¾âéq"¯0)únç'Ãç.×¥‘œwˆ¶ÛôE†H9êÙùÃ}'µ\ß±á‹2¢|dÅ‚TMrý»_œ(ƒBŸEà’J$a£ <&"’(úŽš:®¯’¸rÍ]±  wÙ·uŒEH $àwZAæËNl}ig–»fêü3]wþ‘ÕŒó¥Šú²ÜÜ›7‰êºž€Ì{…UEù7o–]W“qnKM XT¹¹ùù¤LQK†‡úÌjQçæßTµ¶F`[_¾¨UPè0Â^òΖŸ›}æú“ï,HsæRYˆ¯ ygÁ0@H ÿ"ÐòAû¢/œ2qÑêås²õ³îþh¿ï}†„\XD¿ê„i>‘¨Í—5Ö_"NÜŸl ê÷?çVÚ¨Gà˜ø@Ÿ`öò™Ýô>rù2ŽqH _”Ìyãq—~Ñu¯ o¬²®ZrŽç²a‹^žÞÞå×eµJõöÁ|¢Ê-"ÄòÝŽm»Ï 08eÐÔ'çãÔ´±ZZTg>ݰ3#WM„ h4LïE»å9 Œ@¬þú±MÛvç«­`ºsŸqsLW¸Œ‚0¿û´Jm yrÍÓMé‚+˜;®µ3nè­× ½ïÆŠ«,üFH ø ˜óæð~â•_¹á"Ô±à´TæËn–Ñ»<ðaªñôè2}Åë£j¡“,9¿cÙî3 à )‰êëù¹»_#1/s½}.iƒOCnF´„Ä`UüëÚЗ· +Ûñ~#ïNQýÏÛ_R… 1øü¬ƒ¯mRlœÈš“ˆ²Oжœ {òõ¦h<äЗçT&*ˆ…¨OçÓz »øÿg‹Â$€€ÿ€Ž,·+PæÞ ·K_ëÓR™W]geÞ{ œˆ"#é¼Ú².Ãúô¡™¢<ðÚÛjè݃4ßþè¼dóš^¢Êÿ³îŒÚúïýçóaäâl/~âk¦“œoÖnû΢/£cp¨ŽoÛ _ÂQ‹Þ˜Û·©mØýþºÝ¬öC8¬8ï‰À@HÀ ðT•ÐÅêX]Õ¦ß xˆ¿c!j©Ì+º(Épo¯¼xþ¦U’¦ Qå_ÏUgg¿ÛÜg\/:¬9ª¯âÌq„B}¡‰¬"%ýJþîÓá[ñSøtV×Ä¿+Âóíšl§K<±lH˜6jTB°È¢Î?ž‘k%ÖŒÓeƒY¶_/'ž"$€~Y©]&ÔÖÖr½¬_Ö,ݯ´TæS:’K _œ˜´`¸êvzÇÛQáï3µsv®Úš0lÞšùÃs>]û× *Û·? ¹§kÉ èƒßdŸÍƒÄß}ôiC#V 1æDºòĆ÷蕟~€µ-T¤%Z²óÕÇÿºmÔö‰uËc'ÚUçÏWöã µg²ë¹úÀÜYìÜÐYa)ºI—Å‘´9Ÿy uÑñovìç4þŽ é Yk×nH )Ëʧcð“&õ%ìz=#)}ÓÈ™ Cöη¶f“Ü34mHh0¡ƒ DÒçÙ5£ßZøR>ÉÝ´ãô» ÑHî¥ T#*’ñѺ¢ã)ž¦cúûŽJ¡-zX ðä?»Æ@¡€/ƒWw_ýѲ…qv ´a‹Þß×u†ßH _žì'Ï0Œ^¯OL¬×Áùå}ó[Äb±óô~ëb‹k±ÌѸ5ï­›vgÝ4¨òoR‡„yvÁèÈZŨor¨²ìÎvî¬ÈÏW©O)Æ®ˆgõSÄ©¨—–’`…D•›Åö±ÓY=.‘ÔNôaD5ÿÙE£Ê¶ÏÏÏ¢Kí„ Ã^X3Ž\ü‚.²ZˆrѼ>«vf©Ï|úÝÔAãèwˆf½ñ²õ­·Žçnærº.6ï…¹©XtÇ^®ˆè#uêìl=Iã®á'@HÀŸ €\………UTT˜ÍæŽöVõ¦ÞÀð2ï|—p=@555\Lyyyjjj½«¾Oõ•E° N$R$*Ùþ0—J_V¦V(CëÏ‚û¶ÁÆZ*+kE’ÈP•FŒ@‰*(1$QÙ´¥vµeE°× H»"1±)ŽÝÆg¼„ð 6× ËýÂ!uÆêAà¹Ã_}ôíWNNNLL w Zu¾yŶ¼7ï2&‰Lô±M­Dé-ú®´·ýv.Ô÷JÓˆ(ÑÓðJ~§`¨2±ií‚;ÄëH ?!ÐuËOЪ-ß/PÉ`½@H ´{(óíþb@H 4Fe¾12@H Ý@™o÷·+€@H 1(ó‘Áx$€@ížÊ|»¿…X$€@hÑuðT&<™éüt>¤Éc#+ã‘@÷ˆ¾¡ÄÿÃÒð^V6ÐÁ^ZrHc1~M ù2owøgµ;lܧØà_rì×·CŠt:>OÀwá“G„|Âçñø øx C ™2=x»Ãa±³Õa¶“Ía±9@å±+ßa~r°¢H Açó"O, Œ€8„<ŸvFPèÛÁÍC[‰@sežöà©Æ¬Žs¥¦ïóתðn­tOÐ @­J [óP÷àÔ(ºç6_Dø:†è š/ó0DýøRµõÄ ãÓ}E‘¦ƒ Ãj"$оTê_Їˆ¥ ¡"! àcw¾}Ý?ô¶eš+ó„öæa¬~ëiõó#‚cdBxbË<ÁÜH 6! ÚŸìÃÛzN¿dH#€eÂx D ™2„`åÅfWì ñð’c”ùôSƒUE튼’5FF®ªtð'Ëæ´+ßÑY$ÐRÍì‚Ãú;v¥=àGoé}ÀüH ´÷ß(»ƒÇþáÂþ|›±FÃþG ™2O¢ÒÇåý¯>è@H 1®¿Z¸¯1B€š)óøà\þ,`•@Ç €¾:Æ}ÆZ: 4Sæ‘@H $àÿš¿Ïÿë†"$€Ú=kÅ¿wço¼náv&I ^ö»^}•^0onùäæ B”IÊe““ïP_«F«µY¸L¨=µûÊ™:ù¾‘I²ÆrÝ8tvcž5¶$DkµË‚„#úÇ=ÐGÙXú_>ÞSÁF|ñ$Ðùüâ–22oZÚ´Ty#©!e>î"Ö %P¾ñ­kû½êVTkXþÑéççz Á©ôÚì[{T6šDU9{ròíøüî ˯;»Ä왓•g:m#j,#“¼ ¨ÔÖ³TÞ«°,ßçí/ÔoœÖ¥nB9óª`7Ÿ>y%P\.´U’힀>pÐ> o/V öL ìP!§ñ‰I‘ûžôåø`®6¯í½êª–ùÇWØt$Gã ûþ†-Nà÷)e{yLÝ4ªt¬FÄ«†G¬ê/Md­fý÷Ö%#‚£V«Õ˜ÍîJv¯¾½+è³L¯á³ýüØÈ]¥ž”~Vc-µaoÞTrâå7sŸzo~²¸®ƒ&»¢¼ÃuSù>«Ìܰ¹`Þú‘p™ /NÞùþ…/5(ÅwþÆcÁk'VÕˆÛg­¥ÑJ™ .\,PiyrïÉ®A¢FÖ/Ï‘šýYìP½T¶åÑTX— 鿹ðäâ\©­=¯%}!J[˜^ë©îž%sRS!öÈç—Ò+í3¦õšÔUfÎË^ýM­2-nž¢f5ä%¤(·lËG8›ïÇ#WN}eÊ#$1Fööã}”¾4!±kĤh÷}Òý9ó>­,"¶Ÿò4=ÓÈ_f¿xH dlJôêGº³# šw_~5—Î2ˆÄ¢gè4m€Ì§?«'GÒx-jÏYC!ûGO Ò¼zÂì‰ýcWO¦…kùןäo-§…‡¿@ç,Ì?~y f:)„—®¨ó é«÷e²Î{Uy¦¿uËç%{jéhD¸TüÌä®=k ½>•µûËÉT¥¹S8®ñU­Ï‚:Q¿ÚËÁ¶ÔÚÈYMÁ¡ŒýõÛ–¦œ#F|Ê59½ÃwçCAƇé„H.̨rÒ5(åî¬y§2¹íøtÛ;éíÃVÊtpɈ™/\¹rå³ gޏ¡„Úiaäí]Á«H tqOž÷œ™èU§²Sµ ŽDüÖ`Úwª.®½A;ÖŽ¼›–<-OK[ f£%Ëä8Uc5×ZA>ÙÃqCë”ç"•©J*€}Q¹ö™o®;¯×û‚MO¹#!~"ÛG++×úü§ñ]CéŽCßçÞZ°û~ü䋬Æ'ЉÅdùà_yGÊÌ>ýqú©2}pÖ ew-:xöÖrœp†K¿ÎÓ‚ú~¹ñÕx ˜TÓ9‹³—ŒŽË…–¬ZËþë3ç¼J·ì›ku+¨Ý²¹4^$ åUëL¯}•sIåMÀz9Ï”UkºÌRj¤:¾ rÒh'_m)ól—]îê·;ˆÅЗ‹Ù6Ÿwø®xiNlΜðÜ0Ö¤+̖š»+&rÛñév£Ù\h¤R¦‚£ë2È„ç>ùéÌO_¼>›¨Ò]д0²AÙ@0ª.™ØêX댉³î— ¡¥9’IStí=x‚²+MkÙŸE¥œ§® & ü0…JhbJìÛÓb¹+ƒ'íY6tçXÇ{ÿIDATöϪŽ€¼ßkø˜8Íî³ÝZBÞû÷`L?ù÷.ZùæÑcæ$ÏØ~fç)óÃC–?Ŷn=a·>—f|HÈì¯×O…f@òœ÷^ϱîcSç| ‘_¬ŸJ­®ß®R}ªQÇnzo{Ê0(Ê$Kí­A“ÉLX»nuÝ.N|ŸñT.!´-)°âë}ST`CœìU)¸äñ?2v8!éÞ|üWÈ0J@Þ²H·»@H 0“fGîØ\Ym3ÍïÄ ¦ªÒ”ǪìÔñ; +¶dÓ“®)1[¦+ÍFÂhËžù¨<Øfªû@ûÀDöìÏÖž%‹Ù<,3Û/Ê»õu¦À#¥nt®Î«;‚ ]/ŸøZ9„¹¦Db—èºÄjC+>¨u¬Ù|rjæüu\Å(úÆ(FŠËÓMŽ·š‘@¸Æ‡L*gñLJuË–¥)ÇîÍûžØÞþüâÈPkúuêÂÓIaeôTPA!€Ô7òŠNï«Î£fùÐhð$ð& SÎ-÷Q9]ןöxæiÓµ÷b©ÂÁ¤$-,A7™*á d•~z…éyR,ÛS‡Pò€„pøô=47’Ù’ë‡é9¦ ‡ É„^ÜxäM0€huY™ä^Óž<ÿ¯ë‡ÇQ=×d~:iÄÀ#FLš¹ÓõÜÖø2nRL\üÜÙ›°žLÍ|9³Àg¥œ¶ ö,I'I›öýtæÌ™ŸþýÉ’ñø‡™-Œlà6F $XÂRÓçGŽ¥“ÖŽÓåœÆófïºlH¹yk•oÞ¼QÝ¡ FÙm^<íF<[5éáH¶ÿjï*eûÖlŸ®çPºÀÞfÛqIÇ Œ3B¯Î^PýátHË —T`aüý1;çtgä“fÐÁx¬ƒƒ†p…lçã©ð½àÑèA4ÚÂiüìñÝH6æ7;ÎPxÞa§oM”«çG"•Óøc»Îì*iÌyOK„ èt„m뉪=VaWÊжÿD™'›1€ùŸÕqNÜ%%ê¯ÿ^7¸Mœ£šÎ ÐžsÞ îfu’&QåfÂо+5ý®¼°_EÓ¾~Ý0=§‡8.EANh¹PýœC™$yJD¸âHÉž éɳ+.¼¹.ݼéëïá¡61)˜1p&-ªsÔs›ñiÜTbðÔ—gÖ“’‚‚ {Þ_·rEïŸöôæìxUÊcX1gÛÈGö=Œ:”£{ †š©Y1ÀÕ.ò‚!$€‰“úÂs©Ën–U™¬ŒP‘ãük•vðùúùøð#®¸kâµZ # gEÔËtMÛó¼V ]¡ âáowÈÒúIsx}÷|d¨Û W4 JxfY¼²r­ÉAÄR¥Òõ×HÙýí瓪nVÁ<½,F)ãV¿)S}ú3gÙˆ9.»Þá™ËF¸}o?×Y[VM·ïS›Ïiïuœ¯[ÁIeZ"T*¹‡¹’”> ßÕ‘7ZËmÿÿnëÞ|n.Í)õ ¶{Â…ë7…ei°>nm:¶|ô»a ÛzºÏ”™~ é¹1ìÏ‘wØc?iÌRøææ£U’¹–µÏž7¬Dªh¤É¤9ñé’7ÓO0 1ÑBs‚‘Ë¢)9¸~a!]å¡ñj·}W]8sÚÊÌJ—œš: …HwcÁ]A—ÿÐÆ‰OÒ©'¦’œƒfÀz¼Ò–Eº~«xŒH Y‚²S×¥[ãï¦vÐÅ«£ñ®L2YÕøV9@wa“]Æ;2 Jˆwj<Ù¨?wãC Jpjü2x*(SFÕÕx.«'A=ST§^ªvvÚJ·Úw­Aõ"½¯$ójbÅüaäåu3IÜ÷+zǹËYÉ*L_96æPLyý‰É0—î9>cÅì¸o2ÈSÏÅÒk¦RO˜ž;K‰°xûò‚…+g²ȰÅÛ“ÛŸS¹#oÚ+ÝÉ„§†½¹ðñI# sÒ”ÅSlÞ¼pîý_.vyëÃm߯{oš²sîBÖ˜šòú©`œx*µ¢·ÜéÿüáÉßxýâÊu+gn†$PÁ Ï}1'5’´(’µ„H $€%Às8¼÷+v¦«©©áBååå©tO¥ú‡ÉæP›µFûªCª¿Mg¸g+ê§jôz´böñtHÁ…M{FÌ<ôõOÛcÍ•"„Ñôf&M¥Jcfä o Е7›¡ÿîmÔTY©‹ålœIc‚Gùï\¢Oãšx0ŽÈtEûð® ;&F¥úÉãäž®x #½íc ŸÌfóï¿©~g‚"4ˆ"æ‰NBûÌŽ‘HÀäääÄÄÄpþ„……Ýѱ6íÍ7Zº[ã!…+ £ø —Érè‚7ÿË#ã<ê´rÞ@ÄÅ‘žràò]•èÓ¸<®aîJÕ7 í oç.·0²~xŽ@HÀEàžÎÍ» õõÍ$O™òÌ›ã@H ´_¦7ßÐ{qÜðõëFc @H $Ð|~Ó›o~0'@H $à›Ê¼o.‹@H  ÌÀMÄ* $€ðM ™2Ï#ø8Šo ‹€ŸÀ?_~~ƒÐ½Ö%ÐL™‡ –‰ÃRߺ7­!$ЦèŸ,ºSˆÍBÚ´\4Ž~AÍ”yømáóè?<@í…üÉðyøw«½Ü/ô³U4Wæy<¡€oì æë,Ø4n•{Fh+z{·‘HÀð …¾­0£]$Ðl™'B>O,ä=ÖGþÁÏú[ZÏûŒý±–è@˜üúð¬á¡ ìq+Ä™ùü“Ð1«ÞÌíq`Ô ö„f¼…È`q¼sB}½ÊýÙŽIk€Ÿè!z¨{pj¤ˆ°ƒö8jï§7 ÝjÍ•yíÍÃï ý㘴h‘ÕNì°,¯MœD£H æA‡ùx«‡~<ü½b„tªÇ웃ó´[Í•yö—zóÎ_!¡Ãjs€Ìã/P»ýI@Ç‘@ p8`ÍÌÇÃX=@ãavæñNc%ÐL™{ ððë¿3 îðlCȽÑN±Kß(n¼€À=#ÀÎÂÓç~AÙÙOú' 5þžñÇ‚ü„@óe*ÀýæÐšÐ_÷¯;@¯àøåàŸ£_Ž=–ìš¹ÒÞ?œG/@H Û@™¿¼†@H ]@™o×·GH $p;(󷣃×@H ´k(óíúö¡óH $€nGeþvtð@H vMe¾]ß>t $€Àí Ìߎ^CH $Ю  Ì·ëÛ‡Î#$€¸”ùÛÑÁkH $€Ú5mv뮹ƒÛÏÞ}ŽöI_êÑ>ïzh”@KeÞf·ººçÈõýešâF Á í€R?õ¾¹#“&ðù8ÆÓnúˆ¸ -’yèÄ_®<·ïúç É ÑL¯»(“ø/³Ù’~eK„42-jvëý÷>¡gH ¦h‘ÌÛíö-'_‹ë' ì{SÊÅ´~Gnb\§¸ÏÎmzóÁOßù‡!$€@Ó 4_æ¡+2¯5ªEL<ÎÍ7¼?æ1ÂÒÚ›p[aÜ;ôþx‡Ð'$€@ 4_æ¡ NÝvG Åä~Mm~}{Ð9$€@S´H湂ì¸Ì¾)Ä1-@H {F dïï™»X@H $p÷ZCæqÐþîycJ$€@÷@+È<ÚßÃû…E!$€hVy\±ÕÞ˜ $€À=$Ð2oǹù{xǰ($€@wM 5d¾i+ímÚƒ•p[Í6!„åÁAâ»vÙ™Ð\“o°Êƒ##™¦æ¼W鹚2aÔÖ{Ûr ÷ªÞX@H ø Vy{“–à…ÌY5n¤Üf¶°DÆb«.-9¼½â¸¬)Jß=fíÌ^ÝEæ«_]y÷²oš6eE_†h®G´B%}q»X¥bùòÝD–¯sß%wöÖ§)Oz4Ó‚O³‰@…@+(`çæCb™¨^YX·?®›¶Ü:Y'ú¶'‚àh©!ÂhqcŇ<½¸ÿ0°qþÚ¼¯jok«m.†I£ÂÄ2"“:Æ;zëÓ¯*\lžŸf1 $€: V‘ù¦ÌÍ»ºþýå  ";I¢€v\ܤûÊO\6™UÕa]nÜ,R¤„ÊäìÎêy±ZKˆ,)DLl&-‘&Ë.Uþ'Óš*²Ý(e7á³j ªÜÈ”„ˆš›YµI2'7ט»†˜u7uY°Ð‡esMA¨€K†òB? Ô›kPmÏd"EœL&gˆÉ\SêNo$2iL²ŒÎ54ï°9ˆ ûåºÞ“­¦´¶V#¢PÖ[HÚ°Dï*ü×^§¾ÔBAe·Èá0Y™ŸÌ:o®)1:!bV‚ïA1½ÀCÜÞy#ð $Ð xK[3«ß´Ínãš/_y}¯J´KÙñ`˜Œ˜ ƒé~åÓGtã&Û+Ê¿ßqñCÜÖ3úY i¤Y_VKd¡ ),ÝúéÑ5´1Š¥ð<Ÿ)vJÊ’Qc;qÐUg¸™?æþq¬fÄðì¹Ë‹Cë[V¦¼8«owKÍåZI$ 9ŸûÇ]5.&Ä”º ÖTgí=û–ã¾g énÑäÕŠ»&IÀ¶öÒ•w¨ÏöNöï²EˆÝÑ-ØË[b‹“/_7d”e¯S_þ.ç• £¹~‰¹ù“‡{ªZ[™âª/gaî ©³Œª3'ÿ²‹Ÿ×#ö…Y½“‰M«'QÔCsÉm,ûªî±§2BH öD $ iÏÍ;ež‘‰‡wÒ)‰Ibd˜¹VôÞï:Q©¶XÍ"!3öÏ"ËÿªcMîÆe$JÚñ'D.ç¸ÌáèÜyѨxȨ-×"$QÒð>ÓÅ¡F†UyHÍÈb¢ßûUt}Ë_[Ãè°dŸ0jÒ ;ô»HÏÔMFÇC¬Á¬2Ò°ð>ò§/µlúðlz¸(ëyßò?]Y}RÐH¼k§à#{¼5‡­\’:€–i7># é1­÷ªjutý{+¼«$âqSP_o >#"‡®³g=vY.…ig t郈0q~=K³û+ Á $€:$~Ëk /ªkÂ?ײüžiO/ùôc#gt—PÎäþ£¿’*±Y}ꣽx¸¸ ÂÒˆS£RéekÁΌ٫òOs ÷@’¡‹Lã‰Ã`·÷aÃD]qñ|ñµâª+—kŽ¿šu”Ml>•µ óa9ÞiÁ\x+ý£¬¿TyjDjÊtU?îþáß·èÐâJÔ¹<{UÆgåì˜|—„iwŠWíÞÞÎJèMíYr?úyÞGeW©“Âð® K´zUaÑOFO}LÙž·.c[1õ„é×õO®Rª3.Cüg*Z “Õ¿ wŸ¤Ðð@H VèÍ7¶Î7$WW­+¨¶[„‚¨x íw‹nb'‘™Á‹¦våQ°íŠ[G.Bû ôPYâ DVp]­ú–¼}7Ïè1BBdݓƲÍ–übç:?¡XìËrWñ-šØV|øÚ?r\åqßù¥ÓÒ¤#çs]p@A4lVŸÛU ¡o¯jówЇ,”«Ö4lgmÚjr |}퟿×YeA]:=Û«a‰ž*x»-èô%Ô§cùš'âÃÀ=ƒÃ¦Ë©†à-È?TÜ!ñdf¯ã@H t­ óM´ç´È|ùêêÝ:ÊùÉ»zˆI˜$ª˜¥®«:¼ñÔ·qñû‡D¾6$b¤‚(ÅШ£_]áôשh4¬o°%¿ø’ˆTªƒ”)!)!"&¹ó¤þÙ¬=b(³X»Ó`=Ë*aJgˆˆ Þ½3½ç˜0!Ñk~þ¿Ÿ^¿È×£ÂÂs–ÈH:u/ОeÂû°“|§‚7%x«,„EÜŠÍ\]©'ÊUïýn°Èš_+HmX¢Ý½ˆ¯Ìä¥U€ÃmA*­¬Ô‹Í‰áXO®¶h\- :ás6ð $€: VyÏœöÝ@s)Žq844ƒÚb&bÆlSÓÔć…I#F?58Uщ®/³çþ½¼¤‹4™ˆ{®zä¦Ò¹tÌeJï=&Žüks /©‚Ì óÄ¢WóxlZ¦ï ßêkâå ,—95A>ÛáòcûlNqµšœ% ;=õÈߪm²pö)ÿ⚣Þ,«øìܽݢwyâ®Å©ê|ê’´[Zl'ƒ•ZaÂúàZD6EèÔxuÍ¡¿äsJhƒxo½Ã.o<«¥‘ro->õÒ¾JœlðT!Bàôœ¼’·ö?Ut샑*£Å´R|ë“-EùîR¸ñÙvŽhÝõº›Xà $€† FûèêÕÔ8Ÿ-+//OMeÁ5¨1d´X,Oü}’"&¼ÁÅD„' 'öâïŠO•Ñ©ë3!ÄV¶wßÚ£Ñ+wNý5,#?{q6·ÀÌ»˜Þ1'I$sñ•â£Üø?\½/¼/|^©>OSÖµLcnsH‹ˆ×h÷žªv&êÝý/OÄ%˜5GVýit|¼AÿOîRcñ·± —âõ # ñEG)”ÈÅש‚3)ûÕ ‹&úÂ[{/8Û?Þ—›V•WüÛ‹D"žs8¤™v0@H ´œœœ˜çÂó°0º¸íöG+ Ú7m³ÛÛ»WUßf׈s Ëáñ „$-~â»Åμ·nVÀÜsý#«ô›¬úqä’êœ'®®eO¼ÏæçÃ윂û¢]@ÛC Ùíç¾/ò˜m,ÞÑg ¨â@QEÝ+ Jä.ש‚wŽ&UÇ;#†‘@H Ãh™÷9ÐZ/]^r˜ÿêˆÎÁ|‘H@Ôš‹ûN¿úskYoŠ‹±Vm‰0XkêÕ·±ø¦ØÆ´H $€Ú„@kÈ|?lý ëþÑ&uošÑKW×­½ê#Kcñ>’b@H {K 5dÞ×ìþ½­–†@H ø Ð 2ßày4Å`@H $pï ´‚ÌÃóh÷Þo, $€¸#ÖyËÞïX.&@H $€Úœ@+È<Ú·ù]Â@H 4‹@+È<Ú7‹ o+V $€%€2?H $€–Ê|ÀÞZ¬@H ”yü@H $°PæöÖbÅ@H  ÌãÏ@H €%€2°·+†@He@H ,”ù€½µX1$€@wØì–a˜œœÄ„@H øÐå&¹q™o’9LŒ@H ø߃öMm,øO}Ð$€@€X,¾›jú–y‰D‚J7ø0 @H {O4>88ønÊå9Ž»I‡i@H vGÀwo¾ÝUFH $€@™oÈc@H ”ù¹‘X $€@  Ì7d‚1H $€„Ê|€ÜH¬@H †Pæ2Á$€@Be>@n$V $€@C(ó ™` @H !€2 7«@H !”ù†L0 $€@€@™‰Õ@H $ÐÊ|C&ƒ@H @ ÌÈÄj $€hHe¾!ŒAH $ PæäFb5@H 4$€2ß Æ $€(ór#±H $€@™oÈc@H ”ù¹‘X $€@  Ì7d‚1H $€„Ê|€ÜH¬@H †þ? Ofmk áIEND®B`‚python-cyclone-1.1/demos/github_auth/auth_screen.png0000644000175000017500000005644612124336260022010 0ustar lunarlunar‰PNG  IHDR,k‡xŽiCCPICC ProfileX ­YgXK³î™„%çœsÎArÎ9ŠÊ—ŒKF ""I ¢€( ‚(I" ˆ("IEAPT €$¹GÏ9ßóÝûïÎóÌÌ»ÕoWWWõôLÕÀq‹ ÓI¶7ÑçsusçÃMÐ`Dïˆp=[[Kð+c9žJïèú?iÿ{½o„7-Òìåá‚à[ š½ÃÉ‘`vô ÅD†ïà\3‘\µƒýÿÂÍ;Øë/Ü¿Ëq´7@8Ó੉D²?„EDÎííè¡¡ËêŠtãC°¶7‰è‡'‘ ÛÁÙóú—ÿa"ÑëoD¢ÿßø¯¹ =‘ "ƒ‰q»?þ?/!ÁQˆ¿väJä`ÜY¿Åz̆à“$_3ËßòêðH}ûßò¶€H3G3!œQR”©Óo<䤇`.D¾f±ÃGü³…zYÛ ˜ÁBÞˆïwÆ‚UâIŽ.¿9–>¾†FFVìJ³ÿÃ'ED;ü‘ÇÇ“ ¬ÿð‰æ;ñ¦Aø™D2‚ví‹}ƒMvÆ@äWÂ#mwìÜk 4Øú÷\àY?²ñgG¾î±;ßÛH‘$GSDŽØŒ¢‹$;îp9¢¸üŒÍŒØ†’#‘MÿÈuÃw×4ÒåH޲ßñƒ‚ý|Cv|¸#Ïô!îøñ ª" _àBÁà–Àþ¾ò!òPDæ Â@0r’ùhÿ´`Þc†1o1Ï0Ó˜dHÏß<|ü—®õGä |B´ú‚ˆ?£¡9ÐÚhM´%rÕEN´ZýOÛÀbÓâüÛV¤¯ôoÝú¿­F4nýáH!ÿÁ¿ûxýÝã¿m2³ˆüÿ0äêää6ÿôÿgÆX#¬!ÖkŒG¥£n¢zQ÷PPm¨&À‡ê@5£úQwwðo»þŒBD$;^Ùñp°@¼è ¢v…þï?¼õ7ã· e`ô AH[Àß#8ïZð_Z¢†2b µø;¿íB‹ ÞUF룵?#>F³ 9€4Z ñ¸Z‰2"ý'Šÿ9ià·ëíèݹ÷È=d·ô•â3 õ–‘âS“W;{ï€ïö»{*Ä2øŒ €š”úÿȵY_ˆ<gþ‘‰ Ï»:7ì½£ÈÑéCïÜ0€Ð"O;à‚@ ñˆPš@s`جaA,އÁ²@.(gA9¨5à*¸š@¸€Ç`</Á4x>‚%°6 ÂAˆb‡x!aHR€Ô mȲ„ì!7Èò‡B¡(è0tÊ‚NAg¡ P-tjîA aèôZ€¾Aë0 ¦†™`nX–…Õ`=Øv„÷ÁþðA8N…OÂÅp|n„ïÁágð4ü^FŠÅ’F©¡ P6(w”ŠŒJDe¢ŠP¨zT+²Ÿ¢¦Q‹¨54͈æCK#‘4E;¡½ÑщèlôYt º}ýý½„þ…!`¸0’ ŒÆã‰Á¤aŠ0Õ˜Û˜äy~‡YÁb±,XQ¬*²ÚݰØCØll¶Û‰ÆÎ`—q8;N§…³Áq‘¸4ÜÜ\n÷÷O…çÅ+àñîøP| ¾ߎÁÏá7(è(„)4(l(|(â(r(ª(Z))ÞQlPÒSŠRjQ:RR¡,¦¬§ì¡|EùŠŠJ€JÊŽ*€*™ª˜êÕCª7TkÔ ÔÔÔÔQÔ'©/QwR¿ þN DºwB$á$¡–ÐM˜"ü¤a¤‘¡1£ñ¡I¢)¡i¤¡ùLKA+L«G»Ÿ6ž¶ˆö&í í"‘.‘®„®…nœn™ž‘^žÞ†>„>›þ2ý#úyƒƒƒC*C%C7à #ŠQрћñ(ccã;&,“(“S SÓU¦¦%ff%fgæXææ»ÌÓ,(3–`––,c,ë¬Ü¬z¬¾¬¬õ¬#¬«lœlºl¾l™l lÏØÖÙùØØƒØóØ›Ø'9Ðv1ç8z89™859½939opNpÁ\\ö\‡¸*¹ú¹–¹y¸M¸Ã¹Ïpws/ò°ðèòòð´ó,ð2òjóððvð~àcæÓã æ+æ»Ï·ÄÏÅoÊÅ€C@TÀI E A`RRPMÐO°@°KpIˆWÈJè°PЄ0…°š0Iø´p¯ðªˆ¨ˆ‹Èq‘&‘yQ6Q3ÑxÑ:ÑWb1±ƒbb£âXq5ñ ñ2ñ! XBY‚$Q"1( KªHH–IKa¤Ô¥B¥*¤Æ¥©¥õ¤£¥ë¤ßȰÈXʤÈ4É|–’u—Í“í•ý%§,,W%÷RžAÞ\>E¾Uþ›‚„‚·B‰Â¨"AÑX1I±Yñ«’¤’¯Ò9¥çÊŒÊVÊÇ•»”·TTUÈ*õ* ªBªžª¥ªãjLj¶jÙjÕ1êúêIêmêk*‘74¾hJki^Öœß#ºÇwOÕž--¢Ö­im>mOíóÚÓ:ü:D ·º‚º>ºÕºszâzzWô>ëËé“õoë¯h$t¢ M 3 ŒŒœŒÎM û×/™(›2é4ŘZ˜æ™Ž›q›y›Õš-™«š'˜ß· ¶p°8kñÖRÂ’lÙj[™[å[½²¶µn²6f6ù6“¶¢¶mïØaílíJìÞÛËÛ¶ïu`t8àpÙaÅQß1Çñ¥“˜S”S—3­³‡s­óª‹¡Ë)—iWY××ÇnnnÍî8wg÷j÷å½F{ ÷¾óPöHóÛ'º/vߣýûƒ÷ß=@{€xà¦'ÆÓÅó²ç&цXA\ö2ó*õZò6ð>íýÑG×§ÀgÁWË÷”–ß)¿y-ÿ|ÿ’©ˆ´`p6àk i`yàjMÐ¥ í`—à†|ˆgHK(ChPèý0ž°Ø°ápÉð´ðéƒ .‘-ÈÕPľˆæH&ä#·?J,êXÔ›híè’èŸ1Î17cécCcûã$â2âæâã/Bò>Ôu˜ÿð‘Ãoô.$B‰^‰]I‚I©Iï’M’kŽP :ò$E.åTÊ£.G[S¹S“SgŽ™«K£I#§×<^žŽNHÈPÌ8“ñ+Ó'³/K.«(k3Û;»ï„ü‰âÛ'ýNä¨äœËÅæ†æŽåéäÕœ¢?j&ß*¿±€¯ ³àGáÂGEJEå§)OGž.¶,n>#t&÷ÌæYÒÙg%ú% ¥\¥¥«e>e#çtÏÕ—s—g•¯Ÿ8ÿü‚É…Æ ‘Š¢Jletåû*çªÞ‹jk«9ª³ª·.…^𮱝¹_«Z[{™ërN\U·pÅãÊÐUëÍõÒõX²®kQ×>\÷¼>vÃâF×Mµ›õ·„o•Þf¼Ù5Æ5.5‘š¦›Ýš‡[Ì[ºZ5[oß‘¹s©¿­ä.óÝœvÊöÔöíŽøŽåÎðÎÅ{þ÷fºt½ìví½ow Ç¢çáãݽz½µ¶=ÒxÔÒ§Ö×ôXåqc¿rÿí'ÊOn¨ 4ª6©µïnѹ÷ÔðéƒQ³ÑÇϬŸ 9=÷Ÿ~îó|þEð‹¯Ñ/“_a^eNÒMMqMU¼Ý0­2}÷á›þ·o_ÎxÏ|œ˜Ý|—úžð¾hŽw®v^a¾mÁxaèÃÞï>†ÜXLûDÿ©ô³Øç[_t¿ô/¹.½ûJþºý-û;û÷K?”~t-Û.O­„¬l¬fþdÿY³¦¶Ö»î²>·³‰Û,ÞßjýeñëÕvÈöv8‘LÜý@!WØÏ€o—¼È Æ!䛂æ¯Üh—|îBÁÎ ô¾:ŠvÀèbEqx6 ^J-*kê B.M í"½4ƒ/c%Ó ‹k[-§ W÷wÞ=|©üOé…ì…Oˆ<âŠ~’§¥ú¤WeÅäìä“êŸ)Ã*òªûÔ2Õ5Þì!h©i{êdè^×{e€7T1ò6Î5i62‡,„,M¬­slnÙ>·ûéÀâ¨èdãârµÞí±û›½K«û6OJ"»—´·ž½ï?_"É!`O _4Ür>ôh)Üö ™/ñ%r,ª=º&&?61.8ÞíÙa­ÕD•$õd½#).G}S#K+8^•~3£3³?k,ûõ‰¹“Ÿr¾å.ç­œZÎ_.X/BŸf.–:crÖ»$©´¸¬þ\Gùãó£&*¦+ª~T£.1×HÔê_ö¨‹¹RpõFýpÃ×ëô7o:ÜŠ¸ÛXÛÔÚ|¯¥»µóζÛwÚk;*;Ëîvev¾Øãð@¥—­wíáô£Á¾»ûï=ih,Š6!Œ<}Z2ê÷Ly 36>^ó<ú…îv¢Y_ʯæ&ó¦4§f^Ÿ˜Öœþø¦ü­ý j¦aÖiví]Á{©÷sös³óÇdf?Ô| ]T\\þÔðÙû ý—ÛK¶Kï¿þÆúíÁ÷œ¡ËÄ?dÍ®÷lÉloïÆ_º¢Póèë˜d¬+N /M!J)J%@-GР±£õ¦K¤/ghg\`¦cQc%²¥³ßâ˜â¢âVäÙË›Ìw¿Cà¥à²0•¯¨²˜™¸§Dœd¾Ôué~™y9´<¿ÂEw¥Hå,•*Õµ'êo5~ìÁjqjËëXéëåè_32üdŒ7á6U032w²ð¶ µŠµN´9j{Ì.Í>Ý!Ó1Û)Ó9Õ%Εäæèn¸WÇÃxŸûþ˜…ž×ˆ]^}Þ=>·}Kýù»ä¨‡‚ZƒkCJBsÂRÂÉ=Ⱥ¼‘Ï¢®F§ÅxÅÅÉÅ â>ÌžÀœH—„MZI~{¤/åúÑÂÔ˜cûÒ̦[f3d]Ì~pbêäçœåÜÕ¼åSßó— >.}>ýó ÝYõ’ÐÒê²s3å çß]x]ñ¢r¸êáÅöê¶K}5Ÿ.ó×í»RzõEÓ5ëëéÈîµv[¦Ñ§©¤y¤sG©íÀÝcíÕmí÷.wåv'ÜéI~Ó[ö°òѹ¾“£úžH &o e ŽØ=55zf7æ5õ<õÅñ‰„—~¯ &9&§Z^Ÿv}#ýÿöýL÷lÙ»ƒïuç¨çFç+’>|ôY$} ùþ%|)ü+ù[ô÷¸1Ë+&«´«7ý|¼æ¾öi}h“zkb7þ’à>d=‡}QXTZ=ˆ‰ÇÊbpñ$ YŠ5Ê>ªrê‚=- í Ý úN†ZÆ|¦f{V-6qvföMŽyήvîzžJÞ¾"þÁ4¡ha¢ˆ‘(ŸèO±~ñr‰IS)~iXzAf\ö¡\«üe…bÅd%Oeu¬Ê j¡š«:»ú 2MŸ= ZX­)íF]’ž¡¾ˆ!0ün4gÔ>¾—üú«ùo’:’uƒ@Pgð‘ƒPthOرp½ðŸëÈnÈ;»6Ò&òGTqôžè©˜äXîØ»qžñ,ñ‡êMpMK\IêNÎ?âŸbxT"•íUHûq|&ýIFCfv1[éîÄÄÉk9™¹Ay&§N=Èß›¿X_¨W¤:ý þlfÉlû9…rõóê”+d+Ūø/²WÓ_¢¬¡¨¥EV’ÖÏ«Çë¯6<½¶yCì¦û­S·‡›˜šÝZJ[ÇÛ0wÅÛM:¼:“îëjï~}û¯ÁCÿGÙ}×õo ˆî:=<õTaôijÏãÏ[&ø_Nʾ¦y3›5÷ÉúÛÊšÝNüÿª‘í¼°*ä#y¦ó ä\ ¯ ‘;°R`KÀQÀÇëlR  c¿? €x$çd¼@(!™¦%pGòíXd”W@;A²ãMˆ‡t‘ü0:äƒ=Ð Áü°>ìG²¼x%ˆ²BÅ£jPãhR~œÚÞþ.î[yÛÛÛÛ[•H²ñ €Îà¿þwÙ!c‘Z}éëÔ'ñtsçþïãþbÀtv>iTXtXML:com.adobe.xmp 556 284 ¯0€ ;ö¶Ö[fNyhù½¸Vv ñÍ!hD"tªòœ@ £^!@F??ÿê‹u4d‰qÍéÆå=!Šh{Gç8K¸1g Z¸6^ÞÞ M4dy{{_›¯%J2GÆyü鉂x:ñ-ÇyzDÐ?ED<Âë(:OÑU%€™‡Ý1ÀLH Q‚  à¡0òÐÀ¢[  ˜ ‰!Jð@<”fBXt @Ä@@Ì3!1ð…  NˆX„˜›>¬ª9^xªæ*1 ›Ÿ03,À ¬¾‡ †N£ÁÇ'ÀÇ—Ž´Wí+¨eìæ%?Tø÷-8øýöŠ}õ×àÒà[| sO#ç'L®Á·d·Fgýñ¯V±¨Û¾a·ÀPd¶ÖWUm­šÀ˜þP…f欛ÂBsª …O° A@Ä"äžïì1(ž~ý­©.˜ºsG~óÑ1׉DF†„HLcº¾9×Ì”wÿï¾ÇéËìêŽíuÿùë=7”÷ìÜ÷89¥¿|jãÎC×kͽõ-=m«9ñÜû‡ÝkÛk¬þ]vdÓ×ÿíÏÿ<åJ…ë.ÓYoøÉÃß™l±¤¯>ö嫹§%áÁðBÀõ!wäuØ3¡Î›f DÝ“N™×^öµ«ó˜É)ÝÃ]Š_9óÍÙ/™É››Q¢iÑõ;æíÍË"“HüÜ`MpÌ]¯–žúúJ‡Ç7~âÉ$Þ7ªõöå?òIB@ˆå-wÿ>0è7yebþûq× ; p]Fù‘á?b'Ùð ž·4è믬?‘×^_y^gdã”7㕉Ödš´oÿˆ¨¨€«5ßvèïa¦ï¼Z_S§PšÍ˜:/i*|[uÉS¦ÅF…³Úïl:æëæUc͇ÎzÅÆ’YA¹ø†˜oHT„®æô·UláÆœ¨”{³¦‹ç-uµ—Û½mEk€!“…å «5§Ïž(oc,0xR¤rZ˜MsÖviƒš>ºê²¶%P®P(§MˆŠðcŒê^50æ:9*”/lШ«ÚŒÌ[!¸m·–­YaÛq¿ÈàùNÆd!¡ÆËUç«.´õcâ¢ÿ–bc#çÄvô›[ŽöÐηîK­wÖVÕV_`ÁS§sêwåé˵ŸÈÉS¦O“Y®9öB¹ô‹]®¦ÇD÷ú=ˆ©S§t|u”h.)ïüí#ÓICjN}µã­°…ü÷êN'Ñ_­û¨°F•r‹0…’(fmù…âx…Q¨$õ™•ù”2îÂYSuðϯ—´Eq:Ôª®þÞÏùwtSAHm'‹Þ}þsCÔX_KCú“%'k™!ó*-:èç'óe]RiâOWÞ6Ñ_¯·Ô£w‰„Tëµ¼ƒõ‡:×üáÙù“„.è몾šý•iº (=寪«—ýìá{n™(åó´õçþù—?ýÑ“¿¾?‚žŽ4œç­¯Ô¤††˜_n|(\ÂŒ-µ/üýKµbÍpxSgý¯zvÅOh?¬"dáãf&Žüõ¥/£‰‰ÁEÿ5ǵo6wS{¡ü’¯9òôfrbDyç³+¦û3ýùÓçBnZo;ùé»ÿhý—×ýRÁ­u^ùêã÷ßUûRœõÂÇÏ< j:›½çKÆŸøét.kÜä›YÉYì &¶@ÀÓxoÚ´It}¢ïEÿôÀa__óàpÍþ+Tó¿7…[è7´6·yùIé«Öe²1'OžÓñ&Ó’n‰H ÍV616þÞÄÙ ‰$`lwå9¿»EšÛõ–ÈÇwÖk“d lB­ñ) dâ’Ù²ÏNÔ°Žîû×e,í%x’ Ñ –Öí¯nó17ä?qšRÁÕ5]ªoŸ3Qîï<¦C=y¬€¿Í?‰dkÙ·ßôÌŸš1Î Á[¢¼wIè‰/Ï·Ú^ZtŒY±îÉ»¢ÇZó¤²àÙñwøT”)Ì —K¤²ñÍÊ}‰Æ²›'H%Ÿö†Âƒ5j}sV½t×Ó®VMáAµ³~…ÏHšÏõ""zb“q‹ã‚>;zÁØ4É5ÿVgýlvˆ¹›Òq¡ o{YIy#áÓ’æGp­Oа´.™0mÁ]³&¼ý'Ί ¾xìô%§ÑiêöÛÕÐÐTö͉êúKg/MHùþ™„¶ò’•4 E§Œ}·ÒO9àÇUírñç„ø aº¾æ†;§G PN<Õ"Led³„›mvõr ´xŧöªcÿyñ·ïK'™sÛêÞþÛ·77ïÖœ;Ë|®œ½(dQS¦ÙbY2s¢PëÌ¡OÞþì‹WvŸ¸ÜÖ®mko6O}\#è¨|R”B¤ ç+ÝÐøå““Z=Þ2æj¿®Tmýà‹?Ú}¦‡I°‹þÏŠÆ|vªðÿ¶ôÙ+)¼h¹`&çFÌ´®VÛþqá+‡. ûWËÓîΓ=ˆê…å4kŸ“ñ³Û引ŽËU¯½ù™-glƒ€Cæ3oö Xiìɹ֡ûÚœœüÝIæáOþÕãÉ2˸!Êä †½—Í»qÆ»uýÛ21aM‡1/oÛ£MünS½ÖMÏðWñs ÃÕ«æ1œXªäýîžQýêås'ûu†ª^­n_ýòS3øâ¬SóÖ_>)g3¾g‘¤·~g†M-‰ÔÏÖ‹uCKm—ßppóï?¦Ù„<4ЗU4ß4/ȇIÆÝ6+*’wµåÒ¹rÆæX¶SkÆw¬­¹Ø¯ºÚÁ^Ó33QŒuÍ«âuBWΟ«×Oœ(LŽœ1O…Ú››ùŽkiZIÕÚZÚ öé9ó]ìÅÌïÏN˜– eϽ7 Ϧ[±c<‰@ÏŸ¢øzE³“kOí‹fL¶ÖV„›g*|Ždöœé{ÿ¥¶ ß4nµ©­–º-²iÚÔÕÁ_™ŒZn×6™Ló„ÓÔv¹Áh õaí]ÌœÅI˜¥x{k³¹®%‡ÌtÖ×§mÚ0GP ã•Ü—ß+ãŒëºhxæ„RúdµQâkêlkïÔuwûÒ#u¶X›n¯%ããŒMA?ýé"2ÖÙ|ñ_×V5ÏSÑ*œlÉnã¬2ãÙÒ2òÇY­¾ ÿb-æ¼_ÜŠ©ÀÄzº™\õßÒ˜þj5-~…;êü"«Î«•¤·®u“墔¨W€ŒŽ9ÚFM]‹_k=Xðµ5d÷ð pD¼Gcõÿ ˆ›e¾SblÑ47]Õ6]mnjH`ŠÓH L–3b~¬²2oþ²»-7'Ìí È¥ ùØ‚£Â:åÐnõJ(@»ç›… zÉ’”ï“©zÓì”[Ͳ×|é °”ì[—òúźfWÙ‘¼¼ËÆÏŸ2~~XCù&–Ä»¡(í‰5_¶Œ›w낸Ûb'ë…¥®“&”$‹ˆ7äÍÿé=wÍ]07vÞÔHj·¶ðŒåæ_ª£¡°’«î¼_”{q¹_=. uM¬ÚEÿõ¬›¯"Yôƒï)È›ñ‡Zh hÄÒ–¹uG»ö‚€p1cÒ·SêRLš(äà\! œxxuDÀ2Ð::>’ó{]ÏÎÑØÛ§‹û†Æêßýu¿¥òœg~7–vdQ §šª›uæÉÔ_ü%ÏR†{÷çš6 O<3ÉÔßüñð³B/ŽÚ$“éË”Åý2Ž{:;$¶—©«êÿ)5±hsùžºV ^òÉQ– ÆüxųáƿﯻõîI´7#þ‘ñXÛk<[Á™æŒÂ]º2–ž“–†¨² ¬½0ž<|ˆ/v¢¢~~\˜ù¹ëzuy-ß´ÓZw™M{ ¾_V/M¦÷Ê\ò?…+oæü}¾ÇÞÐFÂ…ÖÌT-H-»ÖN Ô ‹ÏÁaã•áܳ¾•¬¬wˆ-eð 0X£t&´Àò$X­º‚Óó¿Uáá+ÉÔY3L _ýßÁZë= flo©2zÑý|º ?|±Ù‚›Á|¼øoú ?oÈä2Œ>üî‰?¼ÜòD–¹R‡æÂÛ?åö­kb^‚5/ÖÝÝó8˜¹"oÖ«½tß__°ÎÀøcºÓÿ¹ã¤µ;üõâsóC–úº£Ÿôw~ÆC­ïî¹EßY~›–qÿœÕ²úæebƒëW/&®úàÿ^è‰3š·°ÎŒô¡jÝÕS LÌøx9ï…•§¹J7ã,à¸HÀòw‡wûèºHŠ)‘Ãá—/¼îKëó7 W."×¶iŸ¬¶ÓÚø‰³Çy3c×IõE;Gûe-\t«’ûdOWSmEáIúì‘ÙŒ ô¦íþâD­C{!KïšBŸ„2¶ž(,£G„tµöèIÿäüßßÃÍ [Õë^ÿÔrDx·_«wnïzúåšÿ—ÞAŸ<­þ¶ì¤ÍM)«'®±·¿q=½°o¹ @W<í/>ý˜¯¯/>'d÷t³I-߸b·gȘÀÄŸ­¼wÖór_õáOþ´ß%)Ø0J€Xt! ;ï"¾'$²œøÃžlQ ÖY›¿ÿ"{HàŒ6"¡¾÷ÿG[èÜÐ߯ŠoÏ5K} º+%y%Ö5:7† p€ˆE—í®…ØI)Í?ò œÆ!j"!Ì„†úä€}j"!Ì„†úä€}j"þœÐP£}¡& æ™Ø>á4Ô±„}Ì„D28  žC3!ω%z ¢#€™èB‡A@Àsˆx&„G´=ç4DO@F+‹Ñ­'-ú b"`þöu1¹|C}ÅrÜ ÅÆ@@l ˆy&„G´m#‰mI3!ça«u¶·µ4»ç÷xœÂQ¸~×SÝãëŠõ÷„šZZÃCC<><è €€Ø ´´¶ùIè7íð£vö#)Ö{BWµmö;„\Iê°fã,b!g}Â1‘€‰$Pp@<‘DÈ£Š>€€H@„D(¸  žH"ä‰QEŸ@@@$ B" ÜO$òĨ¢O  !‘ n‚€€'€ybTÑ' ˆH7A@À @„<1ªè€ˆ„DH$‚›  à‰ BžUô @DB"$’@ÁMðD!OŒ*ú "!I à&€x"ˆ'F}‘€‰$Pp@<‘DÈ£Š>€€H@„D(¸  žH"ä‰QEŸ@@@$ B" ÜO$à㉸Oêã¥}¿b:&‹ž£Ð©£/Ž”÷+ Æ cá/šÂL™$r÷/Ýw¬ë΄²!oÉiºÚÒÒº•*Rª--®ˆˆUEÊ­6µê¢b*QåÎ8;´iÏ_° £–Àèœ i‹V¥ÙIkÒ*¨Ð©‹Öd®)Pk=äœèl~¦¸qwUÛ èNk}ó]®l½M9kBWQ¶&-·BËtê kÒ¸PZ7lêéj‹ÖenPël²®{Ó¡M{8n­vçê´EµŽ àxÑ9’§8JA”²;LËaÛöíœ#\K¥L]AGäò¡Ÿ7ܘ³ÈÇ‹æ%’g™]Óxݘn9iE›š—— §™¬NÂõ‹¤uæž”B.“Kmr®Ó¡M{8lNÿyIicœ~¥Ã8žCà† N#—”ĆO y(½ÓPdÉ`•qiÑnuÉ[¹¥…*õÕ—ÖÆ˜—ljó·ædçh˜25cõ£Æ÷_É¡¥ œ¬ßç–T3¦HHMOO_)eºÚ⬜’ø¤9»7g–h˜*iãÆõÎòû»tæ…ÜËùüdfȩ́îß÷Fù¿q/ü×,^*õ_î<òF[àkÄÊú–œÕkeìRåËï_ÞsÕDöç*ƒžIBñ¯þö©Ú’ï ,ÜßT¨c¾Ré ÿu˼pÞ°ñÒ®¿©ß¨é20¯¥SÇ=q?_¾s\‹ ùm¦àqþ(»{Ú­[]þÔ{Wnžxéd µ5-jìÓ‹%¹yB»/?¶ðfÁ]»~ö˜ÖîÙ´®T•±iY cÚü­ö²¤ß¯M¤XÔmÝ\4ç¥ôÐÜœ¢øôt•¢§Žƒ­Öâ=;)¤|PÖ¯_Ÿm>/®1Ð|+ŽlZ]pn¼vGZJ)•ÍN٪ص61šiËwdmÈ)àϨôõëWšÏºÚÒ=Û‹ô¦'Ó †â%0:—ãŽWANvqÄŠ©šÒ܇Öíá•©qGâòÌÜ‚„Œ-Ó#s³×-Ý/(–9õæåi¹%¡ë·¼´%#® wóòÍ|}ÝÞ‚Üuk2+¶lYŸTºwóòwrë}Žòm,²ÖÊGÞ$òJ^”¬ð*¬hzæ“–ÖUV}å”°ðÕyþ³]èbßNÉ‹=–:Ï>òfýž«,yAÈ“S½Ëª›Sþtœ ´6ëÊÚtÏþ³é?iªÒ× Óeüí[þ~YýËÏWý¹¦ëΩcVŽ)<Ûœòúñ¾÷Ñ8›ä›iéÌÀy8fåa¿nksG™®+÷HKeÿ2…WUMË£ï72i2ßnú¼}~öt„æ6B¸·‘²´ÛsKJr·ÓÚcº’?Ñe3iÊs r˵}µ±`ÝÔdoÎf ë7¦'”îÍJY¾ƒ_„½Ž@s†íÚ´¶8 qyÄL_Z*¡…ÄÕKÊ)`[^Ý’¡*ÈY·tµù¬ÓÏÙ››Ug5Œ 'Q:0Xqëÿú§äX*6“?”M#Û2Iñ[9¶e×Dîj9Q±iyffiFâbn*eIÜ ¬Ø’·-1’6âYÉÞÌrî~ZÁâÖ¿³)9†êÆH4)›³ ÔÉI|½þùÖ r:^¹¿±Š±Ôe±Ì¥k}-{îØî“-Ï|OÆ>lÍ/«ŸwGXSYËÆ^Y¹ÿd¿’Wôwûó°ó—éè“ÿµp¹’sçæO=z¤%¯¼u9 LšúÖƒä»õoÅÕºóF&ûâÂÆÿÉÜ”nzrç'‡Viùò¢þî‰Bo8«ç÷s6þÑ-Îæ|›öò±7ùéÚ¥ýöë.âÛš63ìû§3£úÔó5UÒÀ¼_Σf½~à¹æ.buÉŸònp­2i,‘+)¨Ö­•×–ÓfsÇÕ:U¬zg5KZ+•–py®¥¤-y›¸h%«¢Ë×å¨S—Ô^o ûÛLŽ0{Ó8ðY$O\»~onŠžæeªÈÚü Ô™-y»ù3jqŒB’’™Y Ž_-IÝ•—ÄR×ú‰R 0R @„ìG&^5S8 •’ÈÔitLR§¦œí;rêäL£Ph÷ì¥ÝŠ:íâP-I½±p[QÑέÛOT/(¥R%µŽÙ©K¸QžRtüJÅæNO7¥(ÙÉpYk+­qI—q DIþįç>b”Hüš–’i|úްCG:¨Àݳå—Êì•dçùЬé*õ¿“W Ê™¬”²#†+ÆŸwÎåd“’„sÉØÚÉZ´•»ÿô…b¦÷ÓtÑ@»eõm6"¤?_ÓE6—s DI¾|±ÿ›ŸvÒÖ%uùqåÍãÞ|K½kš”c9‰c,\áÅ4܆C?¹ƒæ©ZÆØÞãj­¢t7‹KMÕçæW'ËOT3ņØHÁŽ¥¬ówej¼¹ã‘Üü#—‚¢©SSë´›V'\2άQÇ]ÑhêÊK_lö‘EÄ“{µÜq)“ʱg‹ ñ€Ù?ô\d’;­- •Z­V¾85}±"6Â*1œ]mþË3i#.)uÉ£[–|ž™Mcˆ9©ÂXM»™0òòGå Õ´•õ&æíÓSÞGÆ?e¾lêÙ³ÚS—ÎækXðÔ p¦ýÒnIN(uV6’16îrw†¬Ioä$Ç6 %[;»¹%;Ö- —.ccîóµ)£û¶™‘MkNk›ùžÃºW¹²Ö¶¸Y“Ñ\Åb¤Õ¹Ÿæbò™é ¶§¤XRR÷ã­ÉúŠÜ%Ŭ„)VÌ$È!W“Í4–€ê]w íØ´ºãŠqkaî„¢ ¥íC2$?H àQ B®‡“þþ•/½´)VHj‹·n¯‹èýpU]ÉTf[áná³'{>àÉ’JK*´1±œiOì¥9’Ô¼Jç(_¨'¿9Ê‹U´Wv²yÜ4¢uç‹eoúŒÝ÷Tì¼;äìlË3»t…Ùî˜LÊf¿äcÂrœ´“ÖÙnæcÞÔÌ©N˜Ÿ/ë«>B»ôJòàõÂc‹øv¹ç~÷‰.<´G iêsk”WîY«oìRuˆtP—!kö6dýìUZ·B•“™MKU££õ ¬:‹vTë3ˆ/7ƒp5•žPëbb¸ˆjë*èUÎM\¯7ÐölZظP4”{TF¯bYEu,9šÏÕjÔô®è™]ó¹x1蹌s/†Üw9¢—¤ÒH¼jÍu£¶V]´zùšÜ½jóe³¥}‰LAeÊi¨±6GÚæRR uePÌYµ¡´V[[ž¿.-—áâø±ª:ʬNŸȘéé7¾©¼Xè½SoêØÜ˜`nª¡ OöfWt4¿ñ¿›_dsX’74y6Ùézêõo*/i/•‚žHóö¿¯çF‹ÐZÏëÍ‹ÇQ»:rêbÃ¥“'ù[S¡ÆØçQïés̾]j8Up$£Ú<»r¥nOK½·\ô3zq_/!&’±ˆ9qüN’*º·±÷²ZS¬n䂲*‡)Ò㣥×èþ6­~¸bœI¥t©KKÕZ]„*‰±’´ »i»±œ~¤íÑ7ñÕ}³WÍ¡Cz/Ú¥|&ÍθëNl&ÎÞùÝoV|Ú–¾£‚?äýÛ•·„÷>_d³çåTN?F6¹2Ó½ªÚÆ8©Û§-ά­Ao.ÑŸÜ1›$V‘ $,‰ LitB+)! âFgrŸËã#HKc}6ø WL©T¯I¹ßOØöÎÊë´›túž¸pÑOBª¢ 7+E1óðÚe»¶T¤df¥dqN*’ÞÙñ ç$%½†þ[®p„,¼‚€øx™L½î ŒüÃá\M}Ì”IÃà­N[«ÑJäòP¹y(èçWBg) Ój郯L½ûŽ”‚]¶Eè5Z¦ˆ Kƒü~6iFuåR³^" ‡+R™÷գǺ³Ÿ\Ì=^fMöJZ²Ö†Kõ:½ïd%ÿt@Ï[W¨< äááÁJ0}s}ÓU“D"ã†àžäBÝžÂ}¶ëgŸêƒÙÕiéÁE¨9(æª×h!¬dÁ¾M«Wg:z0Áúé5¾<‰kd¤Í '«5lŒlåç.L‰ óõõõòâ.øú°½ís»öÐ#I|©\¯R^«øËÕF-]âÊCûÕv”ß»uYp¸­Ò\ªÜõEKnE—oTh/¢J}Jö6Ãdã{Ùés´ÿ®_p¸²n¯IPXxP¯óŽ uíUãóë§CCÚ M[®%ÐÖÆìÛ´9<ÐYÔ#@\¥±šÆˆŒDèÆŒ¦‘½§ B»ŽòðªUÓúçŠæ-ÍùIÌEq@F0,ÇààÀ5ñÀrœóâé8ç|p@@` @„†.Lƒ€8'rÎGA@†DháÂ4€€€s!ç|p@@` @„†.Lƒ€8'rÎGA@†DháÂ4€€€s!ç|p@@` @„†.Lƒ€8'rÎGA@†DháÂ4€€€s!ç|p@@` @„†.Lƒ€8'€ßrÎgpGÕÇK5ú^U3£ûýŒ]¯.îhÕE¥ZU|¬£ßsuÑÌÐ#'‹5ªDU?'µ¥Å±ªHImii]„Jiý RGé\(©­Í/(j•Å&%ÆhÏQ;×’ïŠo×bu@`4ÀLÈQׯKë“R–/MÞZÄÿ²êu5¤«-^·núú ]—TÖÕ­ËÜ`ÇIzÚ´µVWQ¶&-·‚~cv€äBIÝî•Ë3³²³>P`Ë=‡kw®NÛQTKÆ\ðÍ=M Œ˜ ¹3Êòh²–ž—·RÁx¹Ðköf­ÊÊ]W”z qà‹gžH¥t­/³÷Û¬ÎjÝècRšÉävf%úir‰„IcRóòä.L ¥±–” Eú;ù+cnH7õŸ—”6ÆéWÒomìÛ ñ€€GÀLÈÝa”ÉRFšÁ%ydrz:5Pg^¤«Íߺ!q!¥ä­;‹¬ÓZÝÙº:™Ë^˜¸aëžZëtG{\ÈOÞ°£¨\ÍXho_µù[WoØQl-^ºsCÚÖ|nW[¾cƒÅàsC4—Ú¼¡Øb½¶h«¹°NMföåoJ^˜¶»Ü¶ ª²iÃÖ¢âüÕ¼Ói›Ì¾94ÅUn-Þ³ÓR~wßYQã‰ÜœÜ Á®w¼Ý…‰[w[i˜°–$·Wo-=^¼5+œ˜¶õ8WTW¼cCN5Óä¬1°×eÖ§kµ¥Ò6ìÎÏL­Þš¯V—ZÍZ]µŽÚi)¥ŒUg§P%fõ±Û®}ŸÍ=À€-ˆ- ‡Û|ðÁîÝ»vx@W”»—F+hŠÐ¸#qyfnABƖ鑹Ùë–nàƒ©7/OË- ]¿å¥-q¹›—oæóuêÕKWå–´¦®_¿X“³.3·_#r9«(ÈyË©tïæåîä$@ëÈÓdoÎf ë7¦'”îÍJY¾ÃV]tšòÜ‚ÜròAWÎ÷N–±å¥Œ$YnÖš¬|n¥Ëš¬%uÔVInÚª5ÅŠ„õéIšÒÜUëöÐJå š]1ÅÌ™¤ùŽºL+g6]ÓiÕ¥Y™™3W¤'©Jr3SRÒrYBo6e“pÈ#fªxßTÑ¡«oŽÚuä³µwذÀrœ…Ã'Ÿ|ò«¯¾¢ÃÅÅÅ[·nuXN8Pµ’5¿•£a[vHŒ¦åªDUĦ噙¥‰‹¹a_±%o[b$mij’½™åœ6hж—0¶qWÞ2*Ÿœ4sõ›i¿wR%§³ÜÍ{O4ƨBµ'JJ©|òœÚ"®à–¼Ý¼ÁÅ1 IJff:>šµMÜ.·¢%$eêK;ׯ÷)"Š[ÿΦäò9F¢IÙœ] NNêS®·©¤-y›¸¶“UÑŠåër Ô©É–f¸EE.©‹Þ*alý;;“c¤,1ޕܑýÁñ¶K––’Œ¯¡ÊضíANf²ã«rJëØ2UòÚ•;sw<¸vå²èÚü d­——ñíZ»¦SsMSwþÄugqéÞ”ÕúÂmÉhõÞ5Ç[ »”ôÍN8ä‰k×ïÍMѯ_Ÿ¬ŠÔ©yŸ«åcÔ¿]uŸ£¹ö‘@zÀL¨Žþ;4"2òéË/¿üðÃû—飈KX¶lñ2úŸšÀ_>o$Q©SS±í;rvŽíر};7Cª¨Ó2iôÆÂm¬tçÖMÒ’fÒhÊiC݉¦ÈˆÆ;iüJZÖ#3½’4Z•ÊXîî”[º'‡äN-ÕÔ•Ó}©Åœ¤q)"†óA«£Öa"»ÉIq摵_©Ô%1B^t<Ýëb:§¦S¦’äò)’Ÿ=Ø-¯Õ“dØÜ»óx5Ñ‘d,añÁ¦\Jl´œTð~è¸-']îßµxU4o*"AÉ” *ÈXkeœ$;‡ÐíF¾9¾÷â¤]:jÏgkUl€˜ `&4À©`2™ÆŒ1K5m{yy PA™¾~í2ëx¯HNËÝS§[) v­­†$A«•/NM_¬ˆèjóïXžI6ã’R—<ºeÉç™Ù4>ÓBOÝãhuTÆß>)2)C•K““õÑ{™2#)’鎗V3¥Ü:ÅaÌ‘üô±FŬ^Û¶¢êY¢“<ۣ¶­)›mzÁ~Ò×–“ Lø"tÍ~Q!—W²ž½íÒMç]îÕ5«)ŽK¨˜ÞA8ìú¦wÞ®µ!¡roŸíD&ŒF˜ õää丸8o>-Y²ä‡?üá˜ÞvÔ˜CâÁ@”­|é¥Mk7qimjœF+KëJ> üm…‡ÿ´i탉‰rÒ.Iç,Q1º…Ã]èsISQ,lôy‰ÿ1ck×­-`ìÑxjKO‹ê,å´5m*,r&5ohKéþ‘ÌRÈÙ{i‰å‰j퉽´¼(å' TéÒ§µuTLniÚ¦ 蘆wº=i  7®l ¹¼9@—]¶Ãt³Ð^R)ôÂ!êAµ‹Â 0j @„=­Ÿ=ýôÓëׯñÅ.Ý»„‚[ +­Ó°è%©t“hÕšêFm­ºhõò5¹{Õ܈.£%®êòãjmcmþŽ´Í¥ô|™ºNÇ¢'PÅ5›w«µôЫèQ0»)rq†’UsS a .B•ÄX =FËi,ÏO£[ït(Z*˜©d,kënucmñÎuYÔͤŮm!3gÕ†ÒZmmyþ:2ÅRãb0•õКbjƒÊ¯Ê¡g¨-+нZàä{רXº'‹z4ˆœÚÊ<ÙuÔå^Mº¶ã($¹'ui)QµZT»¼Ïºü šH±šÁŒjXŽs)ü?þ1M8\K‘v¡v—ªã—Åçm[¿*-+å¾ÞPܶ}k#i+~E’¢ {MJ6m+“2Rã²ssSr¯MÞµE’™•REGââ”%%)_³÷‹<þѤì̽ Æ «[Òèe»¶TX+2EÒ;;ø…/iì¦õ «²²STt»ªÀ¼ʼn‘eZÓÛ6¿§TU¤-_Êoª^Ú•ÎùìÐ7åS*ÕkRîãË'l{Ǽii‚+@mqN¾T‘²ÎÜ;eÒ– þ9 ¾–ðb.IŸ/ª˜ñt…žr9<‡]æuÖ¦k½MYªó–C…ya¤ÃpÄ&¤* r³R3$ÛôÂjcŸõj­†òíãF+/ºÏ!®¾“Ãá\M}Ì”IâòœóV§­Õh%t{ž¯îI\®Î’©Ój™Üü‰O¶Q£ÕË‘æýž*=[êü )™å=°»×T‚oˆFðÈÈ^ó2®¥%2‹ý+ö¶têÝw¤ì:°-BOn0Ed¨­ :1Ź­cŠÐ^åí´@å´:ú8•kîØ1Ð+ËA—{•qiÇq8èRÿ»Wnk×%çPHtÊÏ]˜æëë;ðeÑõÍc&䊮۠ϯÚù¾€^¹4&[íIå¡vŠ[kËó J¶g(R·õR *Ðˤµe;‘³žb6[$ZÑöÜpbj·­Ö©÷% nJºé›Ïuåþ¡¡’Á×E ¹3ŽÜ^\‹gÝ×ô`BâÌ3ÖvòÌÓy| ZWxÃ< Óž¯ž¨X1kÓU~TùÇ3ƒwvF؆”93|õgÞ?ýâ·ƒ¯>Bj n&¤«{?{{¥,DFÞÓþÈdÓ§/ºóîÛ"¤#¤7p@À=F¯ nL4Ó¾gFؤ@n[63,Ñt~Ÿ³ Œ}<}Aø¦rʼnÀÈ ©ŒIÇ^S«Þþ}$Ìg‚ôšª;srÄÓY¼oÿ‘^îíÊyþî· 6*¡C½°`ÄM`4‹Ð ï ¹-Òî àÛ¦Tr–vuúæº&óâ×Ê:[ÛZ½ÇEûkšõ3˜„éÛ.¶uÉùjF]CÃuk$ÓÕ†EóS]W³ºqzèbÆô¥Q1Â*Ÿ¾YÝá£ð÷ÑuÔW3ƒü³RcŒo×ù:S·AÛqUÓåmY–ëbÞã„…A^£¾4õüÅ Š™ãú¯ûÑB¢¦¢¥“‘ù*"e2¹DðÜÒJ'“†EË8¯øÙÉ·ôýÚÞ+ Bxö¹8…¤¶äÝÞ%IÚÿZáÊ'› ™¨…õ¹„Z ÃN`‹Ðà—ãî žÒ0Ÿ©qƪVïÑÏþô¶¬³âï§ž?ÃT/yd²¯¡ôâSo¹‡!%w,þcp_Ïgbê]»  l}õ™9W>óa]‘òŒÌ‡någWT¦éðW¯ü}LÕ¬ˆg:o†¡ùÛ«³”¬ªñ”B>uJ»MËæýù6y€±›¾|ˆ³éËÚJÕkó.ëç†ÿþÇwLÆä†úÂ7Nl×ÚÞÒu$Ì|ùÞ¥æá»ùJÙ‡Gž7ÝÄynÐV]•NSr^µž:½ý–#±|úçûñ¸±/·ÿè{÷N6™ÆÜ­øùc¯9˜_®OœÜzjÿó¿ùí¡&òeò£Ï>ûàÝÓõ—eoßÇB¦Ofßlßuˆ±yüõòy²ý9ÙûγûÒ}÷dº8Ÿó|ÎùÖÉéÏ¥Ó€ÀH Ðó‹/#Á›é}Nhpÿ:“§sCWKK7ú1=þA²àíDke²`oP.‘I|ý¼$Ë8G9|yºn ¸m‰rƃŽ5éÇ­[} ¯@Ýz>?dáí™?‘½¥œÍ Ð¹¼6°1¾ãi5/h\5á/ ”øÈ$²@)÷O" ó÷2MS¾ôÀ N Fî>Õø°¥¿˜¿Ê¶w7żzo§@ú6j((xîÞösÁó `Ò9ÁAÙÍ7eü|l”£|[ƒns½ìh&4˜$TÖw´ttvv¶·w̺ëû\–º²¾ú£?Î)Ðíß½=„ßþÛ‡û¯j¯Îš}û÷ïÛµ}û.ãíJ*÷Mö“/œÑ ó®:thÎÞo©ií·{wí?tÈ8k¢ß üÀ+€À½"D_¦=¨S§ÜÄ¡õë/6ºÒJ[ãçÅrÏy _„jêà šx-¡Áþíß•ñÛúCeÐ e®ü»,5³$·…³#‘J&ýtb,·©;ùòÁ™%¯×ÐcwL2ÚÏ™y–¦¯¾œûfÙ³_tô4ñÞ±‡U’¼µì®"©NgÙaÍœ{Ã9Ñ·zóŸÏ~ZÃId`Ȭ›ú±æKmM _¾÷ï}—\EúÉ‹çm‡¿MýUÉÛõ\ëlêÄåŽò…‹³å–dþÖnC‡–³×TòÅÿ¾Lï žxý¹gþð—?<@Ûûß>®—ò‹›!¼÷ï×_yÿ_Op:tôÄÅöù<ÂUz/ÿcåùŸÑößåÃ÷”¶‘@†À(¡A\ SQã¢i|´d·ßõêwäÜC[ŒMZ6Ñü¤w÷åÜ&S— &Ú“úòe˜õq‚®¶ª6*SÓ‚!JFFOÚµ«k¹ºŸŸÓrófòµØ¬ù´ò£ò6µy—ŽpI7AòÌÚ¹ó¸’úorŽd•›|ýù¿%c=¶ìùïF…p‡XØ‚`¾8ÿr®®ÁO2~ü+îIŽ–`9¹rú–£¿B…>9Ó*´î$¿Çà€[œé¡H!·Iy1:òÚc÷Þu×ý¿z—k¤z ï;‹V†šÚÛÛL! .ÛËÛh˜°è)N>9ñÍ7_BòüÝÝì âè ÀÈ 0zï Ñ‚Ò Bàûß3-÷mÇrÓ!MUÜW" ÞÝ-íWü†›‡xnÚ ”鸤3ã7½9¡»9æÊ¤6ü5@ ÷¸ÆÆv©~R°Š_£bB o_ú 8ëT‹ò¨ºrì³é±œ鯖<þño»ô,`²I˜Û´5}úò¡O"£Œ acZjz¾¢õÇ7Çù°víÁÿ=°å–ÛvÝEëŠ^æþK&ÏP·‘L žË·>ÆÜzÿüÁ³tÒúNÎ[·Þ°”ÕqrGóÏÊ­+ÿHµBî ®xŸ6¾ûÜ{[¢µµêŠ: e+%¥œMÉh4ùÚÎí³.ÿ]|ÿåß|¼å©§hWùÀ&zt¼öóÇñ 0ÌF¯ ê÷„æEÎç5¨íhÕé$¬S¿hæ/çË$’qó£ÕulüDºCþó{vC„»+ÜÈé%,$IæÝú{ßvs˜ýøÜ<ÀšL‡šÕóg²±wn»gz³OÈ~I©ºþ&é/„ æò–ÙÔ½0…W :* Œ}åþ|}XÍÅWŽj›£‚‚C–<º(&0d2çjwÅgêžQ ?ìúŒQ,›3›¿³e2ÒðÎ7â3ùÑûß½Ò% æ[¯i.2y­àôÏ·È*_íÆ¼IKHžÄ®\¸Â·wû‹/ ?ñ;òî§ÙYS—žÿ€{î€Ý¶ù_f`Œ. zŸÓ4ú¾’}\Í×_ñ£Yº. ÍÓ´ à”Àh^ŽðæzOÅÂ9ŒúŠ·kŽžnk:ºýœ…ÜþÔ?^Lèf¼„*¤® ódN.¡…MS§tîßç—(Cž¸3Š&€ƒK=Ž` @`p“C`vM’Ãá\M}Ì”I×ÐŒPýgÿ¸OF_|à¶tSXüXcó!ùy«Ý›‚¹¥³ÓWúæ[ pÁñ÷Eù³öêËæîuØå²# fÝ5ÿª¡g”û¥ÀE÷„Di[?<$L*‹ñÊÏ"'êµûuäÀ’¨¨Žö…CŽòûYt–¡©¿ò??ÜçëËÝ£x]¼xQ¡àoÔ8«dsÌÛOàk~(AȦ¸ëÚ; $ÂÞ~þ¾†V ÝÄ’È2_}G›Ñ' @Bë˜]­íFæ ó÷öb]†ŽöNZª4~úë›nöÝ?,Ïë¹MCmj4š‰'RGðóÞ¡ÂqûÊÏ]˜†SÈ>¾KŽJyb¾õž[:wª®Ð®Sš£vó{ej ?¥ûןœÛÑü”¿¥om§Û›»þ0ßîzüt”o­xmƒ»Ü1vh[:4d¤ïïëðñ $…ëjÑê¸bÆ6þƒ¸B C›Ö¢5úó¿þÞ沕ÿïΉz÷i]$‘C ÷úùÈñkè=ܘ8ôþÜø W[ !Ææ>(åßxµh4˜—>¢«,¿i·/ðaŠ„OÌ•vvX´Éy%¸aF±úŸr8u&sÃ;gš£|;E“ÕGêSõ:ÊúN~ê•×iY¯Û¨kmççL×a UAÜN`‹Øn†¹=ö£Â ³e½QN`ôŠÐu~êe„Çuº7<3¡.ؽ"DÏÛpÀ&€€À0Å"Ôó}ÃÀ}6‰™Ð( :º ½"„å¸O¡&0zEËqC}nõ±™P Ø £T„%2ƒ ìÕŸéÍøÀ0[ëÞÞô]¬ôuz½¾Á¶ÀÈÜîîîöñ¥##3"ðÊóŒÆ?0 —Å<ô÷“Û½é[¬E6*Šá 4Ñ÷åtß3û‡VÉ¡ ™L¦Õj%‰ðE>bè÷mCíííÁÁÁÖŽˆÂm8 â"0êDˆ”1cÆÜ3åÆ.ãçÕŸ4´Õ‰+`#ßÛñß»éþïDßGœ…á›6éR/_¾LÃúÈï‚àa@@@XXɧµ#bñ~‚€ˆŒº/0¥ØÐ %£ÑH¯´FDIDá®’êP¢Å7J4vS)p»áðáÞÈ$€/0u—Q7"ÂÈHëB‚9„£ƒ%@Ã7^…º"Þ¿#ƒEò "DP„aQx – ß}ªˆxÿŽôévA®“À(!†˜ë<{[ÀK åAÀã ŒÞ_VõøÐ¢ƒ  0ò @„F~Œà!€x,ˆÇ†‘O"4òcA@Àc @„<6´è€Œ|¡‘#x K"䱡EÇ@@`ä€üÁCðX! -: #ŸDhäÇ‚€€Ç€ylhÑ1ù B#?Fð@<–DÈcC‹Ž€ÀÈ' VòñC¿H¡’ÊIDAT7òùÂCÑLÀ`ì ð“Žfö]¬"$—]iÑØ=a$ÐÒÚàrQŠýB]\4kµ¤CFc—³þဠš4Í-4LÑ`…ßÏt/“I|‹Zô³ÜF>µë ºöN“â€Üx¤=þ~Ò©¯Ÿ CŽB J"á$êâmPßÄ(¥ŽB‚|±~D˜„Ç›O´ŸvSQŠu†T‡) BŽº‡|.¤=‚ü@œ„@¬"$t‰tÈIßp@†—äg@þâ¡»‡  #™€(ŸŽÉ@က€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ë B®³BI7€¹(Ì€¸N"ä:+”p3ˆ›Â€€€ëþ?`ò½\iÙÚIEND®B`‚python-cyclone-1.1/demos/github_auth/templates/0000755000175000017500000000000012124336260020761 5ustar lunarlunarpython-cyclone-1.1/demos/github_auth/templates/gists.html0000644000175000017500000000010412124336260022773 0ustar lunarlunar{% for gist in gists %} -- {{ gist["description"] }}
{% end %} python-cyclone-1.1/demos/github_auth/README.md0000644000175000017500000000072412124336260020245 0ustar lunarlunar### GitHub auth demo Implements an oauth2 mixin for github. Inspiration from fbgraphdemo and http://casbon.me/connecting-to-githubs-oauth2-api-with-tornado. To test the demo app that lists the description of all your gists: [Register new app](github_auth/register_new_app.png) Change github_client_id and github_secret with the respective values after you register the new app. $ python github_auth_example.py [Authorize your login](github_auth/auth_screen.png) python-cyclone-1.1/demos/github_auth/github_auth_example.py0000755000175000017500000000634112124336260023362 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 import sys import os.path from github import GitHubMixin import cyclone.escape import cyclone.web from twisted.python import log from twisted.internet import reactor from cyclone import escape class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", MainHandler), (r"/auth/login", AuthLoginHandler), (r"/auth/logout", AuthLogoutHandler), ] settings = dict( cookie_secret="12oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", login_url="/auth/login", github_client_id="see README", github_secret="see README", template_path=os.path.join(os.path.dirname(__file__), "templates"), static_path=os.path.join(os.path.dirname(__file__), "static"), xsrf_cookies=True, debug=True, ) cyclone.web.Application.__init__(self, handlers, **settings) class BaseHandler(cyclone.web.RequestHandler): def get_current_user(self): user_json = self.get_secure_cookie("user") if not user_json: return None return cyclone.escape.json_decode(user_json) class MainHandler(BaseHandler, GitHubMixin): @cyclone.web.authenticated @cyclone.web.asynchronous def get(self): access_token = self.current_user.get("access_token", None) if access_token: self.github_request("/gists", self._get_gists, access_token=self.current_user["access_token"]) else: self.redirect("/auth/login") def _get_gists(self, response): if response is None: # Session may have expired self.redirect("/auth/login") return self.render("gists.html", gists=cyclone.escape.json_decode(response.body)) class AuthLoginHandler(BaseHandler, GitHubMixin): @cyclone.web.asynchronous def get(self): my_url = (self.request.protocol + "://" + self.request.host + "/auth/login?next=" + cyclone.escape.url_escape(self.get_argument("next", "/"))) if self.get_argument("code", False): self.get_authenticated_user( redirect_uri=my_url, client_id=self.settings["github_client_id"], client_secret=self.settings["github_secret"], code=self.get_argument("code"), callback=self._on_auth) return self.authorize_redirect(redirect_uri=my_url, client_id=self.settings["github_client_id"], extra_params={"scope": "bonito"}) def _on_auth(self, user): if not user: raise cyclone.web.HTTPError(500, "GitHub auth failed") self.set_secure_cookie("user", cyclone.escape.json_encode(user)) self.redirect(self.get_argument("next", "/")) class AuthLogoutHandler(BaseHandler, cyclone.auth.FacebookGraphMixin): def get(self): self.clear_cookie("user") self.redirect(self.get_argument("next", "/")) def main(): reactor.listenTCP(8888, Application()) reactor.run() if __name__ == "__main__": log.startLogging(sys.stdout) main() python-cyclone-1.1/demos/github_auth/github.py0000644000175000017500000000612712124336260020625 0ustar lunarlunarfrom cyclone import escape from cyclone import httpclient from cyclone.auth import OAuth2Mixin from twisted.python import log import urllib class GitHubMixin(OAuth2Mixin): _OAUTH_AUTHORIZE_URL = 'https://github.com/login/oauth/authorize' _OAUTH_ACCESS_TOKEN_URL = 'https://github.com/login/oauth/access_token' def get_authenticated_user(self, redirect_uri, client_id, client_secret, code, callback, extra_fields=None): args = { "redirect_uri": redirect_uri, "code": code, "client_id": client_id, "client_secret": client_secret } fields = set(['access_token', 'token_type']) if extra_fields: fields.update(extra_fields) httpclient.fetch(self._oauth_request_token_url(**args))\ .addCallback(self.async_callback( self._on_access_token, redirect_uri, client_id, client_secret, callback, fields)) def _on_access_token(self, redirect_uri, client_id, client_secret, callback, fields, response): if response.error: log.warning('GitHub auth error: %s' % str(response)) callback(None) return args = escape.parse_qs_bytes(escape.native_str(response.body)) session = { "access_token": args["access_token"][-1], "token_type": args["token_type"][-1], } self.github_request( path="/user", callback=self.async_callback( self._on_get_user_info, callback, session, fields), access_token=session["access_token"], fields=",".join(fields) ) def _on_get_user_info(self, callback, session, fields, response): if response is None: callback(None) return fieldmap = {} args = escape.parse_qs_bytes(escape.native_str(response.body)) for field in fields: fieldmap[field] = args.get(field, None) fieldmap.update({"access_token": session["access_token"], "token_type": session.get("token_type")}) callback(fieldmap) def github_request(self, path, callback, access_token=None, post_args=None, **args): url = "https://api.github.com" + path all_args = {} if access_token: all_args["access_token"] = access_token all_args.update(args) if all_args: url += "?" + urllib.urlencode(all_args) cb = self.async_callback(self._on_gh_request, callback) if post_args is not None: httpclient.fetch(url, method="POST", body=urllib.urlencode(post_args)).addCallback(cb) else: httpclient.fetch(url).addCallback(callback) def _on_gh_request(self, callback, response): if response.error: log.warning("Error response %s fetching %s", response.error, response.request.url) callback(None) return callback(escape.json_decode(response.body)) python-cyclone-1.1/demos/upload/0000755000175000017500000000000012124336260015744 5ustar lunarlunarpython-cyclone-1.1/demos/upload/template/0000755000175000017500000000000012124336260017557 5ustar lunarlunarpython-cyclone-1.1/demos/upload/template/index.html0000644000175000017500000000376712124336260021571 0ustar lunarlunar cyclone upload demo

cyclone upload demo


Who are you?

Your full name

A picture of you

{% if info is not None %}

Name: {{info["name"]}}

File: {{info["file"]}}

{% end %}
python-cyclone-1.1/demos/upload/uploaddemo.py0000755000175000017500000000602012124336260020450 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import sys import cyclone.web from twisted.python import log from twisted.internet import reactor # Helper function to convert bytes to human readable strings humanreadable = lambda s: [(s % 1024 ** i and "%.1f" % (s / 1024.0 ** i) or \ str(s / 1024 ** i)) + x.strip() + "B" \ for i, x in enumerate(' KMGTPEZY') \ if s < 1024 ** (i + 1) or i == 8][0] class Application(cyclone.web.Application): def __init__(self): handlers = [ (r"/", IndexHandler), ] settings = dict( debug=True, template_path="./template", repository_path="./uploaded_files", ) if not os.path.exists(settings["repository_path"]): try: os.mkdir(settings["repository_path"]) except Exception, e: print("mkdir failed: %s" % str(e)) sys.exit(1) cyclone.web.Application.__init__(self, handlers, **settings) class IndexHandler(cyclone.web.RequestHandler): def get(self): self.render("index.html", missing=[], info=None) def post(self): name = self.get_argument("fullname", None) if name is None: self.render("index.html", missing=["fullname"], info=None) return picture = self.request.files.get("picture") if picture is None: self.render("index.html", missing=["picture"], info=None) return else: picture = picture[0] # File properties filename = picture["filename"] content_type = picture["content_type"] body = picture["body"] # bytes! try: fn = os.path.join(self.settings.repository_path, filename) fp = open(os.path.abspath(fn), "w") fp.write(body) fp.close() except Exception, e: log.msg("Could not write file: %s" % str(e)) raise cyclone.web.HTTPError(500) self.render("index.html", missing=[], info={ "name": name, "file": "%s, type=%s, size=%s" % \ (filename, content_type, humanreadable(len(body)))}) def main(): log.startLogging(sys.stdout) reactor.listenTCP(8888, Application(), interface="127.0.0.1") reactor.run() if __name__ == "__main__": main() python-cyclone-1.1/scripts/0000755000175000017500000000000012124336260015040 5ustar lunarlunarpython-cyclone-1.1/scripts/cyclone0000755000175000017500000000136212124336260016424 0ustar lunarlunar#!/bin/bash # Copyright 2012 Alexandre Fiori # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. opt=$1 shift case "$opt" in run) twistd -n cyclone $* ;; app) python -m cyclone.app $* ;; *) echo "usage: $0 [run|app] [options]" esac python-cyclone-1.1/README.md0000644000175000017500000000437412124336260014640 0ustar lunarlunarCyclone ======= Cyclone is a web server framework for Python, that implements the Tornado API as a Twisted protocol. See http://cyclone.io for details. Installation ------------ Cyclone is listed in PyPI and can be installed with pip or easy_install. Note that the source distribution includes demo applications that are not present when Cyclone is installed in this way, so you may wish to download a copy of the source tarball as well. Manual installation ------------------- Download the latest release from http://pypi.python.org/pypi/cyclone tar zxvf cyclone-$VERSION.tar.gz cd cyclone-$VERSION sudo python setup.py install The Cyclone source code is hosted on GitHub: https://github.com/fiorix/cyclone Prerequisites ------------- Cyclone runs on Python 2.5, 2.6 and 2.7, and requires: - Twisted: http://twistedmatrix.com/trac/wiki/Downloads - pyOpenSSL: https://launchpad.net/pyopenssl (only if you want SSL/TLS) On Python 2.5, simplejson is required too. Platforms --------- Cyclone should run on any Unix-like platform, although for the best performance and scalability only Linux and BSD (including BSD derivatives like Mac OS X) are recommended. Credits ------- Thanks to (in no particular order): - Nuswit Telephony API - Granting permission for this code to be published and sponsoring - Gleicon Moraes - Testing and using on RestMQ - Vanderson Mota - Patching setup.py and PyPi maintenance - Andrew Badr - Fixing auth bugs and adding current Tornado's features - Jon Oberheide - Syncing code with Tornado and security features/fixes - Silas Sewell - Syncing code and minor mail fix - Twitter Bootstrap - For making our demo applications look good - Dan Griffin - WebSocket Keep-Alive for OpDemand - Toby Padilla - WebSocket server - Jeethu Rao - Minor bugfixes and patches - Flavio Grossi - Minor code fixes and websockets chat statistics example - Gautam Jeyaraman - Minor code fixes and patches - DhilipSiva - Minor patches python-cyclone-1.1/LICENSE0000644000175000017500000002613612124336260014366 0ustar lunarlunar Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. python-cyclone-1.1/twisted/0000755000175000017500000000000012124336260015034 5ustar lunarlunarpython-cyclone-1.1/twisted/plugins/0000755000175000017500000000000012124336260016515 5ustar lunarlunarpython-cyclone-1.1/twisted/plugins/cyclone_plugin.py0000644000175000017500000001120012124336260022073 0ustar lunarlunar# coding: utf-8 # # Copyright 2012 Alexandre Fiori # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import cyclone.web import imp import os import sys import types from twisted.application import internet from twisted.application import service from twisted.plugin import IPlugin from twisted.python import usage from twisted.python import reflect from zope.interface import implements try: from twisted.internet import ssl except ImportError: ssl_support = False else: ssl_support = True class Options(usage.Options): # The reason for having app=x and ssl-app=y is to be able to have # different URI routing on HTTP and HTTPS. # Example: A login handler that only exists in HTTPS. optParameters = [ ["port", "p", 8888, "tcp port to listen on", int], ["listen", "l", "0.0.0.0", "interface to listen on"], ["unix", "u", None, "listen on unix socket instead of ip:port"], ["app", "r", None, "cyclone application to run"], ["appopts", "c", None, "arguments to your application"], ["ssl-port", None, 8443, "port to listen on for ssl", int], ["ssl-listen", None, "0.0.0.0", "interface to listen on for ssl"], ["ssl-cert", None, "server.crt", "ssl certificate"], ["ssl-key", None, "server.key", "ssl server key"], ["ssl-app", None, None, "ssl application (same as --app)"], ["ssl-appopts", None, None, "arguments to the ssl application"], ] def parseArgs(self, *args): if args: self["filename"] = args[0] class ServiceMaker(object): implements(service.IServiceMaker, IPlugin) tapname = "cyclone" description = "A high performance web server" options = Options def makeService(self, options): srv = service.MultiService() s = None if "app" in options and (options["app"] or "")[-3:].lower() == ".py": options["filename"] = options["app"] if "filename" in options and os.path.exists(options["filename"]): n = os.path.splitext(os.path.split(options["filename"])[-1])[0] appmod = imp.load_source(n, options["filename"]) for name in dir(appmod): kls = getattr(appmod, name) if isinstance(kls, (type, types.ClassType)): if issubclass(kls, cyclone.web.Application): options["app"] = kls if ssl_support and os.path.exists(options["ssl-cert"]): options["ssl-app"] = kls # http if options["app"]: if callable(options["app"]): appmod = options["app"] else: appmod = reflect.namedAny(options["app"]) if options["appopts"]: app = appmod(options["appopts"]) else: app = appmod() unix = options.get("unix") if unix: s = internet.UNIXServer(unix, app) else: s = internet.TCPServer(options["port"], app, interface=options["listen"]) s.setServiceParent(srv) # https if options["ssl-app"]: if ssl_support: if callable(options["ssl-app"]): appmod = options["ssl-app"] else: appmod = reflect.namedAny(options["ssl-app"]) if options["ssl-appopts"]: app = appmod(options["ssl-appopts"]) else: app = appmod() s = internet.SSLServer(options["ssl-port"], app, ssl.DefaultOpenSSLContextFactory( options["ssl-key"], options["ssl-cert"]), interface=options["ssl-listen"]) s.setServiceParent(srv) else: print("SSL support is disabled. " "Install PyOpenSSL and try again.") if s is None: print("usage: cyclone run [server.py|--help]") sys.exit(1) return srv serviceMaker = ServiceMaker() python-cyclone-1.1/appskel/0000755000175000017500000000000012124336260015010 5ustar lunarlunarpython-cyclone-1.1/appskel/default/0000755000175000017500000000000012124336260016434 5ustar lunarlunarpython-cyclone-1.1/appskel/default/modname/0000755000175000017500000000000012124336260020054 5ustar lunarlunarpython-cyclone-1.1/appskel/default/modname/views.py0000644000175000017500000000436612124336260021574 0ustar lunarlunar# coding: utf-8 # $license import cyclone.escape import cyclone.locale import cyclone.web from twisted.internet import defer from twisted.python import log from $modname.storage import DatabaseMixin from $modname.utils import BaseHandler from $modname.utils import TemplateFields class IndexHandler(BaseHandler): def get(self): self.render("index.html", hello="world", awesome="bacon") def post(self): f = TemplateFields(post=True, ip=self.request.remote_ip) #f["this_is_a_dict"] = True #f["raw_config"] = self.settings.raw #f["mysql_host"] = self.settings.raw.get("mysql", "host") self.render("post.html", fields=f) class LangHandler(BaseHandler): def get(self, lang_code): if lang_code in cyclone.locale.get_supported_locales(): self.set_secure_cookie("lang", lang_code) self.redirect(self.request.headers.get("Referer", self.get_argument("next", "/"))) class SampleSQLiteHandler(BaseHandler, DatabaseMixin): def get(self): if self.sqlite: response = self.sqlite.runQuery("select strftime('%Y-%m-%d')") self.write({"response": response}) else: self.write("SQLite is disabled\r\n") class SampleRedisHandler(BaseHandler, DatabaseMixin): @defer.inlineCallbacks def get(self): if self.redis: try: response = yield self.redis.get("foo") except Exception, e: log.msg("Redis query failed: %s" % str(e)) raise cyclone.web.HTTPError(503) # Service Unavailable else: self.write({"response": response}) else: self.write("Redis is disabled\r\n") class SampleMySQLHandler(BaseHandler, DatabaseMixin): @defer.inlineCallbacks def get(self): if self.mysql: try: response = yield self.mysql.runQuery("select now()") except Exception, e: log.msg("MySQL query failed: %s" % str(e)) raise cyclone.web.HTTPError(503) # Service Unavailable else: self.write({"response": str(response[0][0])}) else: self.write("MySQL is disabled\r\n") python-cyclone-1.1/appskel/default/modname/web.py0000644000175000017500000000201212124336260021176 0ustar lunarlunar# coding: utf-8 # $license import cyclone.locale import cyclone.web from $modname import views from $modname import config from $modname.storage import DatabaseMixin class Application(cyclone.web.Application): def __init__(self, config_file): handlers = [ (r"/", views.IndexHandler), (r"/lang/(.+)", views.LangHandler), (r"/sample/mysql", views.SampleMySQLHandler), (r"/sample/redis", views.SampleRedisHandler), (r"/sample/sqlite", views.SampleSQLiteHandler), ] conf = config.parse_config(config_file) # Initialize locales if "locale_path" in conf: cyclone.locale.load_gettext_translations(conf["locale_path"], "$modname") # Set up database connections DatabaseMixin.setup(conf) #conf["login_url"] = "/auth/login" #conf["autoescape"] = None cyclone.web.Application.__init__(self, handlers, **conf) python-cyclone-1.1/appskel/default/modname/utils.py0000644000175000017500000000134212124336260021566 0ustar lunarlunar# coding: utf-8 # $license import cyclone.escape import cyclone.web class TemplateFields(dict): """Helper class to make sure our template doesn't fail due to an invalid key""" def __getattr__(self, name): try: return self[name] except KeyError: return None def __setattr__(self, name, value): self[name] = value class BaseHandler(cyclone.web.RequestHandler): #def get_current_user(self): # user_json = self.get_secure_cookie("user") # if user_json: # return cyclone.escape.json_decode(user_json) def get_user_locale(self): lang = self.get_secure_cookie("lang") if lang: return cyclone.locale.get(lang) python-cyclone-1.1/appskel/default/modname/storage.py0000644000175000017500000000571512124336260022102 0ustar lunarlunar# coding: utf-8 # $license try: sqlite_ok = True import cyclone.sqlite except ImportError, sqlite_err: sqlite_ok = False import cyclone.redis from twisted.enterprise import adbapi from twisted.internet import defer from twisted.internet import reactor from twisted.python import log class DatabaseMixin(object): mysql = None redis = None sqlite = None @classmethod def setup(cls, conf): if "sqlite_settings" in conf: if sqlite_ok: DatabaseMixin.sqlite = \ cyclone.sqlite.InlineSQLite(conf["sqlite_settings"].database) else: log.err("SQLite is currently disabled: %s" % sqlite_err) if "redis_settings" in conf: if conf["redis_settings"].get("unixsocket"): DatabaseMixin.redis = \ cyclone.redis.lazyUnixConnectionPool( conf["redis_settings"].unixsocket, conf["redis_settings"].dbid, conf["redis_settings"].poolsize) else: DatabaseMixin.redis = \ cyclone.redis.lazyConnectionPool( conf["redis_settings"].host, conf["redis_settings"].port, conf["redis_settings"].dbid, conf["redis_settings"].poolsize) if "mysql_settings" in conf: DatabaseMixin.mysql = \ adbapi.ConnectionPool("MySQLdb", host=conf["mysql_settings"].host, port=conf["mysql_settings"].port, db=conf["mysql_settings"].database, user=conf["mysql_settings"].username, passwd=conf["mysql_settings"].password, cp_min=1, cp_max=conf["mysql_settings"].poolsize, cp_reconnect=True, cp_noisy=conf["mysql_settings"].debug) # Ping MySQL to avoid timeouts. On timeouts, the first query # responds with the following error, before it reconnects: # mysql.Error: (2006, 'MySQL server has gone away') # # There's no way differentiate this from the server shutting down # and write() failing. To avoid the timeout, we ping. @defer.inlineCallbacks def _ping_mysql(): try: yield cls.mysql.runQuery("select 1") except Exception, e: log.msg("MySQL ping error:", e) else: if conf["mysql_settings"].debug: log.msg("MySQL ping: OK") reactor.callLater(conf["mysql_settings"].ping, _ping_mysql) if conf["mysql_settings"].ping > 1: _ping_mysql() python-cyclone-1.1/appskel/default/modname/__init__.py0000644000175000017500000000012312124336260022161 0ustar lunarlunar# coding: utf-8 # $license __author__ = "$name <$email>" __version__ = "$version" python-cyclone-1.1/appskel/default/modname/config.py0000644000175000017500000000610512124336260021675 0ustar lunarlunar# coding: utf-8 # $license import os import sys import ConfigParser from cyclone.util import ObjectDict def tryget(func, section, option, default=None): try: return func(section, option) except ConfigParser.NoOptionError: return default def my_parse_config(filename): cp = ConfigParser.RawConfigParser() cp.read([filename]) conf = dict(raw=cp, config_file=filename) # server settings conf["debug"] = tryget(cp.getboolean, "server", "debug", False) conf["xheaders"] = tryget(cp.getboolean, "server", "xheaders", False) conf["cookie_secret"] = cp.get("server", "cookie_secret") conf["xsrf_cookies"] = tryget(cp.getboolean, "server", "xsrf_cookies", False) # make relative path absolute to this file's parent directory root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) getpath = lambda k, v: os.path.join(root, tryget(cp.get, k, v)) # locale, template and static directories conf["locale_path"] = getpath("frontend", "locale_path") conf["static_path"] = getpath("frontend", "static_path") conf["template_path"] = getpath("frontend", "template_path") # sqlite support if tryget(cp.getboolean, "sqlite", "enabled", False) is True: conf["sqlite_settings"] = \ ObjectDict(database=cp.get("sqlite", "database")) # redis support if tryget(cp.getboolean, "redis", "enabled", False) is True: conf["redis_settings"] = ObjectDict( unixsocket=tryget(cp.get, "redis", "unixsocket", None), host=tryget(cp.get, "redis", "host", "127.0.0.1"), port=tryget(cp.getint, "redis", "port", 6379), dbid=tryget(cp.getint, "redis", "dbid", 0), poolsize=tryget(cp.getint, "redis", "poolsize", 10)) # mysql support if tryget(cp.getboolean, "mysql", "enabled", False) is True: conf["mysql_settings"] = ObjectDict( host=cp.get("mysql", "host"), port=cp.getint("mysql", "port"), username=tryget(cp.get, "mysql", "username"), password=tryget(cp.get, "mysql", "password"), database=tryget(cp.get, "mysql", "database"), poolsize=tryget(cp.getint, "mysql", "poolsize", 10), debug=tryget(cp.getboolean, "mysql", "debug", False), ping=tryget(cp.getint, "mysql", "ping_interval")) # email support if tryget(cp.getboolean, "email", "enabled", False) is True: conf["email_settings"] = ObjectDict( host=cp.get("email", "host"), port=tryget(cp.getint, "email", "port"), tls=tryget(cp.getboolean, "email", "tls"), username=tryget(cp.get, "email", "username"), password=tryget(cp.get, "email", "password")) return conf def parse_config(filename): try: return my_parse_config(filename) except Exception, e: print("Error parsing %s: %s" % (filename, e)) sys.exit(1) python-cyclone-1.1/appskel/default/start.sh0000755000175000017500000000030312124336260020124 0ustar lunarlunar#!/bin/bash # see scripts/debian-init.d for production deployments export PYTHONPATH=`dirname $$0` twistd -n cyclone -p 8888 -l 0.0.0.0 \ -r $modname.web.Application -c $modname.conf $$* python-cyclone-1.1/appskel/default/.gitignore0000644000175000017500000000003112124336260020416 0ustar lunarlunar*.swp *.pyc dropin.cache python-cyclone-1.1/appskel/default/scripts/0000755000175000017500000000000012124336260020123 5ustar lunarlunarpython-cyclone-1.1/appskel/default/scripts/cookie_secret.py0000644000175000017500000000025512124336260023315 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # $license import base64 import uuid if __name__ == "__main__": print(base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)) python-cyclone-1.1/appskel/default/scripts/debian-init.d0000644000175000017500000000330612124336260022455 0ustar lunarlunar#!/bin/sh ### BEGIN INIT INFO # Provides: $modname # Required-Start: $$all # Required-Stop: $$all # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Starts a service on the cyclone web server # Description: Foobar ### END INIT INFO PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin DAEMON=/usr/bin/twistd SERVICE_DIR=/path/to/$modname SERVICE_NAME=$modname PYTHONPATH=$$SERVICE_DIR:$$PYTHONPATH export PYTHONPATH PORT=8888 LISTEN="127.0.0.1" CONFIG=$$SERVICE_DIR/$$SERVICE_NAME.conf PIDFILE=/var/run/$$SERVICE_NAME.pid LOGFILE=/var/log/$$SERVICE_NAME.log APP=$${SERVICE_NAME}.web.Application USER=www-data GROUP=www-data DAEMON_OPTS="-u $$USER -g $$GROUP --pidfile=$$PIDFILE --logfile=$$LOGFILE cyclone --port $$PORT --listen $$LISTEN --app $$APP -c $$CONFIG" if [ ! -x $$DAEMON ]; then echo "ERROR: Can't execute $$DAEMON." exit 1 fi if [ ! -d $$SERVICE_DIR ]; then echo "ERROR: Directory doesn't exist: $$SERVICE_DIR" exit 1 fi start_service() { echo -n " * Starting $$SERVICE_NAME... " start-stop-daemon -Sq -p $$PIDFILE -x $$DAEMON -- $$DAEMON_OPTS e=$$? if [ $$e -eq 1 ]; then echo "already running" return fi if [ $$e -eq 255 ]; then echo "couldn't start" return fi echo "done" } stop_service() { echo -n " * Stopping $$SERVICE_NAME... " start-stop-daemon -Kq -R 10 -p $$PIDFILE e=$$? if [ $$e -eq 1 ]; then echo "not running" return fi echo "done" } case "$$1" in start) start_service ;; stop) stop_service ;; restart) stop_service start_service ;; *) echo "Usage: /etc/init.d/$$SERVICE_NAME {start|stop|restart}" >&2 exit 1 ;; esac exit 0 python-cyclone-1.1/appskel/default/scripts/debian-multicore-init.d0000644000175000017500000000443312124336260024460 0ustar lunarlunar#!/bin/bash ### BEGIN INIT INFO # Provides: $modname # Required-Start: $$all # Required-Stop: $$all # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Starts a service on the cyclone web server # Description: Foobar ### END INIT INFO PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin DAEMON=/usr/bin/twistd SERVICE_DIR=/path/to/$modname SERVICE_NAME=$modname PYTHONPATH=$$SERVICE_DIR:$$PYTHONPATH export PYTHONPATH INSTANCES=4 START_PORT=9901 LISTEN="127.0.0.1" CONFIG=$$SERVICE_DIR/$$SERVICE_NAME.conf APP=$${SERVICE_NAME}.web.Application USER=www-data GROUP=www-data # Check out the start_service function for other customization options # such as setting CPU affinity. if [ ! -x $$DAEMON ]; then echo "ERROR: Can't execute $$DAEMON." exit 1 fi if [ ! -d $$SERVICE_DIR ]; then echo "ERROR: Directory doesn't exist: $$SERVICE_DIR" exit 1 fi start_service() { echo -n " * Starting $$SERVICE_NAME... " for n in `seq 1 $$INSTANCES` do PORT=$$[START_PORT] PIDFILE=/var/run/$$SERVICE_NAME.$$PORT.pid LOGFILE=/var/log/$$SERVICE_NAME.$$PORT.log DAEMON_OPTS="-u $$USER -g $$GROUP --pidfile=$$PIDFILE --logfile=$$LOGFILE cyclone --port $$PORT --listen $$LISTEN --app $$APP -c $$CONFIG" START_PORT=$$[PORT+1] start-stop-daemon -Sq -p $$PIDFILE -x $$DAEMON -- $$DAEMON_OPTS e=$$? if [ $$e -eq 1 ]; then echo "already running" return fi if [ $$e -eq 255 ]; then echo "couldn't start" return fi # Set CPU affinity if [ -x /usr/bin/taskset ]; then sleep 1 /usr/bin/taskset -pc $$n `cat $$PIDFILE` &> /dev/null fi done echo "done" } stop_service() { echo -n " * Stopping $$SERVICE_NAME... " for n in `seq 1 $$INSTANCES` do PORT=$$[START_PORT] PIDFILE=/var/run/$$SERVICE_NAME.$$PORT.pid START_PORT=$$[PORT+1] start-stop-daemon -Kq -R 10 -p $$PIDFILE e=$$? if [ $$e -eq 1 ]; then echo "not running" return fi done echo "done" } case "$$1" in start) start_service ;; stop) stop_service ;; restart) sp=$$START_PORT stop_service START_PORT=$$sp start_service ;; *) echo "Usage: /etc/init.d/$$SERVICE_NAME {start|stop|restart}" >&2 exit 1 ;; esac exit 0 python-cyclone-1.1/appskel/default/scripts/localefix.py0000644000175000017500000000057612124336260022453 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # $license import re import sys if __name__ == "__main__": try: filename = sys.argv[1] assert filename != "-" fd = open(filename) except: fd = sys.stdin line_re = re.compile(r'="([^"]+)"') for line in fd: line = line_re.sub(r"=\\1", line) sys.stdout.write(line) fd.close() python-cyclone-1.1/appskel/default/README.md0000644000175000017500000000563712124336260017726 0ustar lunarlunar# cyclone-based project This is the source code of $project_name $name <$email> ## About This file has been created automatically by cyclone for $project_name. It contains the following: - ``start.sh``: simple shell script to start the development server - ``$modname.conf``: configuration file for the web server - ``$modname/``: web server code - ``frontend/``: static files, templates and locales - ``scripts/``: debian init scripts and other useful scripts ### Running For development and testing: twistd -n cyclone --help twistd -n cyclone -r $modname.web.Application [--help] or just run ./start.sh For production: twistd cyclone \ --logfile=/var/log/$project.log \ --pidfile=/var/run/$project.pid \ -r $modname.web.Application or check scripts/debian-init.d and scripts/debian-multicore-init.d ## Customization This section is dedicated to explaining how to customize your brand new package. ### Databases cyclone provides built-in support for SQLite and Redis databases. It also supports any RDBM supported by the ``twisted.enterprise.adbapi`` module, like MySQL or PostgreSQL. The default configuration file ``$modname.conf`` ships with pre-configured settings for SQLite, Redis and MySQL. The code for loading all the database settings is in ``$modname/config.py``. Feel free to comment or even remove such code, and configuration entries. It shouldn't break the web server. Take a look at ``$modname/storage.py``, which is where persistent database connections are initialized. ### Internationalization cyclone uses the standard ``gettext`` library for dealing with string translation. Make sure you have the ``gettext`` package installed. If you don't, you won't be able to translate your software. For installing the ``gettext`` package on Debian and Ubuntu systems, do this: apt-get install gettext For Mac OS X, I'd suggest using [HomeBrew](http://mxcl.github.com/homebrew>). If you already use HomeBrew, run: brew install gettext brew link gettext For generating translatable files for HTML and Python code of your software, run this: cat frontend/template/*.html $modname/*.py | python scripts/localefix.py | xgettext - --language=Python --from-code=utf-8 --keyword=_:1,2 -d $modname Then translate $modname.po, compile and copy to the appropriate locale directory: (pt_BR is used as example here) vi $modname.po mkdir -p frontend/locale/pt_BR/LC_MESSAGES/ msgfmt $modname.po -o frontend/locale/pt_BR/LC_MESSAGES/$modname.mo There are sample translations for both Spanish and Portuguese in this package, already compiled. ### Cookie Secret The current cookie secret key in ``$modname.conf`` was generated during the creation of this package. However, if you need a new one, you may run the ``scripts/cookie_secret.py`` script to generate a random key. ## Credits - [cyclone](http://github.com/fiorix/cyclone) web server. python-cyclone-1.1/appskel/default/frontend/0000755000175000017500000000000012124336260020253 5ustar lunarlunarpython-cyclone-1.1/appskel/default/frontend/locale/0000755000175000017500000000000012124336260021512 5ustar lunarlunarpython-cyclone-1.1/appskel/default/frontend/locale/es_ES/0000755000175000017500000000000012124336260022510 5ustar lunarlunarpython-cyclone-1.1/appskel/default/frontend/locale/es_ES/LC_MESSAGES/0000755000175000017500000000000012124336260024275 5ustar lunarlunarpython-cyclone-1.1/appskel/default/frontend/locale/es_ES/LC_MESSAGES/modname.po0000644000175000017500000000131412124336260026254 0ustar lunarlunar# cyclone-tools sample translation. # Copyright (C) 2011 Alexandre Fiori # This file is distributed under the same license as the cyclone package. # Alexandre Fiori , 2011. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2011-02-24 15:36-0300\n" "PO-Revision-Date: 2011-02-28 15:44+-0300\n" "Last-Translator: Alexandre Fiori \n" "Language-Team: Alexandre Fiori \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" #: standard input:5 msgid "cyclone web server" msgstr "servidor web cyclone" #: standard input:8 msgid "It works!" msgstr "¡Funciona!" python-cyclone-1.1/appskel/default/frontend/locale/es_ES/LC_MESSAGES/modname.mo0000644000175000017500000000074112124336260026254 0ustar lunarlunarÞ•4L` akA~ ÀÌIt works!cyclone web serverProject-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2011-02-24 15:36-0300 PO-Revision-Date: 2011-02-28 15:44+-0300 Last-Translator: Alexandre Fiori Language-Team: Alexandre Fiori MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit ¡Funciona!servidor web cyclonepython-cyclone-1.1/appskel/default/frontend/locale/pt_BR/0000755000175000017500000000000012124336260022520 5ustar lunarlunarpython-cyclone-1.1/appskel/default/frontend/locale/pt_BR/LC_MESSAGES/0000755000175000017500000000000012124336260024305 5ustar lunarlunarpython-cyclone-1.1/appskel/default/frontend/locale/pt_BR/LC_MESSAGES/modname.po0000644000175000017500000000131212124336260026262 0ustar lunarlunar# cyclone-tools sample translation. # Copyright (C) 2011 Alexandre Fiori # This file is distributed under the same license as the cyclone package. # Alexandre Fiori , 2011. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2011-02-24 15:36-0300\n" "PO-Revision-Date: 2011-02-28 15:44+-0300\n" "Last-Translator: Alexandre Fiori \n" "Language-Team: Alexandre Fiori \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" #: standard input:5 msgid "cyclone web server" msgstr "servidor web cyclone" #: standard input:8 msgid "It works!" msgstr "Funciona!" python-cyclone-1.1/appskel/default/frontend/locale/pt_BR/LC_MESSAGES/modname.mo0000644000175000017500000000073712124336260026271 0ustar lunarlunarÞ•4L` akA~ ÀÊIt works!cyclone web serverProject-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2011-02-24 15:36-0300 PO-Revision-Date: 2011-02-28 15:44+-0300 Last-Translator: Alexandre Fiori Language-Team: Alexandre Fiori MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Funciona!servidor web cyclonepython-cyclone-1.1/appskel/default/frontend/template/0000755000175000017500000000000012124336260022066 5ustar lunarlunarpython-cyclone-1.1/appskel/default/frontend/template/index.html0000644000175000017500000000057712124336260024074 0ustar lunarlunar{% extends 'base.html' %} {% block page %}

{{_("It works!")}}

English  Español  Português

hello = {{hello}}

awesome = {{awesome}}

{% end %} python-cyclone-1.1/appskel/default/frontend/template/base.html0000644000175000017500000000140112124336260023662 0ustar lunarlunar {{_("cyclone web server")}}
{% block page %}{% end %}
python-cyclone-1.1/appskel/default/frontend/template/post.html0000644000175000017500000000035312124336260023742 0ustar lunarlunar{% extends 'base.html' %} {% block page %}

POST example

This variable exists = {{fields.ip}}

This variable does not exist = {{fields.something}}

This one comes from .conf = {{fields.mysql_host}}

{% end %} python-cyclone-1.1/appskel/default/frontend/static/0000755000175000017500000000000012124336260021542 5ustar lunarlunarpython-cyclone-1.1/appskel/default/frontend/static/favicon.ico0000644000175000017500000000217612124336260023671 0ustar lunarlunarh(  "ÿ"ÿ"ÿ"ÿ"ÿÛ¼õÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ !ÿ !ÿ"ÿ#ÿ"ÿÛ¼õÛ¼õ"ÿ"ÿ !ÿ !ÿ ÿ"ÿ"ÿ"ÿBCEÿBCEÿBCEÿ !ÿ !ÿ !ÿ#ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !%þBCEÿ"ÿ"ÿ"ÿBCEÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ"ÿ"ÿ"ÿ"ÿBCEÿûýþûýþ"ÿ"ÿBCEÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿûýþÛÝÞ."ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿBCEÿ"ÿBCEÿ"ÿÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿÛ¼õ"ÿ"ÿÛ¼õÛ¼õYZ\ÿÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿÛ¼õÛ¼õÛ¼õÛ¼õ"ÿÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿÛ¼õYZ\ÿ"ÿ"ÿ"ÿÛ¼õYZ\ÿ"ÿƒÁ€`ñÿÿÿÿÿÿóŸóžy½ƒ™ÇÃÿçÿpython-cyclone-1.1/appskel/default/modname.conf0000644000175000017500000000111212124336260020716 0ustar lunarlunar[server] debug = true xheaders = false xsrf_cookies = false cookie_secret = $cookie_secret [frontend] locale_path = frontend/locale static_path = frontend/static template_path = frontend/template [sqlite] enabled = yes database = :memory: [redis] enabled = no # unixsocket = /tmp/redis.sock host = 127.0.0.1 port = 6379 dbid = 0 poolsize = 10 [mysql] enabled = no host = 127.0.0.1 port = 3306 username = foo password = bar database = dummy poolsize = 10 debug = yes ping_interval = 3600 [email] enabled = no host = smtp.gmail.com port = 587 tls = yes username = foo password = bar python-cyclone-1.1/appskel/autogen.sh0000755000175000017500000000036312124336260017013 0ustar lunarlunar#!/bin/bash set -e cd `dirname $0` for d in `find . -type d -depth 1 -exec basename {} \;` do name="appskel_$d.zip" skel="../../cyclone/${name}" echo Generating ${name}... rm -f $skel cd $d zip -r $skel . cd .. echo done done python-cyclone-1.1/appskel/foreman/0000755000175000017500000000000012124336260016437 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/modname/0000755000175000017500000000000012124336260020057 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/modname/main.py0000644000175000017500000000146412124336260021362 0ustar lunarlunarimport web import sys, os from twisted.python import log from twisted.internet import defer, reactor def main(config_file): log.startLogging(sys.stdout) application = web.Application(config_file) port = os.environ.get("PORT", 8888) reactor.listenTCP(int(port), application) reactor.run() if __name__ == "__main__": if len(sys.argv) > 1: main(sys.argv[1]) else: log.error("no config file given") sys.exit(-1) python-cyclone-1.1/appskel/foreman/modname/views.py0000644000175000017500000000233112124336260021565 0ustar lunarlunar# coding: utf-8 # $license import cyclone.escape import cyclone.locale import cyclone.web from twisted.internet import defer from twisted.python import log from $modname.utils import BaseHandler from $modname.utils import TemplateFields class IndexHandler(BaseHandler): def get(self): self.render("index.html", hello='world', awesome='bacon') # another option would be # fields = {'hello': 'world', 'awesome': 'bacon'} # self.render('index.html', **fields) def post(self): tpl_fields = TemplateFields() tpl_fields['post'] = True tpl_fields['ip'] = self.request.remote_ip # you can also fetch your own config variables defined in # $modname.conf using # self.settings.raw.get('section', 'parameter') tpl_fields['mysql_host'] = self.settings.raw.get('mysql', 'host') self.render("post.html", fields=tpl_fields) class LangHandler(BaseHandler): def get(self, lang_code): if lang_code in cyclone.locale.get_supported_locales(): self.set_secure_cookie("lang", lang_code) self.redirect(self.request.headers.get("Referer", self.get_argument("next", "/"))) python-cyclone-1.1/appskel/foreman/modname/web.py0000644000175000017500000000132312124336260021205 0ustar lunarlunar# coding: utf-8 # $license import cyclone.locale import cyclone.web from $modname import views from $modname import config class Application(cyclone.web.Application): def __init__(self, config_file): handlers = [ (r"/", views.IndexHandler), (r"/lang/(.+)", views.LangHandler), ] settings = config.parse_config(config_file) # Initialize locales locales = settings.get("locale_path") if locales: cyclone.locale.load_gettext_translations(locales, "$modname") #settings["login_url"] = "/auth/login" #settings["autoescape"] = None cyclone.web.Application.__init__(self, handlers, **settings) python-cyclone-1.1/appskel/foreman/modname/utils.py0000644000175000017500000000141212124336260021567 0ustar lunarlunar# coding: utf-8 # $license import cyclone.escape import cyclone.web from twisted.enterprise import adbapi class TemplateFields(dict): """Helper class to make sure our template doesn't fail due to an invalid key""" def __getattr__(self, name): try: return self[name] except KeyError: return None def __setattr__(self, name, value): self[name] = value class BaseHandler(cyclone.web.RequestHandler): #def get_current_user(self): # user_json = self.get_secure_cookie("user") # if user_json: # return cyclone.escape.json_decode(user_json) def get_user_locale(self): lang = self.get_secure_cookie("lang") if lang: return cyclone.locale.get(lang) python-cyclone-1.1/appskel/foreman/modname/__init__.py0000644000175000017500000000012312124336260022164 0ustar lunarlunar# coding: utf-8 # $license __author__ = "$name <$email>" __version__ = "$version" python-cyclone-1.1/appskel/foreman/modname/config.py0000644000175000017500000000457212124336260021706 0ustar lunarlunar# coding: utf-8 # $license import os import ConfigParser from cyclone.util import ObjectDict def xget(func, section, option, default=None): try: return func(section, option) except: return default def parse_config(filename): cfg = ConfigParser.RawConfigParser() with open(filename) as fp: cfg.readfp(fp) fp.close() settings = {'raw': cfg} # web server settings settings["debug"] = xget(cfg.getboolean, "server", "debug", False) settings["xheaders"] = xget(cfg.getboolean, "server", "xheaders", False) settings["cookie_secret"] = cfg.get("server", "cookie_secret") settings["xsrf_cookies"] = xget(cfg.getboolean, "server", "xsrf_cookies", False) # get project's absolute path root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) getpath = lambda k, v: os.path.join(root, xget(cfg.get, k, v)) # locale, template and static directories' path settings["locale_path"] = getpath("frontend", "locale_path") settings["static_path"] = getpath("frontend", "static_path") settings["template_path"] = getpath("frontend", "template_path") # sqlite support if xget(cfg.getboolean, "sqlite", "enabled", False): settings["sqlite_settings"] = ObjectDict(database=cfg.get("sqlite", "database")) else: settings["sqlite_settings"] = None # redis support if xget(cfg.getboolean, "redis", "enabled", False): settings["redis_settings"] = ObjectDict( host=cfg.get("redis", "host"), port=cfg.getint("redis", "port"), dbid=cfg.getint("redis", "dbid"), poolsize=cfg.getint("redis", "poolsize")) else: settings["redis_settings"] = None # mysql support if xget(cfg.getboolean, "mysql", "enabled", False): settings["mysql_settings"] = ObjectDict( host=cfg.get("mysql", "host"), port=cfg.getint("mysql", "port"), username=xget(cfg.get, "mysql", "username"), password=xget(cfg.get, "mysql", "password"), database=xget(cfg.get, "mysql", "database"), poolsize=xget(cfg.getint, "mysql", "poolsize", 10), debug=xget(cfg.getboolean, "mysql", "debug", False)) else: settings["mysql_settings"] = None return settings python-cyclone-1.1/appskel/foreman/.gitignore0000644000175000017500000000003112124336260020421 0ustar lunarlunar*.swp *.pyc dropin.cache python-cyclone-1.1/appskel/foreman/scripts/0000755000175000017500000000000012124336260020126 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/scripts/cookie_secret.py0000644000175000017500000000025512124336260023320 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # $license import base64 import uuid if __name__ == "__main__": print(base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)) python-cyclone-1.1/appskel/foreman/scripts/localefix.py0000644000175000017500000000057612124336260022456 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # $license import re import sys if __name__ == "__main__": try: filename = sys.argv[1] assert filename != "-" fd = open(filename) except: fd = sys.stdin line_re = re.compile(r'="([^"]+)"') for line in fd: line = line_re.sub(r"=\\1", line) sys.stdout.write(line) fd.close() python-cyclone-1.1/appskel/foreman/README.md0000644000175000017500000000614112124336260017720 0ustar lunarlunar# cyclone-based project for heroku and other PaaS based on foreman. This is the source code of $project_name $name <$email> ## About This file has been created automatically by cyclone for $project_name. It contains the following: - ``Procman``: standard foreman file - ``start.sh``: simple shell script to start the development server - ``$modname.conf``: configuration file for the web server - ``$modname/``: web server code - ``frontend/``: static files, templates and locales - ``scripts/``: other useful scripts ### Running For development and testing: gem install foreman cd $project_name foreman start For production on any foreman based env: Follow foreman instructions, configure Procman as needed. Check the .env file and the configuration file for your app. For production at heroku: - Start a git repo git init git add . git commit -m 'first' heroku create $project_name (or whatever name you want) git push heroku master - check your app, make it better, create a db, etc ## Customization This section is dedicated to explaining how to customize your brand new package. ### Databases cyclone provides built-in support for SQLite and Redis databases. It also supports any RDBM supported by the ``twisted.enterprise.adbapi`` module, like MySQL or PostgreSQL. The default configuration file ``$modname.conf`` ships with pre-configured settings for SQLite, Redis and MySQL. The code for loading all the database settings is in ``$modname/config.py``. Feel free to comment or even remove such code, and configuration entries. It shouldn't break the web server. Take a look at ``$modname/utils.py``, which is where persistent database connections are initialized. ### Internationalization cyclone uses the standard ``gettext`` library for dealing with string translation. Make sure you have the ``gettext`` package installed. If you don't, you won't be able to translate your software. For installing the ``gettext`` package on Debian and Ubuntu systems, do this: apt-get install gettext For Mac OS X, I'd suggest using [HomeBrew](http://mxcl.github.com/homebrew>). If you already use HomeBrew, run: brew install gettext brew link gettext For generating translatable files for HTML and Python code of your software, run this: cat frontend/template/*.html $modname/*.py | python scripts/localefix.py | xgettext - --language=Python --from-code=utf-8 --keyword=_:1,2 -d $modname Then translate $modname.po, compile and copy to the appropriate locale directory: (pt_BR is used as example here) vi $modname.po mkdir -p frontend/locale/pt_BR/LC_MESSAGES/ msgfmt $modname.po -o frontend/locale/pt_BR/LC_MESSAGES/$modname.mo There are sample translations for both Spanish and Portuguese in this package, already compiled. ### Cookie Secret The current cookie secret key in ``$modname.conf`` was generated during the creation of this package. However, if you need a new one, you may run the ``scripts/cookie_secret.py`` script to generate a random key. ## Credits - [cyclone](http://github.com/fiorix/cyclone) web server. python-cyclone-1.1/appskel/foreman/.env0000644000175000017500000000001512124336260017224 0ustar lunarlunarPYTHONPATH=. python-cyclone-1.1/appskel/foreman/frontend/0000755000175000017500000000000012124336260020256 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/frontend/locale/0000755000175000017500000000000012124336260021515 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/frontend/locale/es_ES/0000755000175000017500000000000012124336260022513 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/frontend/locale/es_ES/LC_MESSAGES/0000755000175000017500000000000012124336260024300 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/frontend/locale/es_ES/LC_MESSAGES/modname.po0000644000175000017500000000131412124336260026257 0ustar lunarlunar# cyclone-tools sample translation. # Copyright (C) 2011 Alexandre Fiori # This file is distributed under the same license as the cyclone package. # Alexandre Fiori , 2011. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2011-02-24 15:36-0300\n" "PO-Revision-Date: 2011-02-28 15:44+-0300\n" "Last-Translator: Alexandre Fiori \n" "Language-Team: Alexandre Fiori \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" #: standard input:5 msgid "cyclone web server" msgstr "servidor web cyclone" #: standard input:8 msgid "It works!" msgstr "¡Funciona!" python-cyclone-1.1/appskel/foreman/frontend/locale/es_ES/LC_MESSAGES/modname.mo0000644000175000017500000000074112124336260026257 0ustar lunarlunarÞ•4L` akA~ ÀÌIt works!cyclone web serverProject-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2011-02-24 15:36-0300 PO-Revision-Date: 2011-02-28 15:44+-0300 Last-Translator: Alexandre Fiori Language-Team: Alexandre Fiori MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit ¡Funciona!servidor web cyclonepython-cyclone-1.1/appskel/foreman/frontend/locale/pt_BR/0000755000175000017500000000000012124336260022523 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/frontend/locale/pt_BR/LC_MESSAGES/0000755000175000017500000000000012124336260024310 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/frontend/locale/pt_BR/LC_MESSAGES/modname.po0000644000175000017500000000131212124336260026265 0ustar lunarlunar# cyclone-tools sample translation. # Copyright (C) 2011 Alexandre Fiori # This file is distributed under the same license as the cyclone package. # Alexandre Fiori , 2011. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2011-02-24 15:36-0300\n" "PO-Revision-Date: 2011-02-28 15:44+-0300\n" "Last-Translator: Alexandre Fiori \n" "Language-Team: Alexandre Fiori \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" #: standard input:5 msgid "cyclone web server" msgstr "servidor web cyclone" #: standard input:8 msgid "It works!" msgstr "Funciona!" python-cyclone-1.1/appskel/foreman/frontend/locale/pt_BR/LC_MESSAGES/modname.mo0000644000175000017500000000073712124336260026274 0ustar lunarlunarÞ•4L` akA~ ÀÊIt works!cyclone web serverProject-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2011-02-24 15:36-0300 PO-Revision-Date: 2011-02-28 15:44+-0300 Last-Translator: Alexandre Fiori Language-Team: Alexandre Fiori MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Transfer-Encoding: 8bit Funciona!servidor web cyclonepython-cyclone-1.1/appskel/foreman/frontend/template/0000755000175000017500000000000012124336260022071 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/frontend/template/index.html0000644000175000017500000000057712124336260024077 0ustar lunarlunar{% extends 'base.html' %} {% block page %}

{{_("It works!")}}

English  Español  Português

hello = {{hello}}

awesome = {{awesome}}

{% end %} python-cyclone-1.1/appskel/foreman/frontend/template/base.html0000644000175000017500000000140112124336260023665 0ustar lunarlunar {{_("cyclone web server")}}
{% block page %}{% end %}
python-cyclone-1.1/appskel/foreman/frontend/template/post.html0000644000175000017500000000035312124336260023745 0ustar lunarlunar{% extends 'base.html' %} {% block page %}

POST example

This variable exists = {{fields.ip}}

This variable does not exist = {{fields.something}}

This one comes from .conf = {{fields.mysql_host}}

{% end %} python-cyclone-1.1/appskel/foreman/frontend/static/0000755000175000017500000000000012124336260021545 5ustar lunarlunarpython-cyclone-1.1/appskel/foreman/frontend/static/favicon.ico0000644000175000017500000000217612124336260023674 0ustar lunarlunarh(  "ÿ"ÿ"ÿ"ÿ"ÿÛ¼õÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ !ÿ !ÿ"ÿ#ÿ"ÿÛ¼õÛ¼õ"ÿ"ÿ !ÿ !ÿ ÿ"ÿ"ÿ"ÿBCEÿBCEÿBCEÿ !ÿ !ÿ !ÿ#ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !%þBCEÿ"ÿ"ÿ"ÿBCEÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ"ÿ"ÿ"ÿ"ÿBCEÿûýþûýþ"ÿ"ÿBCEÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿûýþÛÝÞ."ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿBCEÿ"ÿBCEÿ"ÿÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿÛ¼õ"ÿ"ÿÛ¼õÛ¼õYZ\ÿÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿÛ¼õÛ¼õÛ¼õÛ¼õ"ÿÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿÛ¼õYZ\ÿ"ÿ"ÿ"ÿÛ¼õYZ\ÿ"ÿƒÁ€`ñÿÿÿÿÿÿóŸóžy½ƒ™ÇÃÿçÿpython-cyclone-1.1/appskel/foreman/requirements.txt0000644000175000017500000000004212124336260021717 0ustar lunarlunarTwisted==12.2.0 cyclone==1.0-rc13 python-cyclone-1.1/appskel/foreman/modname.conf0000644000175000017500000000066612124336260020736 0ustar lunarlunar[server] debug = true xheaders = false xsrf_cookies = false cookie_secret = $cookie_secret [frontend] locale_path = frontend/locale static_path = frontend/static template_path = frontend/template [sqlite] enabled = yes database = :memory: [redis] enabled = no host = 127.0.0.1 port = 6379 dbid = 0 poolsize = 10 [mysql] enabled = no host = 127.0.0.1 port = 3306 username = foo password = bar database = dummy poolsize = 10 debug = no python-cyclone-1.1/appskel/foreman/Procfile0000644000175000017500000000005112124336260020121 0ustar lunarlunarweb: python $modname/main.py foobar.conf python-cyclone-1.1/appskel/signup/0000755000175000017500000000000012124336260016315 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/modname.sql0000644000175000017500000000055112124336260020457 0ustar lunarlunardrop database if exists dummy; create database dummy; grant all privileges on dummy.* to 'foo'@'localhost' identified by 'bar'; use dummy; create table users ( id integer not null auto_increment, user_email varchar(50) not null, user_passwd varchar(40) not null, user_full_name varchar(80) null, user_is_active boolean not null, primary key(id) ); python-cyclone-1.1/appskel/signup/modname/0000755000175000017500000000000012124336260017735 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/modname/views.py0000644000175000017500000002663212124336260021455 0ustar lunarlunar# coding: utf-8 # $license import OpenSSL import cyclone.escape import cyclone.locale import cyclone.mail import cyclone.web import hashlib import random import string from datetime import datetime from twisted.internet import defer from twisted.python import log from $modname import storage from $modname.utils import BaseHandler from $modname.utils import SessionMixin from $modname.utils import TemplateFields class IndexHandler(BaseHandler, SessionMixin): def get(self): if self.current_user: self.redirect("/dashboard") else: self.render("index.html") class LangHandler(BaseHandler): def get(self, lang_code): if lang_code in cyclone.locale.get_supported_locales(): self.set_secure_cookie("lang", lang_code, expires_days=20) self.redirect(self.request.headers.get("Referer", self.get_argument("next", "/"))) class DashboardHandler(BaseHandler): @cyclone.web.authenticated def get(self): self.render("dashboard.html") class AccountHandler(BaseHandler, storage.DatabaseMixin): @cyclone.web.authenticated @storage.DatabaseSafe @defer.inlineCallbacks def get(self): user = yield storage.users.find_first( where=("user_email=%s", self.current_user["email"])) if user: self.render("account.html", fields=TemplateFields(full_name=user["user_full_name"])) else: self.clear_current_user() self.redirect("/") @cyclone.web.authenticated @storage.DatabaseSafe @defer.inlineCallbacks def post(self): user = yield storage.users.find_first( where=("user_email=%s", self.current_user["email"])) if not user: self.clear_current_user() self.redirect("/") defer.returnValue(None) full_name = self.get_argument("full_name", None) f = TemplateFields(full_name=full_name) if full_name: full_name = full_name.strip() if len(full_name) > 80: f["err"] = ["invalid_name"] self.render("account.html", fields=f) defer.returnValue(None) elif full_name != user.user_full_name: user.user_full_name = full_name passwd_0 = self.get_argument("passwd_0", None) passwd_1 = self.get_argument("passwd_1", None) passwd_2 = self.get_argument("passwd_2", None) if passwd_0 and passwd_1: if hashlib.sha1(passwd_0).hexdigest() != user.user_passwd: f["err"] = ["old_nomatch"] self.render("account.html", fields=f) defer.returnValue(None) elif len(passwd_1) < 3 or len(passwd_1) > 20: f["err"] = ["invalid_passwd"] self.render("account.html", fields=f) defer.returnValue(None) elif passwd_1 != passwd_2: f["err"] = ["nomatch"] self.render("account.html", fields=f) defer.returnValue(None) else: user.user_passwd = hashlib.sha1(passwd_1).hexdigest() elif passwd_1: f["err"] = ["old_missing"] self.render("account.html", fields=f) defer.returnValue(None) if user.has_changes: yield user.save() f["updated"] = True self.render("account.html", fields=f) class SignUpHandler(BaseHandler, storage.DatabaseMixin): def get(self): if self.get_current_user(): self.redirect("/") else: self.render("signup.html", fields=TemplateFields()) @storage.DatabaseSafe @defer.inlineCallbacks def post(self): email = self.get_argument("email", None) legal = self.get_argument("legal", None) f = TemplateFields(email=email, legal=legal) if legal != "on": f["err"] = ["legal"] self.render("signup.html", fields=f) defer.returnValue(None) if not email: f["err"] = ["email"] self.render("signup.html", fields=f) defer.returnValue(None) if not self.valid_email(email): f["err"] = ["email"] self.render("signup.html", fields=f) defer.returnValue(None) # check if the email is awaiting confirmation if (yield self.redis.exists("u:%s" % email)): f["err"] = ["exists"] self.render("signup.html", fields=f) defer.returnValue(None) # check if the email exists in the database if (yield storage.users.find_first(where=("user_email=%s", email))): f["err"] = ["exists"] self.render("signup.html", fields=f) defer.returnValue(None) # create random password random.seed(OpenSSL.rand.bytes(16)) passwd = "".join(random.choice(string.letters + string.digits) for x in range(8)) # store temporary password in redis k = "u:%s" % email t = yield self.redis.multi() t.set(k, passwd) t.expire(k, 86400) # 1 day yield t.commit() # prepare the confirmation email msg = cyclone.mail.Message( mime="text/html", charset="utf-8", to_addrs=[email], from_addr=self.settings.email_settings.username, subject=self.render_string("signup_email_subject.txt") .replace("\n", "").strip(), message=self.render_string("signup_email.html", passwd=passwd, ip=self.request.remote_ip, date=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S GMT"))) try: r = yield cyclone.mail.sendmail(self.settings.email_settings, msg) except Exception, e: # delete password from redis yield self.redis.delete(k) log.err("failed to send signup email to %s: %s" % (email, e)) f["err"] = ["send"] self.render("signup.html", fields=f) else: log.msg("signup email sent to %s: %s" % (email, r)) self.render("signup_ok.html", email=email) class SignInHandler(BaseHandler, storage.DatabaseMixin): def get(self): if self.get_current_user(): self.redirect("/") else: self.render("signin.html", fields=TemplateFields()) @storage.DatabaseSafe @defer.inlineCallbacks def post(self): email = self.get_argument("email", "") passwd = self.get_argument("passwd", "") remember = self.get_argument("remember", "") f = TemplateFields(email=email, remember=remember) if not email: f["err"] = ["auth"] self.render("signin.html", fields=f) defer.returnValue(None) if not self.valid_email(email): f["err"] = ["auth"] self.render("signin.html", fields=f) defer.returnValue(None) if not passwd: f["err"] = ["auth"] self.render("signin.html", fields=f) defer.returnValue(None) user = None # check if the user is awaiting confirmation k = "u:%s" % email pwd = yield self.redis.get(k) if pwd: if pwd != passwd: f["err"] = ["auth"] self.render("signin.html", fields=f) defer.returnValue(None) else: # check if the user is already in mysql user = yield storage.users.find_first( where=("user_email=%s", email)) if not user: # create the user in mysql user = storage.users.new(user_email=email) user.user_passwd = hashlib.sha1(pwd).hexdigest() user.user_is_active = True yield user.save() yield self.redis.delete(k) if not user: user = yield storage.users.find_first( where=("user_email=%s and user_passwd=%s", email, hashlib.sha1(passwd).hexdigest())) if not user: f["err"] = ["auth"] self.render("signin.html", fields=f) defer.returnValue(None) # always update the lang cookie if self.locale.code in cyclone.locale.get_supported_locales(): self.set_secure_cookie("lang", self.locale.code, expires_days=20) # set session cookie self.set_current_user(email=email, expires_days=15 if remember else None) self.redirect("/") class SignOutHandler(BaseHandler): @cyclone.web.authenticated def get(self): self.clear_current_user() self.redirect("/") class PasswdHandler(BaseHandler, storage.DatabaseMixin): def get(self): if self.get_current_user(): self.redirect("/") else: self.render("passwd.html", fields=TemplateFields()) @storage.DatabaseSafe @defer.inlineCallbacks def post(self): email = self.get_argument("email", None) f = TemplateFields(email=email) if not email: f["err"] = ["email"] self.render("passwd.html", fields=f) defer.returnValue(None) if not self.valid_email(email): f["err"] = ["email"] self.render("passwd.html", fields=f) defer.returnValue(None) k = "u:%s" % email # check if the user exists in redis, or mysql if (yield self.redis.exists(k)): f["err"] = ["pending"] self.render("passwd.html", fields=f) defer.returnValue(None) elif not (yield storage.users.find_first( where=("user_email=%s", email))): f["err"] = ["notfound"] self.render("passwd.html", fields=f) defer.returnValue(None) # create temporary password and store in redis random.seed(OpenSSL.rand.bytes(16)) passwd = "".join(random.choice(string.letters + string.digits) for x in range(8)) t = yield self.redis.multi() t.set(k, passwd) t.expire(k, 86400) # 1 day yield t.commit() # prepare the confirmation email msg = cyclone.mail.Message( mime="text/html", charset="utf-8", to_addrs=[email], from_addr=self.settings.email_settings.username, subject=self.render_string("passwd_email_subject.txt") .replace("\n", "").strip(), message=self.render_string("passwd_email.html", passwd=passwd, ip=self.request.remote_ip, date=datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S GMT"))) try: r = yield cyclone.mail.sendmail(self.settings.email_settings, msg) except Exception, e: # do not delete from redis # yield self.redis.delete(k) log.err("failed to send passwd email to %s: %s" % (email, e)) f["err"] = ["send"] self.render("passwd.html", fields=f) else: log.msg("passwd email sent to %s: %s" % (email, r)) self.render("passwd_ok.html", email=email) python-cyclone-1.1/appskel/signup/modname/web.py0000644000175000017500000000236212124336260021067 0ustar lunarlunar# coding: utf-8 # $license import cyclone.locale import cyclone.web from $modname import views from $modname import config from $modname.storage import DatabaseMixin class Application(cyclone.web.Application): def __init__(self, config_file): conf = config.parse_config(config_file) handlers = [ (r"/", views.IndexHandler), (r"/lang/(.+)", views.LangHandler), (r"/dashboard", views.DashboardHandler), (r"/account", views.AccountHandler), (r"/signup", views.SignUpHandler), (r"/signin", views.SignInHandler), (r"/signout", views.SignOutHandler), (r"/passwd", views.PasswdHandler), (r"/legal", cyclone.web.RedirectHandler, {"url": "/static/legal.txt"}), ] # Initialize locales if "locale_path" in conf: cyclone.locale.load_gettext_translations(conf["locale_path"], "$modname") # Set up database connections DatabaseMixin.setup(conf) conf["login_url"] = "/signin" conf["autoescape"] = None cyclone.web.Application.__init__(self, handlers, **conf) python-cyclone-1.1/appskel/signup/modname/utils.py0000644000175000017500000000706012124336260021452 0ustar lunarlunar# coding: utf-8 # $license import OpenSSL import cyclone.escape import cyclone.web import httplib import re import uuid from twisted.internet import defer from $modname.storage import DatabaseMixin class TemplateFields(dict): """Helper class to make sure our template doesn't fail due to an invalid key""" def __getattr__(self, name): try: return self[name] except KeyError: return None def __setattr__(self, name, value): self[name] = value class BaseHandler(cyclone.web.RequestHandler): _email = re.compile("^[a-zA-Z0-9._%-]+@[a-zA-Z0-9._%-]+.[a-zA-Z]{2,8}$$") def valid_email(self, email): return self._email.match(email) def set_current_user(self, expires_days=1, **kwargs): self.set_secure_cookie("user", cyclone.escape.json_encode(kwargs), expires_days=expires_days) def get_current_user(self): user_json = self.get_secure_cookie("user", max_age_days=1) if user_json: return cyclone.escape.json_decode(user_json) def clear_current_user(self): self.clear_cookie("user") def get_user_locale(self): lang = self.get_secure_cookie("lang") if lang: return cyclone.locale.get(lang) # custom http error pages def write_error(self, status_code, **kwargs): kwargs["code"] = status_code kwargs["message"] = httplib.responses[status_code] try: self.render("error_%d.html" % status_code, fields=kwargs) except IOError: self.render("error_all.html", fields=kwargs) class SessionMixin(DatabaseMixin): session_cookie_name = "session" session_redis_prefix = "$modname:s:" @property def session_redis_key(self): token = self.get_secure_cookie(self.session_cookie_name) if token: return "%s%s" % (self.session_redis_prefix, token) @defer.inlineCallbacks def session_create(self, expires_days=1, **kwargs): if not kwargs: raise ValueError("session_create requires one or more key=val") token = uuid.UUID(bytes=OpenSSL.rand.bytes(16)).hex k = "%s%s" % (self.session_redis_prefix, token) yield self.redis.hmset(k, kwargs) yield self.redis.expire(k, expires_days * 86400) self.set_secure_cookie(self.session_cookie_name, token, expires_days=expires_days) defer.returnValue(token) @defer.inlineCallbacks def session_exists(self): k = self.session_redis_key if k: defer.returnValue((yield self.redis.exists(k))) @defer.inlineCallbacks def session_set(self, **kwargs): if not kwargs: raise ValueError("session_set requires one or more key=val") k = self.session_redis_key if k: yield self.redis.hmset(k, kwargs) defer.returnValue(True) @defer.inlineCallbacks def session_get(self, *args): if not args: raise ValueError("session_get requires one or more key names") k = self.session_redis_key if k: r = yield self.redis.hmget(k, args) defer.returnValue(r[0] if len(args) == 1 else r) @defer.inlineCallbacks def session_getall(self): k = self.session_redis_key if k: defer.returnValue((yield self.redis.hgetall(k))) @defer.inlineCallbacks def session_destroy(self): k = self.session_redis_key if k: yield self.redis.delete(k) defer.returnValue(True) python-cyclone-1.1/appskel/signup/modname/storage.py0000644000175000017500000000775712124336260021773 0ustar lunarlunar# coding: utf-8 # $license try: sqlite_ok = True import cyclone.sqlite except ImportError, sqlite_err: sqlite_ok = False import MySQLdb import cyclone.redis import functools from twisted.internet import defer from twisted.internet import reactor from twisted.python import log from $modname import txdbapi class users(txdbapi.DatabaseModel): pass def DatabaseSafe(method): """This decorator function makes all database calls safe from connection errors. It returns an HTTP 503 when either redis or mysql are temporarily disconnected. @DatabaseSafe def get(self): now = yield self.mysql.runQuery("select now()") print now """ @defer.inlineCallbacks @functools.wraps(method) def run(self, *args, **kwargs): try: r = yield defer.maybeDeferred(method, self, *args, **kwargs) except cyclone.redis.ConnectionError, e: m = "redis.ConnectionError: %s" % e log.msg(m) raise cyclone.web.HTTPError(503, m) # Service Unavailable except (MySQLdb.InterfaceError, MySQLdb.OperationalError), e: m = "mysql.Error: %s" % e log.msg(m) raise cyclone.web.HTTPError(503, m) # Service Unavailable else: defer.returnValue(r) return run class DatabaseMixin(object): mysql = None redis = None sqlite = None @classmethod def setup(cls, conf): if "sqlite_settings" in conf: if sqlite_ok: DatabaseMixin.sqlite = \ cyclone.sqlite.InlineSQLite(conf["sqlite_settings"].database) else: log.err("SQLite is currently disabled: %s" % sqlite_err) if "redis_settings" in conf: if conf["redis_settings"].get("unixsocket"): DatabaseMixin.redis = \ cyclone.redis.lazyUnixConnectionPool( conf["redis_settings"].unixsocket, conf["redis_settings"].dbid, conf["redis_settings"].poolsize) else: DatabaseMixin.redis = \ cyclone.redis.lazyConnectionPool( conf["redis_settings"].host, conf["redis_settings"].port, conf["redis_settings"].dbid, conf["redis_settings"].poolsize) if "mysql_settings" in conf: txdbapi.DatabaseModel.db = DatabaseMixin.mysql = \ txdbapi.ConnectionPool("MySQLdb", host=conf["mysql_settings"].host, port=conf["mysql_settings"].port, db=conf["mysql_settings"].database, user=conf["mysql_settings"].username, passwd=conf["mysql_settings"].password, cp_min=1, cp_max=conf["mysql_settings"].poolsize, cp_reconnect=True, cp_noisy=conf["mysql_settings"].debug) # Ping MySQL to avoid timeouts. On timeouts, the first query # responds with the following error, before it reconnects: # mysql.Error: (2006, 'MySQL server has gone away') # # There's no way differentiate this from the server shutting down # and write() failing. To avoid the timeout, we ping. @defer.inlineCallbacks def _ping_mysql(): try: yield cls.mysql.runQuery("select 1") except Exception, e: log.msg("MySQL ping error:", e) else: if conf["mysql_settings"].debug: log.msg("MySQL ping: OK") reactor.callLater(conf["mysql_settings"].ping, _ping_mysql) if conf["mysql_settings"].ping > 1: _ping_mysql() python-cyclone-1.1/appskel/signup/modname/txdbapi.py0000644000175000017500000002345012124336260021746 0ustar lunarlunar# coding: utf-8 # http://en.wikipedia.org/wiki/Active_record_pattern # http://en.wikipedia.org/wiki/Create,_read,_update_and_delete # $license import sqlite3 import sys import types from twisted.enterprise import adbapi from twisted.internet import defer class InlineSQLite: def __init__(self, dbname, autocommit=True, cursorclass=None): self.autocommit = autocommit self.conn = sqlite3.connect(dbname) if cursorclass: self.conn.row_factory = cursorclass self.curs = self.conn.cursor() def runQuery(self, query, *args, **kwargs): self.curs.execute(query.replace("%s", "?"), *args, **kwargs) return self.curs.fetchall() def runOperation(self, command, *args, **kwargs): self.curs.execute(command.replace("%s", "?"), *args, **kwargs) if self.autocommit is True: self.conn.commit() def runOperationMany(self, command, *args, **kwargs): self.curs.executemany(command.replace("%s", "?"), *args, **kwargs) if self.autocommit is True: self.conn.commit() def runInteraction(self, interaction, *args, **kwargs): return interaction(self.curs, *args, **kwargs) def commit(self): self.conn.commit() def rollback(self): self.conn.rollback() def close(self): self.conn.close() def ConnectionPool(dbapiName, *args, **kwargs): if dbapiName == "sqlite3": if sys.version_info < (2, 6): # hax for py2.5 def __row(cursor, row): d = {} for idx, col in enumerate(cursor.description): d[col[0]] = row[idx] return d kwargs["cursorclass"] = __row else: kwargs["cursorclass"] = sqlite3.Row return InlineSQLite(*args, **kwargs) elif dbapiName == "MySQLdb": import MySQLdb.cursors kwargs["cursorclass"] = MySQLdb.cursors.DictCursor return adbapi.ConnectionPool(dbapiName, *args, **kwargs) elif dbapiName == "psycopg2": import psycopg2 import psycopg2.extras psycopg2.connect = psycopg2.extras.RealDictConnection return adbapi.ConnectionPool(dbapiName, *args, **kwargs) else: raise ValueError("Database %s is not yet supported." % dbapiName) class DatabaseObject(object): def __init__(self, model, row): self._model = model self._changes = set() self._data = {} for k, v in dict(row).items(): self.__setattr__(k, v) def __setattr__(self, k, v): if k[0] == "_": object.__setattr__(self, k, v) else: if k in self._data: self._changes.add(k) if k in self._model.codecs and \ not isinstance(v, types.StringTypes): self._data[k] = self._model.codecs[k][0](v) else: self._data[k] = v def __getattr__(self, k): if [0] == "_": object.__getattr__(self, k) else: return self._model.codecs[k][1](self._data[k]) \ if k in self._model.codecs else self._data[k] def __setitem__(self, k, v): self.__setattr__(k, v) def __getitem__(self, k): return self.__getattr__(k) def get(self, k, default=None): return self._data.get(k, default) @property def has_changes(self): return bool(self._changes) @defer.inlineCallbacks def save(self, force=False): if "id" in self._data: if self._changes and not force: kv = dict(map(lambda k: (k, self._data[k]), self._changes)) kv["where"] = ("id=%s", self._data["id"]) yield self._model.update(**kv) elif force: k, v = self._data.items() yield self._model.update(set=(k, v), where=("id=%s", self._data["id"])) self._changes.clear() defer.returnValue(self) else: rs = yield self._model.insert(**self._data) self["id"] = rs["id"] defer.returnValue(self) @defer.inlineCallbacks def delete(self): if "id" in self._data: yield self._model.delete(where=("id=%s", self._data["id"])) self._data.pop("id") defer.returnValue(self) def __repr__(self): return repr(self._data) class DatabaseCRUD(object): #db = None allow = [] deny = [] codecs = {} @classmethod def __table__(cls): return getattr(cls, "table_name", cls.__name__) @classmethod def kwargs_cleanup(cls, kwargs): if cls.allow: deny = cls.deny + [k for k in kwargs if k not in cls.allow] else: deny = cls.deny if deny: map(lambda k: kwargs.pop(k, None), deny) return kwargs @classmethod @defer.inlineCallbacks def insert(cls, **kwargs): kwargs = cls.kwargs_cleanup(kwargs) keys = kwargs.keys() q = "insert into %s (%s) values " % (cls.__table__(), ",".join(keys)) + "(%s)" vs = [] vd = [] for v in kwargs.itervalues(): vs.append("%s") vd.append(v["id"] if isinstance(v, DatabaseObject) else v) if isinstance(cls.db, InlineSQLite): vs = [s.replace("%s", "?") for s in vs] q = q % ",".join(vs) if "id" in kwargs: yield cls.db.runOperation(q, vd) else: def _insert_transaction(trans, *args, **kwargs): trans.execute(*args, **kwargs) if isinstance(cls.db, InlineSQLite): trans.execute("select last_insert_rowid() as id") elif cls.db.dbapiName == "MySQLdb": trans.execute("select last_insert_id() as id") elif cls.db.dbapiName == "psycopg2": trans.execute("select currval('%s_id_seq') as id" % cls.__table__()) return trans.fetchall() r = yield cls.db.runInteraction(_insert_transaction, q, vd) kwargs["id"] = r[0]["id"] defer.returnValue(DatabaseObject(cls, kwargs)) @classmethod def update(cls, **kwargs): where = kwargs.pop("where", None) kwargs = cls.kwargs_cleanup(kwargs) keys = kwargs.keys() vals = [kwargs[k] for k in keys] keys = ",".join(["%s=%s" % (k, "%s") for k in keys]) if where: where, args = where[0], list(where[1:]) for arg in args: if isinstance(arg, DatabaseObject): vals.append(arg["id"]) else: vals.append(arg) return cls.db.runOperation("update %s set %s where %s" % (cls.__table__(), keys, where), vals) else: return cls.db.runOperation("update %s set %s" % (cls.__table__(), keys), vals) @classmethod @defer.inlineCallbacks def select(cls, **kwargs): extra = [] star = "id,*" if isinstance(cls.db, InlineSQLite) else "*" if "groupby" in kwargs: extra.append("group by %s" % kwargs["groupby"]) if "orderby" in kwargs: extra.append("order by %s" % kwargs["orderby"]) if "asc" in kwargs and kwargs["asc"] is True: extra.append("asc") if "desc" in kwargs and kwargs["desc"] is True: extra.append("desc") if "limit" in kwargs: extra.append("limit %s" % kwargs["limit"]) if "offset" in kwargs: extra.append("offset %s" % kwargs["offset"]) extra = " ".join(extra) if "where" in kwargs: where, args = kwargs["where"][0], list(kwargs["where"][1:]) for n, arg in enumerate(args): if isinstance(arg, DatabaseObject): args[n] = arg["id"] rs = yield cls.db.runQuery("select %s from %s where %s %s" % (star, cls.__table__(), where, extra), args) else: rs = yield cls.db.runQuery("select %s from %s %s" % (star, cls.__table__(), extra)) result = map(lambda d: DatabaseObject(cls, d), rs) defer.returnValue(result) @classmethod def delete(cls, **kwargs): if "where" in kwargs: where, args = kwargs["where"][0], kwargs["where"][1:] return cls.db.runOperation("delete from %s where %s" % (cls.__table__(), where), args) else: return cls.db.runOperation("delete from %s" % cls.__table__()) def __str__(self): return str(self.data) class DatabaseModel(DatabaseCRUD): @classmethod @defer.inlineCallbacks def count(cls, **kwargs): if "where" in kwargs: where, args = kwargs["where"][0], kwargs["where"][1:] rs = yield cls.db.runQuery("select count(*) as count from %s" "where %s" % (cls.__table__(), where), args) else: rs = yield cls.db.runQuery("select count(*) as count from %s" % cls.__table__()) defer.returnValue(rs[0]["count"]) @classmethod def all(cls): return cls.select() @classmethod def find(cls, **kwargs): return cls.select(**kwargs) @classmethod @defer.inlineCallbacks def find_first(cls, **kwargs): kwargs["limit"] = 1 rs = yield cls.select(**kwargs) defer.returnValue(rs[0] if rs else None) @classmethod def new(cls, **kwargs): return DatabaseObject(cls, kwargs) python-cyclone-1.1/appskel/signup/modname/__init__.py0000644000175000017500000000012312124336260022042 0ustar lunarlunar# coding: utf-8 # $license __author__ = "$name <$email>" __version__ = "$version" python-cyclone-1.1/appskel/signup/modname/config.py0000644000175000017500000000677312124336260021571 0ustar lunarlunar# coding: utf-8 # $license import os import sys import ConfigParser from cyclone.util import ObjectDict def tryget(func, section, option, default=None): try: return func(section, option) except ConfigParser.NoOptionError: return default def my_parse_config(filename): cp = ConfigParser.RawConfigParser() cp.read([filename]) conf = dict(raw=cp, config_file=filename) # server settings conf["debug"] = tryget(cp.getboolean, "server", "debug", False) conf["xheaders"] = tryget(cp.getboolean, "server", "xheaders", False) conf["cookie_secret"] = cp.get("server", "cookie_secret") conf["xsrf_cookies"] = tryget(cp.getboolean, "server", "xsrf_cookies", False) # make relative path absolute to this file's parent directory root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) getpath = lambda k, v: os.path.join(root, tryget(cp.get, k, v)) # locale, template and static directories conf["locale_path"] = getpath("frontend", "locale_path") conf["static_path"] = getpath("frontend", "static_path") conf["template_path"] = getpath("frontend", "template_path") # sqlite support if tryget(cp.getboolean, "sqlite", "enabled", False) is True: conf["sqlite_settings"] = \ ObjectDict(database=cp.get("sqlite", "database")) # redis support if tryget(cp.getboolean, "redis", "enabled", False) is True: conf["redis_settings"] = ObjectDict( unixsocket=tryget(cp.get, "redis", "unixsocket", None), host=tryget(cp.get, "redis", "host", "127.0.0.1"), port=tryget(cp.getint, "redis", "port", 6379), dbid=tryget(cp.getint, "redis", "dbid", 0), poolsize=tryget(cp.getint, "redis", "poolsize", 10)) else: raise ValueError("Redis is mandatory, but is currently disabled " "in $modname.conf. Not running.") # mysql support if tryget(cp.getboolean, "mysql", "enabled", False) is True: conf["mysql_settings"] = ObjectDict( host=cp.get("mysql", "host"), port=cp.getint("mysql", "port"), username=tryget(cp.get, "mysql", "username"), password=tryget(cp.get, "mysql", "password"), database=tryget(cp.get, "mysql", "database"), poolsize=tryget(cp.getint, "mysql", "poolsize", 10), debug=tryget(cp.getboolean, "mysql", "debug", False), ping=tryget(cp.getint, "mysql", "ping_interval")) else: raise ValueError("MySQL is mandatory, but is currently disabled " "in $modname.conf. Not running.") # email support if tryget(cp.getboolean, "email", "enabled", False) is True: conf["email_settings"] = ObjectDict( host=cp.get("email", "host"), port=tryget(cp.getint, "email", "port"), tls=tryget(cp.getboolean, "email", "tls"), username=tryget(cp.get, "email", "username"), password=tryget(cp.get, "email", "password")) else: raise ValueError("Email is mandatory, but is currently disabled " "in $modname.conf. Not running.") return conf def parse_config(filename): try: return my_parse_config(filename) except Exception, e: print("Error parsing %s: %s" % (filename, e)) sys.exit(1) python-cyclone-1.1/appskel/signup/start.sh0000755000175000017500000000030312124336260020005 0ustar lunarlunar#!/bin/bash # see scripts/debian-init.d for production deployments export PYTHONPATH=`dirname $$0` twistd -n cyclone -p 8888 -l 0.0.0.0 \ -r $modname.web.Application -c $modname.conf $$* python-cyclone-1.1/appskel/signup/.gitignore0000644000175000017500000000003112124336260020277 0ustar lunarlunar*.swp *.pyc dropin.cache python-cyclone-1.1/appskel/signup/scripts/0000755000175000017500000000000012124336260020004 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/scripts/cookie_secret.py0000644000175000017500000000025512124336260023176 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # $license import base64 import uuid if __name__ == "__main__": print(base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes)) python-cyclone-1.1/appskel/signup/scripts/debian-init.d0000644000175000017500000000330612124336260022336 0ustar lunarlunar#!/bin/sh ### BEGIN INIT INFO # Provides: $modname # Required-Start: $$all # Required-Stop: $$all # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Starts a service on the cyclone web server # Description: Foobar ### END INIT INFO PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin DAEMON=/usr/bin/twistd SERVICE_DIR=/path/to/$modname SERVICE_NAME=$modname PYTHONPATH=$$SERVICE_DIR:$$PYTHONPATH export PYTHONPATH PORT=8888 LISTEN="127.0.0.1" CONFIG=$$SERVICE_DIR/$$SERVICE_NAME.conf PIDFILE=/var/run/$$SERVICE_NAME.pid LOGFILE=/var/log/$$SERVICE_NAME.log APP=$${SERVICE_NAME}.web.Application USER=www-data GROUP=www-data DAEMON_OPTS="-u $$USER -g $$GROUP --pidfile=$$PIDFILE --logfile=$$LOGFILE cyclone --port $$PORT --listen $$LISTEN --app $$APP -c $$CONFIG" if [ ! -x $$DAEMON ]; then echo "ERROR: Can't execute $$DAEMON." exit 1 fi if [ ! -d $$SERVICE_DIR ]; then echo "ERROR: Directory doesn't exist: $$SERVICE_DIR" exit 1 fi start_service() { echo -n " * Starting $$SERVICE_NAME... " start-stop-daemon -Sq -p $$PIDFILE -x $$DAEMON -- $$DAEMON_OPTS e=$$? if [ $$e -eq 1 ]; then echo "already running" return fi if [ $$e -eq 255 ]; then echo "couldn't start" return fi echo "done" } stop_service() { echo -n " * Stopping $$SERVICE_NAME... " start-stop-daemon -Kq -R 10 -p $$PIDFILE e=$$? if [ $$e -eq 1 ]; then echo "not running" return fi echo "done" } case "$$1" in start) start_service ;; stop) stop_service ;; restart) stop_service start_service ;; *) echo "Usage: /etc/init.d/$$SERVICE_NAME {start|stop|restart}" >&2 exit 1 ;; esac exit 0 python-cyclone-1.1/appskel/signup/scripts/debian-multicore-init.d0000644000175000017500000000443312124336260024341 0ustar lunarlunar#!/bin/bash ### BEGIN INIT INFO # Provides: $modname # Required-Start: $$all # Required-Stop: $$all # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Starts a service on the cyclone web server # Description: Foobar ### END INIT INFO PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin DAEMON=/usr/bin/twistd SERVICE_DIR=/path/to/$modname SERVICE_NAME=$modname PYTHONPATH=$$SERVICE_DIR:$$PYTHONPATH export PYTHONPATH INSTANCES=4 START_PORT=9901 LISTEN="127.0.0.1" CONFIG=$$SERVICE_DIR/$$SERVICE_NAME.conf APP=$${SERVICE_NAME}.web.Application USER=www-data GROUP=www-data # Check out the start_service function for other customization options # such as setting CPU affinity. if [ ! -x $$DAEMON ]; then echo "ERROR: Can't execute $$DAEMON." exit 1 fi if [ ! -d $$SERVICE_DIR ]; then echo "ERROR: Directory doesn't exist: $$SERVICE_DIR" exit 1 fi start_service() { echo -n " * Starting $$SERVICE_NAME... " for n in `seq 1 $$INSTANCES` do PORT=$$[START_PORT] PIDFILE=/var/run/$$SERVICE_NAME.$$PORT.pid LOGFILE=/var/log/$$SERVICE_NAME.$$PORT.log DAEMON_OPTS="-u $$USER -g $$GROUP --pidfile=$$PIDFILE --logfile=$$LOGFILE cyclone --port $$PORT --listen $$LISTEN --app $$APP -c $$CONFIG" START_PORT=$$[PORT+1] start-stop-daemon -Sq -p $$PIDFILE -x $$DAEMON -- $$DAEMON_OPTS e=$$? if [ $$e -eq 1 ]; then echo "already running" return fi if [ $$e -eq 255 ]; then echo "couldn't start" return fi # Set CPU affinity if [ -x /usr/bin/taskset ]; then sleep 1 /usr/bin/taskset -pc $$n `cat $$PIDFILE` &> /dev/null fi done echo "done" } stop_service() { echo -n " * Stopping $$SERVICE_NAME... " for n in `seq 1 $$INSTANCES` do PORT=$$[START_PORT] PIDFILE=/var/run/$$SERVICE_NAME.$$PORT.pid START_PORT=$$[PORT+1] start-stop-daemon -Kq -R 10 -p $$PIDFILE e=$$? if [ $$e -eq 1 ]; then echo "not running" return fi done echo "done" } case "$$1" in start) start_service ;; stop) stop_service ;; restart) sp=$$START_PORT stop_service START_PORT=$$sp start_service ;; *) echo "Usage: /etc/init.d/$$SERVICE_NAME {start|stop|restart}" >&2 exit 1 ;; esac exit 0 python-cyclone-1.1/appskel/signup/scripts/localefix.py0000644000175000017500000000057612124336260022334 0ustar lunarlunar#!/usr/bin/env python # coding: utf-8 # $license import re import sys if __name__ == "__main__": try: filename = sys.argv[1] assert filename != "-" fd = open(filename) except: fd = sys.stdin line_re = re.compile(r'="([^"]+)"') for line in fd: line = line_re.sub(r"=\\1", line) sys.stdout.write(line) fd.close() python-cyclone-1.1/appskel/signup/README.md0000644000175000017500000000607212124336260017601 0ustar lunarlunar# cyclone-based project This is the source code of $project_name $name <$email> ## About This file has been created automatically by cyclone for $project_name. It contains the following: - ``start.sh``: simple shell script to start the development server - ``$modname.conf``: configuration file for the web server - ``$modname/``: web server code - ``frontend/``: static files, templates and locales - ``scripts/``: debian init scripts and other useful scripts ### Running For development and testing: twistd -n cyclone --help twistd -n cyclone -r $modname.web.Application [--help] or just run ./start.sh For production: twistd cyclone \ --logfile=/var/log/$project.log \ --pidfile=/var/run/$project.pid \ -r $modname.web.Application or check scripts/debian-init.d and scripts/debian-multicore-init.d ## Customization This section is dedicated to explaining how to customize your brand new package. ### Databases cyclone provides built-in support for SQLite and Redis databases. It also supports any RDBM supported by the ``twisted.enterprise.adbapi`` module, like MySQL or PostgreSQL. The default configuration file ``$modname.conf`` ships with pre-configured settings for SQLite, Redis and MySQL. The code for loading all the database settings is in ``$modname/config.py`` and is required by this application. Take a look at ``$modname/storage.py``, which is where persistent database connections are initialized. This template uses the experimental ``$modname/txdbapi.py`` for interacting with MySQL. ### Email Please edit ``$modname.conf`` and adjust the email settings. This server sends email on user sign up, and to reset passwords. ### Internationalization cyclone uses the standard ``gettext`` library for dealing with string translation. Make sure you have the ``gettext`` package installed. If you don't, you won't be able to translate your software. For installing the ``gettext`` package on Debian and Ubuntu systems, do this: apt-get install gettext For Mac OS X, I'd suggest using [HomeBrew](http://mxcl.github.com/homebrew>). If you already use HomeBrew, run: brew install gettext brew link gettext For generating translatable files for HTML and Python code of your software, run this: cat frontend/template/*.html $modname/*.py | python scripts/localefix.py | xgettext - --language=Python --from-code=utf-8 --keyword=_:1,2 -d $modname Then translate $modname.po, compile and copy to the appropriate locale directory: (pt_BR is used as example here) vi $modname.po mkdir -p frontend/locale/pt_BR/LC_MESSAGES/ msgfmt $modname.po -o frontend/locale/pt_BR/LC_MESSAGES/$modname.mo There are sample translations for both Spanish and Portuguese in this package, already compiled. ### Cookie Secret The current cookie secret key in ``$modname.conf`` was generated during the creation of this package. However, if you need a new one, you may run the ``scripts/cookie_secret.py`` script to generate a random key. ## Credits - [cyclone](http://github.com/fiorix/cyclone) web server. python-cyclone-1.1/appskel/signup/frontend/0000755000175000017500000000000012124336260020134 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/frontend/locale/0000755000175000017500000000000012124336260021373 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/frontend/locale/es_ES/0000755000175000017500000000000012124336260022371 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/frontend/locale/es_ES/LC_MESSAGES/0000755000175000017500000000000012124336260024156 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/frontend/locale/es_ES/LC_MESSAGES/modname.po0000644000175000017500000001152212124336260026137 0ustar lunarlunar# cyclone signup template. # Copyright (C) 2012 cyclone # This file is distributed under the same license as the cyclone package. # Alexandre Fiori , 2012. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-12-11 23:20-0500\n" "PO-Revision-Date: 2012-12-12 00:00:00+0500\n" "Last-Translator: Alexandre Fiori \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: standard input:36 msgid "Account settings" msgstr "Datos de la conta" #: standard input:37 msgid "Name" msgstr "Nombre" #: standard input:39 msgid "Password" msgstr "Contraseña" #: standard input:45 msgid "Invalid name" msgstr "Nombra inválido" #: standard input:47 msgid "Invalid password" msgstr "Contraseña inválida" #: standard input:49 msgid "Passwords don't match" msgstr "Las contraseñas no coinciden" #: standard input:53 msgid "Saved!" msgstr "Guardado!" #: standard input:55 msgid "Update" msgstr "Actualizar" #: standard input:84 msgid "Dashboard" msgstr "Tablero" #: standard input:90 msgid "Account" msgstr "Cuenta" #: standard input:92 msgid "Language" msgstr "Idioma" #: standard input:100 msgid "Sign out" msgstr "Cerrar" #: standard input:139 msgid "Bootstrap starter template" msgstr "Template inicial de Bootstrap" #: standard input:140 msgid "" "Use this document as a way to quick start any new project.
All you get " "is this message and a barebones HTML document." msgstr "" "Utilice este documento para cualquier nuevo proyecto como una forma de " "de inicio rápido.
Solo tiene es este mensaje y un documento básico HTML." #: standard input:246 standard input:352 standard input:430 standard input:504 #: standard input:580 standard input:665 msgid "Home" msgstr "Inicio" #: standard input:247 standard input:353 standard input:431 standard input:505 #: standard input:513 standard input:522 standard input:581 standard input:666 msgid "Sign in" msgstr "Entrar" #: standard input:255 msgid "Super awesome marketing speak!" msgstr "Hablar de marketing impressionante!" #: standard input:257 msgid "Sign up today" msgstr "Inscríbase hoy" #: standard input:361 msgid "Reset password" msgstr "Restablecer contraseña" #: standard input:366 standard input:594 msgid "Invalid email address" msgstr "Correo electrónico no válido" #: standard input:368 msgid "Email address not registered" msgstr "Correo electrónico no registrada" #: standard input:370 standard input:598 msgid "Please try again later" msgstr "Por favor inténtelo más tarde" #: standard input:374 msgid "Send" msgstr "Enviar" #: standard input:386 msgid "New password" msgstr "Nueva contraseña" #: standard input:387 msgid "" "Your password has been reset, and must be used to reactivate the account " "within 24 hours." msgstr "" "La contraseña se ha restablecido y debe utilizarse para reactivar la cuenta " "dentro de 24 horas." #: standard input:388 msgid "Here's the new password:" msgstr "Aquí está la nueva contraseña:" #: standard input:391 standard input:625 #, python-format msgid "Requested by IP %s on %s" msgstr "Solicitado por IP %s en %s" #: standard input:440 standard input:675 #, python-format msgid "A confirmation email has been sent to %s with instructions." msgstr "Ha enviado un email de confirmación a %s con instrucciones." #: standard input:517 msgid "Invalid email or password" msgstr "Correo electrónico o contraseña no válida" #: standard input:520 msgid "Remember me" msgstr "Recuerde" #: standard input:523 msgid "Forgot password?" msgstr "¿Olvidó la contraseña?" #: standard input:589 standard input:608 msgid "Sign up" msgstr "Registrarse" #: standard input:596 msgid "Email already registered" msgstr "Correo electrónico registrado" #: standard input:603 msgid "I agree with the " msgstr "Estoy de acuerdo con los " #: standard input:603 msgid "Terms of Service" msgstr "Términos de Servicio" #: standard input:606 msgid "Please accept the Terms of Service" msgstr "Por favor, acepte los Términos de Servicio" #: standard input:620 msgid "Welcome!" msgstr "¡Bienvenido!" #: standard input:621 msgid "Your account has been created, and must be activated within 24 hours." msgstr "Su cuenta ha sido creada y debe activarse dentro de 24 horas." #: standard input:622 msgid "Here's your password:" msgstr "Aquí está la contraseña:" #: misc msgid "Full name" msgstr "Nombre completo" msgid "Email address" msgstr "Correo electrónico" msgid "Confirm" msgstr "Confirmación" msgid "Sign up confirmation" msgstr "Confirmación de registro" msgid "Password reset" msgstr "Nueva contraseña" msgid "Old password" msgstr "Contraseña anterior" msgid "This field is mandatory" msgstr "Este campo es obligatorio" msgid "This account is pending confirmation" msgstr "Esta cuenta está pendiente de confirmación" python-cyclone-1.1/appskel/signup/frontend/locale/es_ES/LC_MESSAGES/modname.mo0000644000175000017500000000714412124336260026141 0ustar lunarlunarÞ•0œC(B)lt…  ¨ ²ÀÝö *@EWm ‡”¥® ³ ÀÍÖå"û 5AZipu}†Ž £±Ð$áy%ŸE¨YîHCe © °  à î ö ! , K e !u — ³ º Ô ,ó  1 G N U g | ˆ š +¸ ä   ( @ J Q X _ k … #• ¹ ,Ï ü  •! · =Å ` "/.# !) - 0% &*$( '+,A confirmation email has been sent to %s with instructions.AccountAccount settingsBootstrap starter templateConfirmDashboardEmail addressEmail address not registeredEmail already registeredForgot password?Full nameHere's the new password:Here's your password:HomeI agree with the Invalid email addressInvalid email or passwordInvalid nameInvalid passwordLanguageNameNew passwordOld passwordPasswordPassword resetPasswords don't matchPlease accept the Terms of ServicePlease try again laterRemember meRequested by IP %s on %sReset passwordSaved!SendSign inSign outSign upSign up confirmationSign up todaySuper awesome marketing speak!Terms of ServiceThis account is pending confirmationThis field is mandatoryUpdateUse this document as a way to quick start any new project.
All you get is this message and a barebones HTML document.Welcome!Your account has been created, and must be activated within 24 hours.Your password has been reset, and must be used to reactivate the account within 24 hours.Project-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2012-12-11 23:20-0500 PO-Revision-Date: 2012-12-12 00:00:00+0500 Last-Translator: Alexandre Fiori Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ha enviado un email de confirmación a %s con instrucciones.CuentaDatos de la contaTemplate inicial de BootstrapConfirmaciónTableroCorreo electrónicoCorreo electrónico no registradaCorreo electrónico registrado¿Olvidó la contraseña?Nombre completoAquí está la nueva contraseña:Aquí está la contraseña:InicioEstoy de acuerdo con los Correo electrónico no válidoCorreo electrónico o contraseña no válidaNombra inválidoContraseña inválidaIdiomaNombreNueva contraseñaContraseña anteriorContraseñaNueva contraseñaLas contraseñas no coincidenPor favor, acepte los Términos de ServicioPor favor inténtelo más tardeRecuerdeSolicitado por IP %s en %sRestablecer contraseñaGuardado!EnviarEntrarCerrarRegistrarseConfirmación de registroInscríbase hoyHablar de marketing impressionante!Términos de ServicioEsta cuenta está pendiente de confirmaciónEste campo es obligatorioActualizarUtilice este documento para cualquier nuevo proyecto como una forma de de inicio rápido.
Solo tiene es este mensaje y un documento básico HTML.¡Bienvenido!Su cuenta ha sido creada y debe activarse dentro de 24 horas.La contraseña se ha restablecido y debe utilizarse para reactivar la cuenta dentro de 24 horas.python-cyclone-1.1/appskel/signup/frontend/locale/pt_BR/0000755000175000017500000000000012124336260022401 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/frontend/locale/pt_BR/LC_MESSAGES/0000755000175000017500000000000012124336260024166 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/frontend/locale/pt_BR/LC_MESSAGES/modname.po0000644000175000017500000001131012124336260026142 0ustar lunarlunar# cyclone signup template. # Copyright (C) 2012 cyclone # This file is distributed under the same license as the cyclone package. # Alexandre Fiori , 2012. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: 0.1\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2012-12-11 23:20-0500\n" "PO-Revision-Date: 2012-12-12 00:00:00+0500\n" "Last-Translator: Alexandre Fiori \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: standard input:36 msgid "Account settings" msgstr "Dados da conta" #: standard input:37 msgid "Name" msgstr "Nome" #: standard input:39 msgid "Password" msgstr "Senha" #: standard input:45 msgid "Invalid name" msgstr "Nome inválido" #: standard input:47 msgid "Invalid password" msgstr "Senha inválida" #: standard input:49 msgid "Passwords don't match" msgstr "As senhas não coincidem" #: standard input:53 msgid "Saved!" msgstr "Gravado!" #: standard input:55 msgid "Update" msgstr "Atualizar" #: standard input:84 msgid "Dashboard" msgstr "Painel" #: standard input:90 msgid "Account" msgstr "Conta" #: standard input:92 msgid "Language" msgstr "Idioma" #: standard input:100 msgid "Sign out" msgstr "Sair" #: standard input:139 msgid "Bootstrap starter template" msgstr "Template inicial do Bootstrap" #: standard input:140 msgid "" "Use this document as a way to quick start any new project.
All you get " "is this message and a barebones HTML document." msgstr "" "Use este documento para iniciar qualquer novo projeto rapidamente.
Tudo " "que há nele é esta mensagem e um documento HTML puro." #: standard input:246 standard input:352 standard input:430 standard input:504 #: standard input:580 standard input:665 msgid "Home" msgstr "Início" #: standard input:247 standard input:353 standard input:431 standard input:505 #: standard input:513 standard input:522 standard input:581 standard input:666 msgid "Sign in" msgstr "Entrar" #: standard input:255 msgid "Super awesome marketing speak!" msgstr "Linguagem de marketing impressionante!" #: standard input:257 msgid "Sign up today" msgstr "Cadastre-se hoje" #: standard input:361 msgid "Reset password" msgstr "Reiniciar a senha" #: standard input:366 standard input:594 msgid "Invalid email address" msgstr "Endereço de email inválido" #: standard input:368 msgid "Email address not registered" msgstr "Endereço de email não registrado" #: standard input:370 standard input:598 msgid "Please try again later" msgstr "Por favor tente novamente mais tarde" #: standard input:374 msgid "Send" msgstr "Enviar" #: standard input:386 msgid "New password" msgstr "Nova senha" #: standard input:387 msgid "" "Your password has been reset, and must be used to reactivate the account " "within 24 hours." msgstr "" "Sua senha foi reiniciada, e deve ser usada para reativar a conta em até " "24 horas." #: standard input:388 msgid "Here's the new password:" msgstr "Aqui está sua nova senha:" #: standard input:391 standard input:625 #, python-format msgid "Requested by IP %s on %s" msgstr "Solicitado por IP %s em %s" #: standard input:440 standard input:675 #, python-format msgid "A confirmation email has been sent to %s with instructions." msgstr "Um email de confirmação foi enviado para %s com instruções." #: standard input:517 msgid "Invalid email or password" msgstr "Endereço de email ou senha inválidos" #: standard input:520 msgid "Remember me" msgstr "Lembre-me" #: standard input:523 msgid "Forgot password?" msgstr "Esqueceu a senha?" #: standard input:589 standard input:608 msgid "Sign up" msgstr "Cadastre-se" #: standard input:596 msgid "Email already registered" msgstr "Email já registrado" #: standard input:603 msgid "I agree with the " msgstr "Eu concordo com os " #: standard input:603 msgid "Terms of Service" msgstr "Termos de Serviço" #: standard input:606 msgid "Please accept the Terms of Service" msgstr "Por favor aceite os Termos de Serviço" #: standard input:620 msgid "Welcome!" msgstr "Bem vindo!" #: standard input:621 msgid "Your account has been created, and must be activated within 24 hours." msgstr "Sua conta foi criada, e deve ser ativada em até 24 horas." #: standard input:622 msgid "Here's your password:" msgstr "Aqui está sua senha:" #: misc msgid "Full name" msgstr "Nome completo" msgid "Email address" msgstr "Endereço de email" msgid "Confirm" msgstr "Confirmação" msgid "Sign up confirmation" msgstr "Confirmação de registro" msgid "Password reset" msgstr "Nova senha" msgid "Old password" msgstr "Senha antiga" msgid "This field is mandatory" msgstr "Este campo é obrigatório" msgid "This account is pending confirmation" msgstr "Esta conta está pendente de confirmação" python-cyclone-1.1/appskel/signup/frontend/locale/pt_BR/LC_MESSAGES/modname.mo0000644000175000017500000000673212124336260026153 0ustar lunarlunarÞ•0œC(B)lt…  ¨ ²ÀÝö *@EWm ‡”¥® ³ ÀÍÖå"û 5AZipu}†Ž £±Ð$áy%ŸE¨YîHFe ¬ ² Á ß í ô " * ? Q _ z  ˜ ¬ &É ð ÿ    & 3 9 D &] $„ © ³ Î à é ð ÷ ü  " &3 Z *m ˜ ³ ƒ½ A :L R‡  "/.# !) - 0% &*$( '+,A confirmation email has been sent to %s with instructions.AccountAccount settingsBootstrap starter templateConfirmDashboardEmail addressEmail address not registeredEmail already registeredForgot password?Full nameHere's the new password:Here's your password:HomeI agree with the Invalid email addressInvalid email or passwordInvalid nameInvalid passwordLanguageNameNew passwordOld passwordPasswordPassword resetPasswords don't matchPlease accept the Terms of ServicePlease try again laterRemember meRequested by IP %s on %sReset passwordSaved!SendSign inSign outSign upSign up confirmationSign up todaySuper awesome marketing speak!Terms of ServiceThis account is pending confirmationThis field is mandatoryUpdateUse this document as a way to quick start any new project.
All you get is this message and a barebones HTML document.Welcome!Your account has been created, and must be activated within 24 hours.Your password has been reset, and must be used to reactivate the account within 24 hours.Project-Id-Version: 0.1 Report-Msgid-Bugs-To: POT-Creation-Date: 2012-12-11 23:20-0500 PO-Revision-Date: 2012-12-12 00:00:00+0500 Last-Translator: Alexandre Fiori Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Um email de confirmação foi enviado para %s com instruções.ContaDados da contaTemplate inicial do BootstrapConfirmaçãoPainelEndereço de emailEndereço de email não registradoEmail já registradoEsqueceu a senha?Nome completoAqui está sua nova senha:Aqui está sua senha:InícioEu concordo com os Endereço de email inválidoEndereço de email ou senha inválidosNome inválidoSenha inválidaIdiomaNomeNova senhaSenha antigaSenhaNova senhaAs senhas não coincidemPor favor aceite os Termos de ServiçoPor favor tente novamente mais tardeLembre-meSolicitado por IP %s em %sReiniciar a senhaGravado!EnviarEntrarSairCadastre-seConfirmação de registroCadastre-se hojeLinguagem de marketing impressionante!Termos de ServiçoEsta conta está pendente de confirmaçãoEste campo é obrigatórioAtualizarUse este documento para iniciar qualquer novo projeto rapidamente.
Tudo que há nele é esta mensagem e um documento HTML puro.Bem vindo!Sua conta foi criada, e deve ser ativada em até 24 horas.Sua senha foi reiniciada, e deve ser usada para reativar a conta em até 24 horas.python-cyclone-1.1/appskel/signup/frontend/template/0000755000175000017500000000000012124336260021747 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/frontend/template/index.html0000644000175000017500000000465012124336260023751 0ustar lunarlunar{% extends "base.html" %} {% block header %} {% end %} {% block page %}

{{_("Super awesome marketing speak!")}}

Cras justo odio, dapibus ac facilisis in, egestas eget quam. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.

{{_("Sign up today")}}

Subheading

Donec id elit non mi porta gravida at eget metus. Maecenas faucibus mollis interdum.

Subheading

Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.

Subheading

Maecenas sed diam eget risus varius blandit sit amet non magna.

Subheading

Donec id elit non mi porta gravida at eget metus. Maecenas faucibus mollis interdum.

Subheading

Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Cras mattis consectetur purus sit amet fermentum.

Subheading

Maecenas sed diam eget risus varius blandit sit amet non magna.


{% end %} python-cyclone-1.1/appskel/signup/frontend/template/passwd.html0000644000175000017500000000444012124336260024140 0ustar lunarlunar{% extends "base.html" %} {% block header %} {% end %} {% block scripts %} {% end %} {% block page %}

{{_("Reset password")}}

{% if fields.err and "email" in fields.err %}
{{_("Invalid email address")}}×
{% elif fields.err and "notfound" in fields.err %}
{{_("Email address not registered")}}×
{% elif fields.err and "pending" in fields.err %}
{{_("This account is pending confirmation")}}×
{% elif fields.err and "send" in fields.err %}
{{_("Please try again later")}}×
{% end %}
{% end %} python-cyclone-1.1/appskel/signup/frontend/template/error_all.html0000644000175000017500000000133112124336260024614 0ustar lunarlunar{% extends "base.html" %} {% block header %} {% end %} {% block page %}

Oooops!

{{"HTTP %s: %s" % (fields["code"], fields["message"])}}

{% if "exception" in fields and handler.settings.debug is True %}

Debug:

{{escape(str(fields["exception"]))}}
{% end %}
{% end %} python-cyclone-1.1/appskel/signup/frontend/template/signup_ok.html0000644000175000017500000000202312124336260024630 0ustar lunarlunar{% extends "base.html" %} {% block header %} {% end %} {% block page %}

Thank you!

{{_("A confirmation email has been sent to %s with instructions.") % email}}

Sign in

{% end %} python-cyclone-1.1/appskel/signup/frontend/template/account.html0000644000175000017500000000502212124336260024270 0ustar lunarlunar{% extends "admin.html" %} {% block header %} {% end %} {% block page %}

{{_("Account settings")}}

{% if fields.err and "invalid_name" in fields.err %}
{{_("Invalid name")}}×
{% end %} {% if fields.err and "old_nomatch" in fields.err %}
{{_("Invalid password")}}×
{% elif fields.err and "old_missing" in fields.err %}
{{_("This field is mandatory")}}×
{% end %} {% if fields.err and "invalid_passwd" in fields.err %}
{{_("Invalid password")}}×
{% elif fields.err and "nomatch" in fields.err %}
{{_("Passwords don't match")}}×
{% elif fields.updated %}
{{_("Saved!")}}×
{% end %}

{% end %} python-cyclone-1.1/appskel/signup/frontend/template/admin.html0000644000175000017500000000335412124336260023732 0ustar lunarlunar{% extends "base.html" %} {% block header %} {% end %} {% block navbar %} {% end %} python-cyclone-1.1/appskel/signup/frontend/template/signup.html0000644000175000017500000000454512124336260024152 0ustar lunarlunar{% extends "base.html" %} {% block header %} {% end %} {% block scripts %} {% end %} {% block page %}

{{_("Sign up")}}

{% if fields.err and "legal" not in fields.err %}
{% if "email" in fields.err %} {{_("Invalid email address")}} {% elif "exists" in fields.err %} {{_("Email already registered")}} {% elif "send" in fields.err %} {{_("Please try again later")}} {% end %} ×
{% end %} {% if fields.err and "legal" in fields.err %}
{{_("Please accept the Terms of Service")}}×
{% end %}
{% end %} python-cyclone-1.1/appskel/signup/frontend/template/signup_email.html0000644000175000017500000000175112124336260025315 0ustar lunarlunar

{{_("Welcome!")}}

{{_("Your account has been created, and must be activated within 24 hours.")}}

{{_("Here's your password:")}} {{passwd}}


{{_("Requested by IP %s on %s") % (ip, date)}}

python-cyclone-1.1/appskel/signup/frontend/template/dashboard.html0000644000175000017500000000053012124336260024562 0ustar lunarlunar{% extends "admin.html" %} {% block dashboard_menu %}active{% end %} {% block page %}

{{_("Bootstrap starter template")}}

{{_("Use this document as a way to quick start any new project.
All you get is this message and a barebones HTML document.")}}


{% end %} python-cyclone-1.1/appskel/signup/frontend/template/passwd_email.html0000644000175000017500000000200312124336260025300 0ustar lunarlunar

{{_("New password")}}

{{_("Your password has been reset, and must be used to reactivate the account within 24 hours.")}}

{{_("Here's the new password:")}} {{passwd}}


{{_("Requested by IP %s on %s") % (ip, date)}}

python-cyclone-1.1/appskel/signup/frontend/template/base.html0000644000175000017500000000161112124336260023546 0ustar lunarlunar {% block title %}$modname{% end %} {% block header %}{% end %} {% block navbar %}{% end %}
{% block page %}{% end %}
{% block scripts %}{% end %} python-cyclone-1.1/appskel/signup/frontend/template/passwd_ok.html0000644000175000017500000000202712124336260024630 0ustar lunarlunar{% extends "base.html" %} {% block header %} {% end %} {% block page %}

Done!

{{_("A confirmation email has been sent to %s with instructions.") % email}}

{{_("Sign in")}}

{% end %} python-cyclone-1.1/appskel/signup/frontend/template/passwd_email_subject.txt0000644000175000017500000000004212124336260026673 0ustar lunarlunar$modname: {{_("Password reset")}} python-cyclone-1.1/appskel/signup/frontend/template/signin.html0000644000175000017500000000413412124336260024126 0ustar lunarlunar{% extends "base.html" %} {% block header %} {% end %} {% block scripts %} {% end %} {% block page %}

{{_("Sign in")}}

{% if fields.err and "auth" in fields.err %}
{{_("Invalid email or password")}}×
{% end %} {{_("Forgot password?")}}
{% end %} python-cyclone-1.1/appskel/signup/frontend/template/signup_email_subject.txt0000644000175000017500000000005012124336260026676 0ustar lunarlunar$modname: {{_("Sign up confirmation")}} python-cyclone-1.1/appskel/signup/frontend/static/0000755000175000017500000000000012124336260021423 5ustar lunarlunarpython-cyclone-1.1/appskel/signup/frontend/static/legal.txt0000644000175000017500000000003412124336260023245 0ustar lunarlunarTerms of Service: whatever. python-cyclone-1.1/appskel/signup/frontend/static/favicon.ico0000644000175000017500000000217612124336260023552 0ustar lunarlunarh(  "ÿ"ÿ"ÿ"ÿ"ÿÛ¼õÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ !ÿ !ÿ"ÿ#ÿ"ÿÛ¼õÛ¼õ"ÿ"ÿ !ÿ !ÿ ÿ"ÿ"ÿ"ÿBCEÿBCEÿBCEÿ !ÿ !ÿ !ÿ#ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !%þBCEÿ"ÿ"ÿ"ÿBCEÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ !ÿ"ÿ"ÿ"ÿ"ÿBCEÿûýþûýþ"ÿ"ÿBCEÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿûýþÛÝÞ."ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿBCEÿ"ÿBCEÿ"ÿÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿÛ¼õ"ÿ"ÿÛ¼õÛ¼õYZ\ÿÛ¼õÛ¼õÛ¼õ"ÿ"ÿ"ÿÛ¼õÛ¼õÛ¼õÛ¼õ"ÿÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿ"ÿÛ¼õÛ¼õ"ÿ"ÿ"ÿ"ÿ"ÿÛ¼õYZ\ÿ"ÿ"ÿ"ÿÛ¼õYZ\ÿ"ÿƒÁ€`ñÿÿÿÿÿÿóŸóžy½ƒ™ÇÃÿçÿpython-cyclone-1.1/appskel/signup/modname.conf0000644000175000017500000000111512124336260020602 0ustar lunarlunar[server] debug = true xheaders = false xsrf_cookies = false cookie_secret = $cookie_secret [frontend] locale_path = frontend/locale static_path = frontend/static template_path = frontend/template [sqlite] enabled = yes database = :memory: [redis] enabled = yes # unixsocket = /tmp/redis.sock host = 127.0.0.1 port = 6379 dbid = 0 poolsize = 10 [mysql] enabled = yes host = 127.0.0.1 port = 3306 username = foo password = bar database = dummy poolsize = 10 debug = yes ping_interval = 3600 [email] enabled = yes host = smtp.gmail.com port = 587 tls = yes username = foo password = bar python-cyclone-1.1/cyclone/0000755000175000017500000000000012124336260015005 5ustar lunarlunarpython-cyclone-1.1/cyclone/appskel_default.zip0000644000175000017500000003560212124336260020702 0ustar lunarlunarPK bŸŠA”‰•$ .gitignoreUT X…ÆPʶ3Qux õ*.swp *.pyc dropin.cache PK YWí@ frontend/UT ©7Pÿ¶3Qux õPK YWí@frontend/locale/UT ©7Pÿ¶3Qux õPK YWí@frontend/locale/es_ES/UT ©7Pÿ¶3Qux õPK YWí@"frontend/locale/es_ES/LC_MESSAGES/UT ©7Pÿ¶3Qux õPKYWí@r ÊþDá,frontend/locale/es_ES/LC_MESSAGES/modname.moUT ©7Pʶ3Qux õËN1†‹—…㎸tQÖ¦ØÑñ!!HÈĵeæ0V†–´.ßÂðMŒkŸÁgñ °qáŸ|ù»øÏ¥ç§|ðNPûÈ) ‡HlôŒ!)#¤Y"ä ýùÄ÷ ú7zi[³·í·V×Ñ…6[!ñ*δº€µ`æ`ÈÀèWˆë&ì Œ•Z…”W}o3mëÛT&ì>O-‹tH½ÁcÄZ„à {BZã¾ÏxÕê_„õKÆëœc a.í¹F‘ ‚³M°'¬c‘ÊfÂiÒfK¡´#µ‘ôz\Øò. ™Uc=½Å•æ"˜þ«¢ßí·wô«Ükiå@áèÕ —s°tç³LHuEãa,¸›ÜYc—+VƒamëDª4¤‘tùúèä*ƾ¢BŠ£ÊD›õ…·×&¿PKYWí@ZÂ]ŽÌ,frontend/locale/es_ES/LC_MESSAGES/modname.poUT ©7Pʶ3Qux õQMoÛ0 ½çW°ÉeE«ÌNÓ!p?°-k VÆN½(í¨µ%ƒ¢Û¤ÿ¦e¿l’ç hÑCO¢ÈÇÇ÷Ȩ­ªœEÁÎU¼¬› IZ_I6ÎŽ#˜»fK¦\3|™Â$ISøQáFZMׯ‘  |m<&t‡WÏdV-£†Öj$à5Fv„Ê(´Aú.×χFªGYb÷Ž΋øl¾—µ4ÕX¹úò¸°ƒÑ1íËËvPûÒhcfÇhxKî‹…|p“A2Nïm¨ÝaãˆÅ2¶‰ŸméEî2èJ·¿s1'ìì‹_’1ëÆ‰d"&SHO³“o"9I’,îðÉø°³ˆNöàéYäýreŸ°Ú÷Ù² Û9ÊúÓ]ËÅòjo<ÿ×0w–ÑÛ&ˆeÜðצ’ÆžZKòÈ-bö%HâÊ*§-3˜­ GÌ`”ç F’c›–³ÓÝ1v·}Æx¤'¤ýyâßhG]±~È6Û±-ž=úƒ=Éß×ëÖª`O†ä?PK YWí@frontend/locale/pt_BR/UT ©7Pÿ¶3Qux õPK YWí@"frontend/locale/pt_BR/LC_MESSAGES/UT ©7Pÿ¶3Qux õPKYWí@òye@ß,frontend/locale/pt_BR/LC_MESSAGES/modname.moUT ©7Pʶ3Qux õËN1†‹—…,‰KemÎÀhÈx‰ˆ@$dâÚ2s+CKÚ—oáû¸ö!|ÏÈ$l\x’/Óüÿééù®}0ªC⌈cbÈvõBœ‚¨s¢Saì½¼ÿ¤ó)éi¥Ì”ý~kàøZ›¹­³xgZ!_ã”[4+4llôÆ <£±R«{ ¿:Á¥6F6• <ä©…H‡¼:~Š kP82£pò¦çûà5¡pÿ2l]×ò<2ÂWÒþák¾ 8߇Â:ˆŒP6N›w2Ü•ä}©ä7³B6÷éBȬëÅeTš‹!B±øWb4õöô^µ«•CEOo—4œÃ»XfBªk¿ cÑÝæní½¯q†z*Ö‰TiÈÛS骬Ÿ«˜ºŠ:+V*m~÷[îšýPKYWí@6ŽÔ(‡Ê,frontend/locale/pt_BR/LC_MESSAGES/modname.poUT ©7Pʶ3Qux õQÁNã0½÷+†ö²\’RP•ÝE,¤JT qÚ‹kORCbGã ´|=vHUíŠ'gÞ¼yofj«*gQ°s•/ë¦B`’ÖW’³ãÁæ®Ù’)× ?æ‡0IÒþT¸‘V­qd(_… ÝáÕÆ3™U˨¡µ x‘¡2 ­G¾Ëõó¡‘êE–ÇýÇ ¿Šøl.ËZšj¬\}q܉ØÁèŠöý};¨}i4 ‡1³c4| ÷ŒŠÅB‹'$ÜdŒÓ¿6Ô±qÄbÛÄU[z‘» ºÒÃ}.æ„}q-³nœH&b2…ô,;=Éi’ô`ñˆ¯ÆEìtz´ßIÏ"ï—ë(û†Õ¾Ï–mØŽÈQÖßîZ.–7{ãéøSÃÜYFdl› –qÃ'M%ý j-É#ÿn¹³±Qr$n¬rÚØ2ƒÙÊpÄ Fxj$i0¶i9;ÛcwÛ7\GzEÚŸ'þvÔ{à—l³Û‚áÍÑ‹?ؓܶVs2¤>PK YWí@frontend/static/UT ©7Pÿ¶3Qux õPKYWí@3ÜÈñá~frontend/static/favicon.icoUT ©7Pʶ3Qux õc``B0È`a`Ò@ RbF0 , Xœ¼Òt|{ÏWdŒM Š^Eó•QÌ‹É!ÔÈÊ+ ˜ãäì ÇpsÀê”QøèXAQõXšYøô øA¢þ÷ß ŒbÿÂ0Híí»÷ô`a€+,ñ‰ W ;Bb €œ¾`[8aCN“èf ƒÈ¨˜ÿ09ô´Œl>¶ôŽn®xDw>5¸ÂäNJÒ ²¤èk>,ü6†ý ÿÿ#ðçù@<¡¾’ao3ÃÌã ‡âÏPK o=ÿr æƒL8 l´Bó`É» ÔS•kÐ.îÏTgÊBØG™s{Çž ÆØŸñ†ÝO“àóºI ôÊ9Y¶7džq§BÇMhßߘ܅ÎÐ ®ämИžþ ±Ý“º|G÷š¨Ý¾2ù{ºáò…®_%O‹“5ÎEh çe*¶k FƒÑ›%ú¯á1ööN!6lœ¯xC6–"[>¨¢€>“iÚŸˆ]o4…â…4«|rFð/zåqYª/þPKûl2A ¨¥™ëfrontend/template/post.htmlUT é±XPʶ3Qux õmÏA Â0Ð}O1›Ò]‹ûØ+(ؽ¤É´ &™Ø ¢„ÞÝ]qûÿ<˜ŸkÀg šQ2¶&y×@½V¹†Ñ‘ºA”3n0‡þ|º HŠ®•ˆý`,ÃC.VŽKk91!çÉ¢ÓÜÚ¸®¢‹n5!C ôA{Ãä1æJA•ŽaZÈC«(L{ç_|wWCœ¾°Ì(ë¶ÿßPK ü}cBmodname/UT û¶3Qÿ¶3Qux õPKYWí@HÏßISmodname/__init__.pyUT ©7Pʶ3Qux õSVHÎOÉÌK·R(-IÓµàRæRÉÉLNÍ+NåâŠO,-ÉÈ/ŠW°UPRÉKÌMU°QIÍMÌ̱SÊ–¥gæçA¥¡<%.PKh}cB#ë©cE modname/config.pyUT äµ3Qʶ3Qux õ¥W]o›0}çWXtÕ@ÊP³Ië)O[÷ØNÓÞº 9p“¸1˜Ù¦MöëwmÀ˜6äc£ª áœsï½vœ ’‰œ•«©õòݧà"xÃY¥‚ `E%¤&Buwjçn¿ˆrÉVß©T ƒ`)EA²]ÆE I­'-ìnñ™þÊ2AK¢ån:ZÖe6! ß1QNˆ¨š´æz~‹:ñ, x!¡¹1—]Ë’vô‚[l3¨†î’[qg7R ùJ« ÙÚ+vieXif¢%ãPÒ¢3“Ud>ÿAŸýç(nq‰šG÷ÿ!šˆE‰Iú<Ϫ iB¥:wñø¦H>ÄAk¬’r"÷a‹z> Z›S ŠÃBÓ6Ôïè„|£\Aìil×è¤:MÆ¡÷(eBl¤XÌ«•kt"?Ä |(¹L›×¼¸Ò_¾[_Ó9nÓZÐ `pªÙŠê5¡ %x­hAôš)bjñVáK ¥Æ’Il8!wVA ¡Ñ¦P‰á&È5cÔ=? Vºdš’F©-qšÆh.I¸IÎφŸN‹ENÉfBžfd d¢M†9™X\ì&ÄEF9 Š g„–9Qç—9ë˜/ç #5QlÊ[#Qˆ‹¹ÔPæ&‰>È/X£|„ìƒ|rçñ}ëWÄoÎp~ª®Ìc?dËÑæµ`£†ËjÁ!w@°Â?e ý†ÐÎÌ2ÒnÁY{¿¦ßÏ¢œjº  æ®Ñ]¬îUØ×GBŽO3m±çx¶„¡eÏèÞeS—l«D¶=ÑW}øƒOvKÞ¿×BP1oÍ8}\áß4‘1™ʰr d8~üpýyD"_°ü „àx5jApÅþÀ ï§W}…‹6À‰¶Øs*l çUØV¥ëMÐVãPÜ”=’Mü©Æ×lo¯À±;ÄhXªÔ³ù¸B‡SpKqT¡_‘çÞKƒ_ø+æ«v~¬ìÃ/äKXéÃvâgøG¹·Õ@AÙ©h±ç4¢%üG#º€GqÏÔùPCj®Æòïøˆ9»ŸùŸû¹·ïú¹-Z{5)n¢‡N¡ûŽÄ£WÿX|c{hö [I»ÚíñØÆÅÊ’K5Ãÿ\'…¤öÌb.ü-À–éhPK`cByn&^Í modname/storage.pyUT ’3Qæ¶3Qux õÅVQoÓ0~ϯ8e ¥R‰U*š4@Bl0ÄxT9ñ¥5síÌvÖ…_ÏÙIË’6,š¸—(öÝ—»ï.ww¹æB-gP¹âÙ«è(z"EŽÊb9SÏ" ±7R8\èk˜Ã•©0Šu©ƒ¼Î¥V˜6:ÞåX:x.ߣÍtkŽÆìýcÒªf E…Ñkpaò•CSaqûiÆ3VŠ®–ðZ ÝV‡cæï*YîtO©¬ÝJ«­ŠÔË(Šrɬ…7̱ŒY¼wB%:û‰¹›4­k ‚úH1„ƒÇýƒ&ôíI8: °k¤ïñp@.ƒEW•I.í”ò£Š?Ð^@ÜHJŽRgc*¨ýÑj5wLwo¼tÂHw~}ßSì&8}¯¤Pøåó9½$þ£ßöÜù‘ò|ÒACJõ¾#ÄmJ¥‘Ä &aye ¥[Ö@ô±L"ŸÁS óé½RšDJÓ#i<îiÿH—è’¸RâÎêü]6 G†…64Iülo£³³L;­Ó°£Ì yñüùË)7R Ü¢³°¤lÃêãnç;êá]­Ðà±¥”ip´xÐŒfš[Qlö ò·E·«*p\oT)ãGì &hä.S¸ÚñF -]SØ ”þºƒpöZuü°>cRf,¿¶¿f,¼å"‘y»°/µ@ÉÖ“¦á¤¦RŸ}ª’Ø¢$¶á$žì¶«áÛð &4…£Ã‹ßÖvÙ6¨]“ÙYL6€!/»¸H ¸0ƒOâ^Y{i×Ç4'–Ï)Ù&ú1é9½OylØÙÀÀk8Ù÷·“Àè7PK.•ŠA™èSâmodname/utils.pyUT (sÆPʶ3Qux õ}RKK1¾çW [Á-hÏRð"(‚àA¼‰„˜|[bÓdÍCí¿7Ón»«çØ™ï5ÙÌHcýjI%w—Wb&ΜÕð BØMb&½Õ.x,´êñ·û…7!„v*%zƦw*ãΙÔ«ó|)¨VÓ4÷p="í‘9ÐF­A©DP(qâʃ™€äÏ3uÊ:2LQž¬ÿTÎZc[5w4ƒŽ¤\!«œ£”m‚ë.È« ónÜŽ\¹DO ~aìëqŠo>Ó¶·1†x’öXW÷tÂý‚jÔ2 1šÑõ~x¼º•p¯¼qˆíäjOø(Hy Z36­ K]b„ϲ¤Jcõ€nÊ÷|5ãÙ‚ •©CX[´ cšùȱÝH›HM6ÿý ”õ¡=2çãÕ°ç®ï‚VÓ\NùÕ?ñx<ÄÒqçä9äÚû°XËØ¹øPKѲŠAˆ)œóåömodname/views.pyUT ú¦ÆPʶ3Qux õÍUakÛ0ýî_!Ô†Úz…1ÀØÖÑA [›}m Š}vDeÉÑÉMÃØßÉNÜ8[6Ƙ1ËïžîÞ;XbR©ó«\vú:8 Ž•L@#,JcKV‰2bÀD”°»ªL"Tou ³ Ȭ)˜[JtÆR;°[#SÈÀv!åÊÍÞ”É×Ç…Iµ( Fg¬Èaƒx/œ˜ „+ù$õ´rRáø–@B§j³ã~ØŠR $¨ƒ H”@du Oëðp‹*Œ.*„åàB•­—üå_c kC.=E™Ìëï5-‚sÔ+ÓǸXáBMç”È~pìjPT1¯Ñ~©|5¥²ºÀqµ^\ fÅ)ÂR )lÉ'³çe&õNû4§X•¾ 6‹nÅ·é¢GBRY .ó !䞘ooìV˜J I“^kÑÕB×þP€åÃΆ\5¥Ï^ؼ*@—†'çÅ~Á£èYÀA½7Ÿ/¥ƒ=B»‡êç-Nb6Z,±uE²€¥¡)ÒvC‰m¥?W`W!§UÒ‚¡³™“„'ƒ¯§ƒâtžD[­ÑÖ¶´~ã^>j·øþ …°Ç¬&˜7U3‰,•(f Ò;{WŸ¸Ž8×ä¬Í›zÑxSRÃ;¡ã/ ظÀÜwQ±…÷…eB’.#6@ÎÞ¢¨Ÿ’”ÏÖ°/&“OçÖ¾:{ÑT`7`éO¾hñH¤^ïnŽ=¯þªÙMQ¿òújEMñ¼®Gß{]G÷ÏŽ6Ëp÷°üžóuùÿ¹ó>“ÍëíÙ=ÝÑÁMÐÔ×o‚PKÝD+B¸Ò˜ˆ modname/web.pyUT ñðPʶ3Qux õSÁnÛ0 ½ë+§§Ëìk`‡;¬@; Ë±V¦²¤Ir›ìë+ËŽk§:^l=ò‰OOÔŠKS‘n¶¼ õ×¶bWŠ$jŒQk \ž¤2 e$(¼D_ð‰±Ú™–_µ¦ÒÐ"+ž _üÇ)itMÍ2Wø`4SÍðïéHš1&xÏ¿[õA £ó™„b†¯·ŒÇ¨°æB¦ DîQÕ›±­¨IáXÔÇt¥Ðyþ?N`¹ËÊlÑUÜê ?Þzóޤ@7e^|YìtÑs<´VaÙžüÕÓÎ.¡÷§ÝÃݧT‡ùKêïü”{RÀÈScϾçîÙôÛm|-,8bXäs¯ß+~/„@Ñ_äÃ@ù)I5ÏLX‡Œ“N[o²—ã?P‰CÀcÁö*MO î—üwdçÍGÙaàåÕ8¨½V25ŸªS\x MÂæ…6¤EçT¶–f%táP&0»,)ƒ^‚ÅTû+Úñv%?Šââ)œ‡~ï¯=¯PK°}cBt  >J modname.confUT l¶3Qʶ3Qux õ}‘ÝnÂ0 …ïýHÛu)«F7¤= B•Û¸4"‰³8etO¿„˜zåÏö9ÇÍZ(ì)l@Q;ng³F‚Ã@¨(Hª{4’€„¾é˜wš~á©n„º@1Ñç?`Ýv‘œÚ€á 5ã×Ï|~ £îîº' ‘¬7ï·/ä$ŸFGÚ9l ©45‘€Âˆ- ¥reÉr˜Vi6Òr=êžf£Óánw¼d­ŸçŠÌ``ÉxñReúà9d°¬êwP­Î*e‚lDg»E™Œì”bÝ=Rªªr cz ‡6 ôÌàQä‹CÞl1\_£Fk§¿ËæÃ½vÛF§¿öh²ú²ÌÈ¢~HlôÅ6÷‹Ží%Õë[ ÑÈYõÿx?PK¢SŸAîˆägmŸ README.mdUT ?¯áPʶ3Qux õVýkÜFý]Å€ IÊIGûS1u Ÿ­!¦©B! ¾=itÚÜJ+vWÖ©ôï›]é|ÎÔrwšyófÞ[Q9•Ævœo•çŠzg?s²Œð÷¡Ñžð/4LÞ®d*mÅdk:Ÿï:Õr >—OôË9·J›çY–Ñ‹­+橵aj”§-sG¥cPO Á¶*èR3ÑvZàPmÝã"EvP¾ Jw Rm±£îvY–Ófãƒr¡ðÍfsA^·=êù†!_:Ý –bH<\ñ=Û·ÜòìîÙÅç­­b5Tª%‘ü¯wƒFÛ¥&š¤yû­£k9öð,RjôÜU1HÐuLèWpAˆ'ÕUd,è`ŸšŠØ}àŒn†®CYö O{–s¨}2À0j*Ê»ã òôõß{ˆù,d¡ÛâEߌ1Òô1ü”£òçÁrCGÅz™R–0aÄÕPÊ©Ç0–2Ç—¿<7v'œ]®ï•[ãËzY’_¾ŠîuõúÑxòeô÷:öQ6\îŠ×i¹Ì£¨"£_Í4A’´uR·ã1ëU¹W;è$Mþµ J4MX¸Dï÷ºÂ’mm0‘úÞB²Ò·¼Óc7€ K†(=e¼]âeí&ºyýòzùh!^QÅf'ÈU5c×;í¹PÕVõz³É@ð`xEFï™®'ÔZß[vŽñ­>DµwßRßW…ÈuïiÔ¡A‡œ/g¸Ê<YpÒßjnNÚŒõçŠÑØ$ÌXU Ý0¤d3 tL&¦Ø >U,úi³)²·Ì†jLJeÛ¨6ä…ö:rÜÚ{¤Ê&V\E»D¼Ó ®Bæ;˜ª{0gVû/lG +ð¨ÚîI…SPØ'ë ¨V46%|„U`Øy -ýe@Ñ¥-;ˆ‘•ÕÊ`Óªe§®d¢]„)æ^Ö 4ß])WΤñ!`JFcUÝ9®ÇÁqœ™G»p¨€Eö&¦D¹kéËcˆ²å¸6@ZZ®‡„ó¾'ê”tUÇðÊ‚±Uü8ÊÇl ޶&Nd)3ËÇÛ:Œè¶Hn4çlß+‡ ½N>,£ûs;ta ?ÌF^¡ô<{™êCŽ KZš³¥Zת¤ßoé¯]=s »ì$Jñ¿Ù–_:?=mBè/ÖëöPšb¾†-ö¾]7ÀNŒÏŸA©me°#Õ$s åüJ\wF#á_A9>@ÓûÇwܱì¤1³YŒwVœão®ßEÞO¡1ËÛÁ#jW™ÿ +07:Þ…ËÍ·þ¡hBkŽŒïýDÿRŸ2/Æš®ÆZÒÓà ˜r¹T·0¢ËMž£J› ¨Ë!ÔùÏøeÏÓh]uywñãê'Ê«c½èÝÉz]¦·+Ñq/æ“ÔŠÒ²IXÕÃSaqr AË*í "릹ۧ}¸{y#Òä /@|PñåD”ø,ÆÜëÓrñ§vD”÷L¥üë˜nýîÕÝõ›ÛÛ¿¾¹]§x¿«Ûpš†rû?´6Rʼnø}‚x"Ê4ñ-Þ5è¶WöMš<ÌØ ìEˆqÊ‹RVÙ²3{Gy·ÒL·Œ·À0Ûïàœ¸Q™ùøˆ0­ÇV»Xþçõ§ÕàfÅfñ½R\[x ¦€"FX°[‘NjéX¦!7'$ÍÉ-Z5QZVÎ^½¦»„)úéÉå‚©ä&¶­€.Ò=+HË›XNg<ÊùDɵ¶NÖsijGþþPK Ît6Bscripts/UT ´êþPÿ¶3Qux õPKYWí@¶Ç梄­scripts/cookie_secret.pyUT ©7Pʶ3Qux õeÁ ƒ0Dïû[ÓƒRª‘"ø-K¢k»P×`Á¿o¤ôÔ9 Ì 1—&…­q¢ ëŽþˆ¯UÁà¸N¢ÏSœï0p}ËÈ@¿n ܵ¿”’Lyš‘HíÂD8 X-V”¨è³ü&Ë/X»®eÍ/\žl}Z[Vµ;"¼áYUðPKL‡*B€®ÂøÆscripts/debian-init.dUT €9ïPʶ3Qux õ•Tao›0ýî_q#h[+M¶nS*6e IÑÚ€H:iš¦Ê'±D1Ó¤jûßw˜$ÔVª#%¾»çwwïìtÞY×<±ò%!N~:cwîÄá×È#ð3qÇ#–÷a·ô%ô†a4`·ÏXdL%ͤÂè:ãýH«ÓÛÐÍiËÆ!èÁ'ø Ç{Áí18‚.|ÁÐt)2i Yf<•\$}P9PÈYvÇC"¹dÞ‡±H¬Øµ ±LQ7NVk$Ä5ÍTïÎdØèœøƒÙ™myfÅ"¤±•£Ný†­Ìʹ‹ÔÜáÀ¹ð&öÖ¶äŠç2"dê¿ÝSçjè¶•R¹´¤°v¢n£“Á…cï¼Äÿ3;ó&ª(]o0ôu½¶NQ"hxˆï3û.rîNgÎÄÖº½¯æ~º9õ&#w¼ÏhÕVYƒŠdN|w8rÏÛº£™•I”òˆœ{ã‹Eƒ.2ð}ÌöÐô?™8$s¦1i9B.1n¯V+#¢’’qà]úµYÉzåù³©­ÞªÆw †åÌyÌ0Õ¦pôaúoSèî’ ¾T Á¨U‰Ä9±íJ1ôÐ4E‹#ÄM%›FŸÃ_xÆU]ð祿€ `áR€æôá”&$°5 ÉvhS+qk.¡Kæ¼æ‹`o$/‘ñ…Rd÷ –W °öþþéV޼|2W›÷òñ¶¬FVOŠ' hÏ4¡dRÇ_'ŽƒÝà{3¦·`”úì´nÈa»½Y™ GðU³ºŽú³[|áuÛ.iœ1ÝÞ· ÒT(c²ÈJTÙK‹¤w|ü M(Š8*ÅQ¥?ËR#¼ y*%é« ‰4}“B¿P¡ºG{:½EŠDÈWeh5Òœ¦ë] x²-é@Ü¿òœœ(„H·€ºù:ž±=ŽäÒÃF —9]°>XL†O¸4£Ö<(šÇ’ýq“ïIƒïï{Ku…7ä,§!!ÊwDþPKN‡*BñøicÄ scripts/debian-multicore-init.dUT „9ïPʶ3Qux õÍUmoÛ6þÎ_q•…ví ÉΚ u ž­¤ÂRÙC$ŒDÇBR©8Yšÿ¾#eKrÞ€}› Ø<Þñ¹ãsÏI½7ÞEƽ *W„ôz=ø#8 #£p?‡SÒƒY)n²”É!4ûJ¤œ^1ôÆìºÊJ–:sEKebl›æù®Kõé­k–´ÊUçìÁ/ðöwœÛcЇüŠ®ùJ”Ê™0™”Y¡2Á‡` $P¬¼É‚ƒZ1Hî’\pkva\¬4ГõçPˆ Zš»Ѥss2-¾ø^%K/ Í=‰L ;¶1ëÍÆÓÆà‚LFÁ×iäomO­3©RBæAüW8Î&aì{U+O ¯!uëF_¿Ù%³¿_¦‘)ʶ;CÛn]„ÝHtvHÍ£hÌýWñâl6þ§Oý9ç‹ ò­ÁÞon¿‹Œ§Ñax´›Ãk-]•›¾$£Ù £î»ŽévGE‘g Õ,r‚~½^;)U”ÅÓ“YkbKÆ+–|Q)Ó5©»y¶må²â‰F¥(A ¿„¤’J\eÿt¦•ad•¬€JlµR¿„ñìèr™ñLݹ„dKøoÀ¹E Ö]Ó‘–¬XAOã!Œ)§€Ý²¤R¬‰v-w›)eÖ⥰ÃÓK œƒD‰òRÁd¥0Ü=ý(Ç?½‡û-ªÃÁ‚µòõ]5ÇuA#iÎ8dÎ%»Æù±íFçèNÑò7R°ío­0Nëýpr¾wCK¯¬øc è0Ö-²Ô„OÚð\\¾Ž^“z6-æ¾åTX›– 8ú.F"à8½Ìr†ÅmjÁ=<¿ÙÛ$lÆãµîë<:éeíZá¸C‹M-8 .j™[¦œÎT úÿçÁ)1.Ó5W bÙjΙ_ƒ£¡š²:¢rœfmng0t½¿›•‘mc¹¦%­ZL˜Q ÍKFÓ;@Î96×Ú8K¦ª²ŽÔÚx¶·¿ÿ,\"ª<Õ‚3y ®LíŒM›oØ>èüŽCö(™Ì+P·µõ$Ø)4å(E|*´ÌÃÛÏà¥ìÆã¾6ÕhirÖL6,ò ÇA¯Nƒ(ŠÿÉ4<¯§äô'Ê)†AGTÿ]7\¨×5³áõ)± • ,ÛXÈÙÔø¾-wKºÙ980¢Ø´Miý%ëbú]Ò0òü±ÆdñZò5l}‰I/Ù<¦O‹ÖMµî ÌñǦ® >¿Ý«QêÇíœIšböúä_PKYWí@4¶ã~scripts/localefix.pyUT ©7Pʶ3Qux õUÛŽƒ †ïy Лӭ¦w OÒv‰Å¡K¢`l×·_ «fçjò¾É»zòXß­Á>é8‡ogIA•ëŒ}4t úx&ùèë3ŒEX6?û¨j*¥m’ A™”Ck¬”¬!4NÀù½¤Ñ¦‡¥"u«ÏËé¶Ú­÷±kjqG¶µ»Øs#X¾$ÊìÁ‚14ÿs‰ïC|…d½7$¦Ã•rà ÷‚ñË»J¶³´Ãœ¥ÆFÎÆÌšX0•Ÿî™¸^Oì3‹åšü;ì¦P½Ðà›­»JõÎ/É/PK™ŠA,SJ(›Ãstart.shUT •yÆPʶ3Qux õE=‚0E÷þŠg`2iatq`sRúñˆ/)¯M[ƒü{Ïoνա1ÄÑù)*Ȉm¢XrãÐfILE9˜B‚˜‚{ÙBÁaôa‘Kß1¤ý}¸Ü®}7\Σ£ÄzF¨ëve¡\H»ZAF8m€ôЪ=ððC&¨çྺZШ.FOVï·Òþ;xÚöâPK bŸŠA”‰•$ ¤.gitignoreUTX…ÆPux õPK YWí@ íA]frontend/UT©7Pux õPK YWí@íA frontend/locale/UT©7Pux õPK YWí@íAêfrontend/locale/es_ES/UT©7Pux õPK YWí@"íA:frontend/locale/es_ES/LC_MESSAGES/UT©7Pux õPKYWí@r ÊþDá,¤–frontend/locale/es_ES/LC_MESSAGES/modname.moUT©7Pux õPKYWí@ZÂ]ŽÌ,¤@frontend/locale/es_ES/LC_MESSAGES/modname.poUT©7Pux õPK YWí@íA4frontend/locale/pt_BR/UT©7Pux õPK YWí@"íA„frontend/locale/pt_BR/LC_MESSAGES/UT©7Pux õPKYWí@òye@ß,¤àfrontend/locale/pt_BR/LC_MESSAGES/modname.moUT©7Pux õPKYWí@6ŽÔ(‡Ê,¤†frontend/locale/pt_BR/LC_MESSAGES/modname.poUT©7Pux õPK YWí@íAs frontend/static/UT©7Pux õPKYWí@3ÜÈñá~¤½ frontend/static/favicon.icoUT©7Pux õPK o=J ¤Lmodname.confUTl¶3Qux õPK¢SŸAîˆägmŸ ¤¯README.mdUT?¯áPux õPK Ît6BíA_%scripts/UT´êþPux õPKYWí@¶Ç梄­¤¡%scripts/cookie_secret.pyUT©7Pux õPKL‡*B€®ÂøÆ¤w&scripts/debian-init.dUT€9ïPux õPKN‡*BñøicÄ ¤¾)scripts/debian-multicore-init.dUT„9ïPux õPKYWí@4¶ã~¤Û-scripts/localefix.pyUT©7Pux õPK™ŠA,SJ(›Ãí /start.shUT•yÆPux õPK ƒ é/python-cyclone-1.1/cyclone/appskel_foreman.zip0000644000175000017500000003014112124336260020676 0ustar lunarlunarPK Âm‰AÃ[™l .envUT kÜÄPʶ3Qux õPYTHONPATH=. PK Âm‰A”‰•$ .gitignoreUT kÜÄPʶ3Qux õ*.swp *.pyc dropin.cache PK Âm‰A frontend/UT lÜÄPÿ¶3Qux õPK Âm‰Afrontend/locale/UT lÜÄPÿ¶3Qux õPK Âm‰Afrontend/locale/es_ES/UT kÜÄPÿ¶3Qux õPK Âm‰A"frontend/locale/es_ES/LC_MESSAGES/UT lÜÄPÿ¶3Qux õPKÂm‰Ar ÊþDá,frontend/locale/es_ES/LC_MESSAGES/modname.moUT lÜÄPʶ3Qux õËN1†‹—…㎸tQÖ¦ØÑñ!!HÈĵeæ0V†–´.ßÂðMŒkŸÁgñ °qáŸ|ù»øÏ¥ç§|ðNPûÈ) ‡HlôŒ!)#¤Y"ä ýùÄ÷ ú7zi[³·í·V×Ñ…6[!ñ*δº€µ`æ`ÈÀèWˆë&ì Œ•Z…”W}o3mëÛT&ì>O-‹tH½ÁcÄZ„à {BZã¾ÏxÕê_„õKÆëœc a.í¹F‘ ‚³M°'¬c‘ÊfÂiÒfK¡´#µ‘ôz\Øò. ™Uc=½Å•æ"˜þ«¢ßí·wô«Ükiå@áèÕ —s°tç³LHuEãa,¸›ÜYc—+VƒamëDª4¤‘tùúèä*ƾ¢BŠ£ÊD›õ…·×&¿PKÂm‰AZÂ]ŽÌ,frontend/locale/es_ES/LC_MESSAGES/modname.poUT lÜÄPʶ3Qux õQMoÛ0 ½çW°ÉeE«ÌNÓ!p?°-k VÆN½(í¨µ%ƒ¢Û¤ÿ¦e¿l’ç hÑCO¢ÈÇÇ÷Ȩ­ªœEÁÎU¼¬› IZ_I6ÎŽ#˜»fK¦\3|™Â$ISøQáFZMׯ‘  |m<&t‡WÏdV-£†Öj$à5Fv„Ê(´Aú.×χFªGYb÷Ž΋øl¾—µ4ÕX¹úò¸°ƒÑ1íËËvPûÒhcfÇhxKî‹…|p“A2Nïm¨ÝaãˆÅ2¶‰ŸméEî2èJ·¿s1'ìì‹_’1ëÆ‰d"&SHO³“o"9I’,îðÉø°³ˆNöàéYäýreŸ°Ú÷Ù² Û9ÊúÓ]ËÅòjo<ÿ×0w–ÑÛ&ˆeÜðצ’ÆžZKòÈ-bö%HâÊ*§-3˜­ GÌ`”ç F’c›–³ÓÝ1v·}Æx¤'¤ýyâßhG]±~È6Û±-ž=úƒ=Éß×ëÖª`O†ä?PK Âm‰Afrontend/locale/pt_BR/UT lÜÄPÿ¶3Qux õPK Âm‰A"frontend/locale/pt_BR/LC_MESSAGES/UT lÜÄPÿ¶3Qux õPKÂm‰Aòye@ß,frontend/locale/pt_BR/LC_MESSAGES/modname.moUT lÜÄPʶ3Qux õËN1†‹—…,‰KemÎÀhÈx‰ˆ@$dâÚ2s+CKÚ—oáû¸ö!|ÏÈ$l\x’/Óüÿééù®}0ªC⌈cbÈvõBœ‚¨s¢Saì½¼ÿ¤ó)éi¥Ì”ý~kàøZ›¹­³xgZ!_ã”[4+4llôÆ <£±R«{ ¿:Á¥6F6• <ä©…H‡¼:~Š kP82£pò¦çûà5¡pÿ2l]×ò<2ÂWÒþák¾ 8߇Â:ˆŒP6N›w2Ü•ä}©ä7³B6÷éBȬëÅeTš‹!B±øWb4õöô^µ«•CEOo—4œÃ»XfBªk¿ cÑÝæní½¯q†z*Ö‰TiÈÛS骬Ÿ«˜ºŠ:+V*m~÷[îšýPKÂm‰A6ŽÔ(‡Ê,frontend/locale/pt_BR/LC_MESSAGES/modname.poUT lÜÄPʶ3Qux õQÁNã0½÷+†ö²\’RP•ÝE,¤JT qÚ‹kORCbGã ´|=vHUíŠ'gÞ¼yofj«*gQ°s•/ë¦B`’ÖW’³ãÁæ®Ù’)× ?æ‡0IÒþT¸‘V­qd(_… ÝáÕÆ3™U˨¡µ x‘¡2 ­G¾Ëõó¡‘êE–ÇýÇ ¿Šøl.ËZšj¬\}q܉ØÁèŠöý};¨}i4 ‡1³c4| ÷ŒŠÅB‹'$ÜdŒÓ¿6Ô±qÄbÛÄU[z‘» ºÒÃ}.æ„}q-³nœH&b2…ô,;=Éi’ô`ñˆ¯ÆEìtz´ßIÏ"ï—ë(û†Õ¾Ï–mØŽÈQÖßîZ.–7{ãéøSÃÜYFdl› –qÃ'M%ý j-É#ÿn¹³±Qr$n¬rÚØ2ƒÙÊpÄ Fxj$i0¶i9;ÛcwÛ7\GzEÚŸ'þvÔ{à—l³Û‚áÍÑ‹?ؓܶVs2¤>PK Âm‰Afrontend/static/UT lÜÄPÿ¶3Qux õPKÂm‰A3ÜÈñá~frontend/static/favicon.icoUT lÜÄPʶ3Qux õc``B0È`a`Ò@ RbF0 , Xœ¼Òt|{ÏWdŒM Š^Eó•QÌ‹É!ÔÈÊ+ ˜ãäì ÇpsÀê”QøèXAQõXšYøô øA¢þ÷ß ŒbÿÂ0Híí»÷ô`a€+,ñ‰ W ;Bb €œ¾`[8aCN“èf ƒÈ¨˜ÿ09ô´Œl>¶ôŽn®xDw>5¸ÂäNJÒ ²¤èk>,ü6†ý ÿÿ#ðçù@<¡¾’ao3ÃÌã ‡âÏPK Âm‰Afrontend/template/UT lÜÄPÿ¶3Qux õPKÂm‰Aë™:ךfrontend/template/base.htmlUT lÜÄPʶ3Qux õ­RËnÜ0 ¼ç+A`e¥—¢ØÚû+,q×DdIho Ãÿ^ù‘¶·^z‘†"9|ŒêG O¡ãÞ]êõ§ý­èÅå îPÛØ#k0N¹_åq¸˜Øáežß_„™Œ áŽ-dL#&ñº,µÚCþbòºÇFŒ„÷ 0Á3úÂ|'Ë]cq$ƒr3N@ž˜´“Ùh‡Í·ê­TÞ¹ùè^Ñ1dzR|'fLÕ¸ÚÊ„^µ!p椣ҹtŸ•ÉùÏcU, ]#2Os‡È_£ý~™0Çà3øïR»¯++1•­X5jõú†ª¼ˆ£Ÿy†ÌšÉ¼ɽ<_õ¸†Wåx~…eù¢Ý í  v‚9jkÉß$‡x†ïoñóç²Çª#x•^íÚ¯pÍ:Ø,`\³«bš|Qøð=J¹) `´“P3B?8¦èZÌ‘a†ˆ ûèJÌ–†ŸgÈd±Õé=úáײCL›WʽÊütðIJxZŠÞpLP¼l­€úÝà‘\«}2ÚöáPKÂm‰A'.*Yùfrontend/template/index.htmlUT lÜÄPʶ3Qux õ}¿NÃ0‡÷<Åa),f•:°!sd·n\Õÿ”»( +/ÄÆ3ôňº!¶Óý¾³>ÿr æƒL8 l´Bó`É» ÔS•kÐ.îÏTgÊBØG™s{Çž ÆØŸñ†ÝO“àóºI ôÊ9Y¶7džq§BÇMhßߘ܅ÎÐ ®ämИžþ ±Ý“º|G÷š¨Ý¾2ù{ºáò…®_%O‹“5ÎEh çe*¶k FƒÑ›%ú¯á1ööN!6lœ¯xC6–"[>¨¢€>“iÚŸˆ]o4…â…4«|rFð/zåqYª/þPKÂm‰A ¨¥™ëfrontend/template/post.htmlUT lÜÄPʶ3Qux õmÏA Â0Ð}O1›Ò]‹ûØ+(ؽ¤É´ &™Ø ¢„ÞÝ]qûÿ<˜ŸkÀg šQ2¶&y×@½V¹†Ñ‘ºA”3n0‡þ|º HŠ®•ˆý`,ÃC.VŽKk91!çÉ¢ÓÜÚ¸®¢‹n5!C ôA{Ãä1æJA•ŽaZÈC«(L{ç_|wWCœ¾°Ì(ë¶ÿßPK 铊Amodname/UT ÆpÆPÿ¶3Qux õPKÂm‰AHÏßISmodname/__init__.pyUT lÜÄPʶ3Qux õSVHÎOÉÌK·R(-IÓµàRæRÉÉLNÍ+NåâŠO,-ÉÈ/ŠW°UPRÉKÌMU°QIÍMÌ̱SÊ–¥gæçA¥¡<%.PKÂm‰A—ããïÀz modname/config.pyUT lÜÄPʶ3Qux õVMÓ0½çWXY¤&Rˆà†*õâˆ+B‘[ïºv°í.ˆÿÎØùn›6"—4ñ›7oÞŒ>J3¡ö[Ò8þöCô½‘¢e!ŠÄ±ÖÆmû_µâbÿ &âFIõZI­ oœ¤C}-¡rŸDå¢(bÀÉË\ÂUeÄâŠÐ*#ºnï¸Név_%ÝF/g^Ûþ2ࣈN΂ӂ— jwÐñv j/¹¨‚ü„ Šûtߓݬ´ü;=MŸ“6ÓI¸¦52j ¯ÇäÈ• Œ× ¯Û(^çh‘$ ÏœC¿-æü³1ô´Ùú¨¿íâ9A‰ó f@ÎÂ~Ä ÊfÿÄøà«O‰÷Rk ½‰ÛèµÈŒ|¦ÒBzFór@`ì*¦¼@Viý$ À¡ý±#K&$sÐ…kxÑBVjšdC n]öÎjä%µÑ~X7–ÐÒjÙ8ÀYq‡€0Z;Ô¡mî_åð÷¤~ÔB L?IQøÙ(Šæyœ¶Ub&B2I%£ä)#Ï[2còÙ²YÑY€¥ƒ^©+*!#޵¤¨”*F¬£NTócÚ ›±‚ÑÝ6¶ð+ÁÜNQã.VóŽNAçíióÜ!˜‚Î zÕw(æ°¡vûK ¬Ø6µ?bÂKÁ—F$`=nÒR¦vܨ“ºè_eã –0êhI-ìÆîèWMÜí+îÙûAT¹V¤?1{{ 0a×¹ kÍ àEof´u£ICÿ:Nçfy‘=T¨)Ú¯œ£Y)Øu´_¹äÖÒŠß°Äß®ÞóûJÝS»¯ØuvèZ»øì²¬²{@_³»ÁÖe»ùQ4õ€‹4ÔÚ“6l1°\4¸ßbKã.Yèõ4+œ%í;ž‘÷ïÎûïãîNßæÑÛcs¥ãØtL†¯ú?PKÂm‰A>Šû#4modname/main.pyUT lÜÄPʶ3Qux õ­RMkÃ0 ½ûWˆœlȹBc×ÁÊèm 㵊'Hä ¸íúïg§_+ FGuFz<==™ú!H„-~(Ú?ÇÝXBU+¡‡¸¥1âªvñ30 ]ð—mâˆÂ€¶(%ºe ¢àÆ¡T½#ÖËÀ-yÛR‡f6 Jâª1:‰ÏÁ{b¯ÓB©° ëh®”“†Ž–.R2 ÉNUçÊ…5á'šda…¼! \yŒº˜¿¼.ŠîSü¥c¢9˜WuÙb^<ÍurYgrSþÔd.вf}õš¿¿e(jÁZv=Z M…µùxÖ³æ”H;äé¼NüÆÀÔ³“öéw{oõûÞ&ìFƒ¤Ê"A.!Ç·…Fã¹ ¡˜H>þ2Á;:âÔ4Ï4‡‘´> ˜RXë>#ŒwÍ„Åü¬Ë§õGŠQ*GðwÁþ¸NñÛb.ô§ỎoÒÛ†ÔÆ=ßp¿£µnC¬fôá2¼®î£Éx0Ñp¿¹á+ž+rYF‹ÖNLÛµ­ÌíVtÍ&ê¯ùHSÿÌ)63™ ÂÈhh›ÒÑ£ïÓíWŽWÚFjsò¿Í @íÐÞú+s¿®F<Ïý¬ ؆” &þOÆK¼%tnÞÈk®‹ˆõ‚mQþPKÂm‰AÂõ`$ Ùmodname/views.pyUT lÜÄPʶ3Qux õ”ÁŠÛ0†ïzŠÁY½¤îµré¡ìBO¥·RŒ"mQYòJr½¡ôÝ;rÇɦ°ºÿùôÏhä H§´mv0ÄúÃ'¶aFK´Ó]ï|y”ÆY,1HÑãmÔ8)Ì›èˆÆjï:ˆ£U©mDo1¬TX£¿–ôÇØ:{×̈‡Î)+:,‡¨M8ÿ,> «Ìs_ö»Þˆˆ_4cÒˆàÙ*|ÓóªØ1 Eî Á˜4õJ+½–)×ç™Nˆ²É¶Ð¢1nÏGçâ[#×áž„t– aºآ×GMÕŽn0 ¸RÔ“UØÃ>aù0ŸÉ)vbÿ]e®ýñ‹?Ê{|°{ÒGBï92mƘú{5Ò;e­ðyÈæîÿI+Ø¥'jaô/Bß÷0%‡sµY1£=Î[4Ÿªµ Û›'ÝN5-¢âI&â%òè… ¦ë{Èvtœ";=ìÓýµ¶üä =$o´§x,;>*Oi‡AŠ»úÏd†,Í->[Œ~r›Í(œ“?PKÂm‰AšÉŸëá¶ modname.confUT lÜÄPʶ3Qux õ‘ÁNÃ0 †ï~Š8CK¥MLâI¦ªr—F$ug§@yz•‰Á.('þýÿNrR’7’<¹åe÷¼Ë²|Œ„žD­0ª•¡ë™_ýÀ­î”z¡lôî8 ÂS¦É·¹ÇHÝŒy,ãßüaàsèoº†LiŽ˜o§/ KÒs ™Z  ]$oª•ËÄO£;¦V9*™ª,+˜jò¦W^WªëöTîgèö$H‘Ýx„¼ÒC„Ô˜®3;=l.³,§õúΚ à×ëKr*[Ï It1Âë ׊•îG€r-w¹ÊêÑ“7$&¡æGîÌØóàɱ}d+>Î{S $Ài‚£ð_o&‹DLŒ%øƒ‹—ߺº מŸ ¯bÐX¤ÈC½Li€qèäpÁš“òuœ±‹I v'wbY'ÇÍ4'å¤gt? èʲwœ[ð¿>rª¹ážÀ³GYf弪¿Qú™d!.z‡M=UB>jØŒ¢ÖxxL‘>HCPo"å™X¦T\‚€æšëBn_·\m…è.#õ’MËß«ÊJ&5ŽÅWH•Oý‘ å´)(ÚhO–G#ÇÂô ýɪk*NN*Ó÷ø—÷tÑhëüÅáijÄØ§œÒ ÀÚµ8Ê`¦üËïãäÚÙQ¯œ‡Æ"ìJX™]àá– Jö0ZÌQÕ储W±[¯'‡fÔ _©kGjðœ£MCÓ¢Gø RÔAMÔ¢z8©ÒmŽqKÊ0ð.UµUôoá;åUÐD9÷:’Ô5t]Nºó¹†’¦q46ެÕoŸ´U½™=ÈHP3³½¥Ý¿{{;Ÿ-†JPÃzíwÕ H­v\¨ºT£^¯3´åÔñ‚: ¦n÷ˆI}gœßXƯ"ðB£¦ÎKZ_Ì=:Úiß"CÎZ®3‡:€:w”ß"%Ò”ø)¢ Ü`ÖUºCGÊlJ,ÐÁYÖÃñŒ‰‹q¿^Ùf´²e–bA—¡ñá" íÞ<ÂÕTµq!8N³„½Õ n|æZ3uõpIAJÛÿLº=(N´Ù†¶:5yÝ9Á´€È5÷*†غP"›³Ë€aˆ7° M§UÕ³¢nB=$ùÎâÂ$LlÞ ëõ&ôÁ“G: ¡Z™N(.®ƒa©¦P•2v¸D¸Û• ã(4d«@Y”ֳäöyx%Ý4b^𵈭¾f%*;©Ç&53ß!Û4¢’¯€í{áPŸw\ê0 Q¸ßËið¹=Èì1FkD@7§±¦FŸÃÃa¾'o1Ö­ªè×ý± ›‹¹n6Ø 1ÿòÑôüÖòîÏ­÷ãårÙ?U]QÔN%Tß/[@»×/Ñ›1mÕA!õ>Ôæû ²ÓÐó¯  éí)À ÈH¬ ‹²$¥Ž?ß~îö¾1ó;Ë µ‹ ŽYÁh£ÃòWíòUÑú¾£ƒz_A¹ôÑó¼wã.nôS|ú”cçy§†Í„]%4yŽ(}@]M¾ÉÂÉ–÷;c뫇Ë?R^âÉ Žäq˜1£ Û±ç…W„J‚@0ó­Á€ "´¬Ö]dì>eûbôoïCëMac«ò“’·¡Ð‰qÍ<êãprÔoáˆòñ™©è)î–Ÿ®n߯Vo~y¿ZF{·izì†ró?..ôF(@Ç…æwâQSÆŠ—xé¡Õ¨…(•ÇèŸ6»ÐˆRå¹SÙ,ÈÄÞaŒ\cVi¦c;ú4|'kÃ4ªâ#'Õ:´óÀ߯$OpZO6ul&ûVÞ…š0:b¶ü‚tì–ðnƒ¹‰½‰–æ8-zµ§(VΞßõ"¦‡ˆIæéÑ+쌮Â6}]Ä-¤Ã+aN_ÒŒ<´óQ'7ÚXý´L/O¦û¿PK Âm‰Až¥ÎŸ""requirements.txtUT lÜÄPʶ3Qux õTwisted==12.2.0 cyclone==1.0-rc13 PK G ŠAscripts/UT †ÆPÿ¶3Qux õPKÂm‰A¶Ç梄­scripts/cookie_secret.pyUT lÜÄPʶ3Qux õeÁ ƒ0Dïû[ÓƒRª‘"ø-K¢k»P×`Á¿o¤ôÔ9 Ì 1—&…­q¢ ëŽþˆ¯UÁà¸N¢ÏSœï0p}ËÈ@¿n ܵ¿”’Lyš‘HíÂD8 X-V”¨è³ü&Ë/X»®eÍ/\žl}Z[Vµ;"¼áYUðPKÂm‰A4¶ã~scripts/localefix.pyUT lÜÄPʶ3Qux õUÛŽƒ †ïy Лӭ¦w OÒv‰Å¡K¢`l×·_ «fçjò¾É»zòXß­Á>é8‡ogIA•ëŒ}4t úx&ùèë3ŒEX6?û¨j*¥m’ A™”Ck¬”¬!4NÀù½¤Ñ¦‡¥"u«ÏËé¶Ú­÷±kjqG¶µ»Øs#X¾$ÊìÁ‚14ÿs‰ïC|…d½7$¦Ã•rà ÷‚ñË»J¶³´Ãœ¥ÆFÎÆÌšX0•Ÿî™¸^Oì3‹åšü;ì¦P½Ðà›­»JõÎ/É/PK Âm‰AÃ[™l ¤.envUTkÜÄPux õPK Âm‰A”‰•$ ¤K.gitignoreUTkÜÄPux õPK Âm‰A íA¨frontend/UTlÜÄPux õPK Âm‰AíAëfrontend/locale/UTlÜÄPux õPK Âm‰AíA5frontend/locale/es_ES/UTkÜÄPux õPK Âm‰A"íA…frontend/locale/es_ES/LC_MESSAGES/UTlÜÄPux õPKÂm‰Ar ÊþDá,¤áfrontend/locale/es_ES/LC_MESSAGES/modname.moUTlÜÄPux õPKÂm‰AZÂ]ŽÌ,¤‹frontend/locale/es_ES/LC_MESSAGES/modname.poUTlÜÄPux õPK Âm‰AíAfrontend/locale/pt_BR/UTlÜÄPux õPK Âm‰A"íAÏfrontend/locale/pt_BR/LC_MESSAGES/UTlÜÄPux õPKÂm‰Aòye@ß,¤+frontend/locale/pt_BR/LC_MESSAGES/modname.moUTlÜÄPux õPKÂm‰A6ŽÔ(‡Ê,¤Ñfrontend/locale/pt_BR/LC_MESSAGES/modname.poUTlÜÄPux õPK Âm‰AíA¾ frontend/static/UTlÜÄPux õPKÂm‰A3ÜÈñá~¤ frontend/static/favicon.icoUTlÜÄPux õPK Âm‰AíA> frontend/template/UTlÜÄPux õPKÂm‰Aë™:ך¤Š frontend/template/base.htmlUTlÜÄPux õPKÂm‰A'.*Yù¤y frontend/template/index.htmlUTlÜÄPux õPKÂm‰A ¨¥™ë¤Èfrontend/template/post.htmlUTlÜÄPux õPK 铊AíA¶modname/UTÆpÆPux õPKÂm‰AHÏßIS¤ømodname/__init__.pyUTlÜÄPux õPKÂm‰A—ããïÀz ¤Žmodname/config.pyUTlÜÄPux õPKÂm‰A>Šû#4¤™modname/main.pyUTlÜÄPux õPKÂm‰AZKm ¤modname/utils.pyUTlÜÄPux õPKÂm‰AÂõ`$ Ù¤¼modname/views.pyUTlÜÄPux õPKÂm‰AHzN3EÓ¤modname/web.pyUTlÜÄPux õPKÂm‰AšÉŸëá¶ ¤modname.confUTlÜÄPux õPK Âm‰AŸé))¤ÄProcfileUTkÜÄPux õPKBTŸA‡z+Äa ¤/README.mdUTl°áPux õPK Âm‰Až¥ÎŸ""¤6"requirements.txtUTlÜÄPux õPK G ŠAíA¢"scripts/UT†ÆPux õPKÂm‰A¶Ç梄­¤ä"scripts/cookie_secret.pyUTlÜÄPux õPKÂm‰A4¶ã~¤º#scripts/localefix.pyUTlÜÄPux õPK ` ë$python-cyclone-1.1/cyclone/bottle.py0000644000175000017500000000670112124336260016654 0ustar lunarlunar# coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Support for Bootle application style. http://bottlepy.com For more information see the bottle demo: https://github.com/fiorix/cyclone/tree/master/demos/bottle """ import cyclone.web import functools import sys from twisted.python import log from twisted.internet import reactor _handlers = [] _BaseHandler = None class Router: def __init__(self): self.items = [] def add(self, method, callback): self.items.append((method, callback)) def __call__(self, *args, **kwargs): obj = _BaseHandler(*args, **kwargs) for (method, callback) in self.items: callback = functools.partial(callback, obj) setattr(obj, method.lower(), callback) return obj def route(path=None, method="GET", callback=None, **kwargs): """Use this decorator to route requests to the handler. Example:: @route("/") def index(cli): cli.write("Hello, world") @route("/foobar", method="post") def whatever(cli): ... """ if callable(path): path, callback = None, path def decorator(callback): _handlers.append((path, method.lower(), callback, kwargs)) return callback return decorator def create_app(**settings): """Return an application which will serve the bottle-defined routes. Parameters: base_handler: The class or factory for request handlers. The default is cyclone.web.RequestHandler. more_handlers: A regular list of tuples containing regex -> handler All other parameters are passed directly to the `cyclone.web.Application` constructor. """ global _handlers, _BaseHandler _BaseHandler = settings.pop("base_handler", cyclone.web.RequestHandler) handlers = {} for (path, method, callback, kwargs) in _handlers: if path not in handlers: handlers[path] = Router() handlers[path].add(method, callback) _handlers = None handlers = handlers.items() + settings.pop("more_handlers", []) return cyclone.web.Application(handlers, **settings) def run(**settings): """Start the application. Parameters: host: Interface to listen on. [default: 0.0.0.0] port: TCP port to listen on. [default: 8888] log: The log file to use, the default is sys.stdout. base_handler: The class or factory for request handlers. The default is cyclone.web.RequestHandler. more_handlers: A regular list of tuples containing regex -> handler All other parameters are passed directly to the `cyclone.web.Application` constructor. """ port = settings.get("port", 8888) interface = settings.get("host", "0.0.0.0") log.startLogging(settings.pop("log", sys.stdout)) reactor.listenTCP(port, create_app(**settings), interface=interface) reactor.run() python-cyclone-1.1/cyclone/jsonrpc.py0000644000175000017500000000610112124336260017033 0ustar lunarlunar# coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Server-side implementation of the JSON-RPC protocol. `JSON-RPC `_ is a lightweight remote procedure call protocol, designed to be simple. For more information, check out the `RPC demo `_. """ import types import cyclone.escape from cyclone.web import HTTPError, RequestHandler from twisted.internet import defer from twisted.python import log, failure class JsonrpcRequestHandler(RequestHandler): """Subclass this class and define jsonrpc_* to make a handler. Example:: class MyRequestHandler(JsonrpcRequestHandler): def jsonrpc_echo(self, text): return text def jsonrpc_sort(self, items): return sorted(items) @defer.inlineCallbacks def jsonrpc_geoip_lookup(self, address): response = yield cyclone.httpclient.fetch( "http://freegeoip.net/json/%s" % address.encode("utf-8")) defer.returnValue(response.body) """ def post(self, *args): self._auto_finish = False try: req = cyclone.escape.json_decode(self.request.body) jsonid = req["id"] assert isinstance(jsonid, types.IntType), \ "Invalid id type: %s" % type(jsonid) method = req["method"] assert isinstance(method, types.StringTypes), \ "Invalid method type: %s" % type(method) params = req["params"] assert isinstance(params, (types.ListType, types.TupleType)), \ "Invalid params type: %s" % type(params) except Exception, e: log.msg("Bad Request: %s" % str(e)) raise HTTPError(400) function = getattr(self, "jsonrpc_%s" % method, None) if callable(function): args = list(args) + params d = defer.maybeDeferred(function, *args) d.addBoth(self._cbResult, jsonid) else: self._cbResult(AttributeError("method not found: %s" % method), jsonid) def _cbResult(self, result, jsonid): if isinstance(result, failure.Failure): error = str(result.value) result = None else: error = None data = {"result": result, "error": error, "id": jsonid} self.finish(cyclone.escape.json_encode(data)) python-cyclone-1.1/cyclone/httpclient.py0000644000175000017500000001540312124336260017540 0ustar lunarlunar# coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Non-blocking HTTP client""" import functools import types from cyclone import escape from cyclone.web import HTTPError from twisted.internet import defer from twisted.internet import reactor from twisted.internet.protocol import Protocol from twisted.web.client import Agent from twisted.web.http_headers import Headers from twisted.web.iweb import IBodyProducer from zope.interface import implements agent = Agent(reactor) class StringProducer(object): implements(IBodyProducer) def __init__(self, body): self.body = body self.length = len(body) def startProducing(self, consumer): consumer.write(self.body) return defer.succeed(None) def pauseProducing(self): pass def stopProducing(self): pass class Receiver(Protocol): def __init__(self, finished): self.finished = finished self.data = [] def dataReceived(self, bytes): self.data.append(bytes) def connectionLost(self, reason): self.finished.callback("".join(self.data)) class HTTPClient(object): def __init__(self, url, *args, **kwargs): self._args = args self._kwargs = kwargs self.url = url self.followRedirect = self._kwargs.get("followRedirect", 0) self.maxRedirects = self._kwargs.get("maxRedirects", 3) self.headers = self._kwargs.get("headers", {}) self.body = self._kwargs.get("postdata") self.method = self._kwargs.get("method", self.body and "POST" or "GET") agent._connectTimeout = self._kwargs.get("timeout", None) if self.method.upper() == "POST" and \ "Content-Type" not in self.headers: self.headers["Content-Type"] = \ ["application/x-www-form-urlencoded"] self.response = None if self.body: self.body_producer = StringProducer(self.body) else: self.body_producer = None @defer.inlineCallbacks def fetch(self): request_headers = Headers(self.headers) response = yield agent.request( self.method, self.url, request_headers, self.body_producer) mr = self.maxRedirects while mr >= 1: if response.code in (301, 302, 303) and self.followRedirect: mr -= 1 headers = dict(response.headers.getAllRawHeaders()) location = headers.get("Location") if location: if isinstance(location, types.ListType): location = location[0] #print("redirecting to:", location) response = yield agent.request( "GET", # self.method, location, request_headers, self.body_producer) else: break else: break response.error = None response.headers = dict(response.headers.getAllRawHeaders()) # HTTP 204 and 304 responses have no body # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html if response.code in (204, 304): response.body = '' else: d = defer.Deferred() response.deliverBody(Receiver(d)) response.body = yield d response.request = self defer.returnValue(response) def fetch(url, *args, **kwargs): """A non-blocking HTTP client. Example:: d = httpclient.fetch("http://google.com") d.addCallback(on_response) By default the client does not follow redirects on HTTP 301, 302, 303 or 307. Parameters: followRedirect: Boolean, to tell the client whether to follow redirects or not. [default: False] maxRedirects: Maximum number of redirects to follow. This is to avoid infinite loops cause by misconfigured servers. postdata: Data that accompanies the request. If a request ``method`` is not set but ``postdata`` is, then it is automatically turned into a ``POST`` and the ``Content-Type`` is set to ``application/x-www-form-urlencoded``. headers: A python dictionary containing HTTP headers for this request. Note that all values must be lists:: headers={"Content-Type": ["application/json"]} The response is an object with the following attributes: code: HTTP server response code. phrase: Text that describe the response code. e.g.: 302 ``See Other`` headers: Response headers length: Content length body: The data, untouched """ return HTTPClient(escape.utf8(url), *args, **kwargs).fetch() class JsonRPC: """JSON-RPC client. Once instantiated, may be used to make multiple calls to the server. Example:: cli = httpclient.JsonRPC("http://localhost:8888/jsonrpc") response1 = yield cli.echo("foobar") response2 = yield cli.sort(["foo", "bar"]) Note that in the example above, ``echo`` and ``sort`` are remote methods provided by the server. """ def __init__(self, url): self.__rpcId = 0 self.__rpcUrl = url def __getattr__(self, attr): return functools.partial(self.__rpcRequest, attr) def __rpcRequest(self, method, *args): q = escape.json_encode({"method": method, "params": args, "id": self.__rpcId}) self.__rpcId += 1 r = defer.Deferred() d = fetch(self.__rpcUrl, method="POST", postdata=q) def _success(response, deferred): if response.code == 200: data = escape.json_decode(response.body) error = data.get("error") if error: deferred.errback(Exception(error)) else: deferred.callback(data.get("result")) else: deferred.errback(HTTPError(response.code, response.phrase)) def _failure(failure, deferred): deferred.errback(failure) d.addCallback(_success, r) d.addErrback(_failure, r) return r python-cyclone-1.1/cyclone/mail.py0000644000175000017500000001343612124336260016310 0ustar lunarlunar# coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Support for sending e-mails with attachments to SMTP servers over plain text, SSL or TLS. For more information, check out the `e-mail demo `_. """ import types import os.path from cStringIO import StringIO from OpenSSL.SSL import SSLv3_METHOD from email import Encoders from email.MIMEText import MIMEText from email.MIMEBase import MIMEBase from email.MIMEMultipart import MIMEMultipart from email.Utils import COMMASPACE, formatdate from twisted.internet import reactor from twisted.internet.defer import Deferred from twisted.internet.ssl import ClientContextFactory from twisted.mail.smtp import ESMTPSenderFactory class Message(object): """Create new e-mail messages. Example:: msg = mail.Message( from_addr="root@localhost", to_addrs=["user1", "user2", "user3"], subject="Test, 123", mime="text/html") """ def __init__(self, from_addr, to_addrs, subject, message, mime="text/plain", charset="utf-8"): self.subject = subject self.from_addr = from_addr if isinstance(to_addrs, types.StringType): self.to_addrs = [to_addrs] else: self.to_addrs = to_addrs self.msg = None self.__cache = None self.message = MIMEText(message, _charset=charset) self.message.set_type(mime) def attach(self, filename, mime=None, charset=None, content=None): """Attach files to this message. Example:: msg.attach("me.png", mime="image/png") It also supports fake attachments:: msg.attach("fake.txt", mime="text/plain", content="gotcha") """ base = os.path.basename(filename) if content is None: fd = open(filename) content = fd.read() fd.close() elif not isinstance(content, types.StringType): raise TypeError("Don't know how to attach content: %s" % repr(content)) part = MIMEBase("application", "octet-stream") part.set_payload(content) Encoders.encode_base64(part) part.add_header("Content-Disposition", "attachment", filename=base) if mime is not None: part.set_type(mime) if charset is not None: part.set_charset(charset) if self.msg is None: self.msg = MIMEMultipart() self.msg.attach(self.message) self.msg.attach(part) def __str__(self): return self.__cache or "cyclone email message: not rendered yet" def render(self): if self.msg is None: self.msg = self.message self.msg["Subject"] = self.subject self.msg["From"] = self.from_addr self.msg["To"] = COMMASPACE.join(self.to_addrs) self.msg["Date"] = formatdate(localtime=True) if self.__cache is None: self.__cache = self.msg.as_string() return StringIO(self.__cache) def add_header(self, key, value, **params): """Adds custom headers to this message. Example:: msg.add_header("X-MailTag", "foobar") """ if self.msg is None: self.msg = self.message self.msg.add_header(key, value, **params) def sendmail(mailconf, message): """Takes a regular dictionary as mailconf, as follows. Example:: mailconf = dict( host="smtp.gmail.com", # required port=25, # optional, default 25 or 587 for SSL/TLS username=foo, # optional, no default password=bar, # optional, no default tls=True, # optional, default False ) d = mail.sendmail(mailconf, msg) d.addCallback(on_response) """ if not isinstance(mailconf, types.DictType): raise TypeError("mailconf must be a regular python dictionary") if not isinstance(message, Message): raise TypeError("message must be an instance of cyclone.mail.Message") host = mailconf.get("host") if not isinstance(host, types.StringType): raise ValueError("mailconf requires a 'host' configuration") use_tls = mailconf.get("tls") if use_tls: port = mailconf.get("port", 587) contextFactory = ClientContextFactory() contextFactory.method = SSLv3_METHOD else: port = mailconf.get("port", 25) contextFactory = None if not isinstance(port, types.IntType): raise ValueError("mailconf requires a proper 'port' configuration") result = Deferred() u = mailconf.get("username") p = mailconf.get("password") factory = ESMTPSenderFactory(u, p, message.from_addr, message.to_addrs, message.render(), result, contextFactory=contextFactory, requireAuthentication=(u and p), requireTransportSecurity=use_tls) reactor.connectTCP(host, port, factory) return result python-cyclone-1.1/cyclone/options.py0000644000175000017500000004423012124336260017055 0ustar lunarlunar#!/usr/bin/env python # # Copyright 2009 Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # cyclone notice # drop in from tornado to cyclone by Gleicon (2012) to help porting some # applications that depends on it. I'd also recommend to look over # http://twistedmatrix.com/documents/current/core/howto/options.html or # straight getopt so you can move your code over other frameworks (nothing # negative related to this module or its code) """A command line parsing module that lets modules define their own options. Each module defines its own options, e.g.:: from cyclone.options import define, options define("mysql_host", default="127.0.0.1:3306", help="Main user DB") define("memcache_hosts", default="127.0.0.1:11011", multiple=True, help="Main user memcache servers") def connect(): db = database.Connection(options.mysql_host) ... The main() method of your application does not need to be aware of all of the options used throughout your program; they are all automatically loaded when the modules are loaded. Your main() method can parse the command line or parse a config file with:: import cyclone.options cyclone.options.parse_config_file("/etc/server.conf") cyclone.options.parse_command_line() Command line formats are what you would expect ("--myoption=myvalue"). Config files are just Python files. Global names become options, e.g.:: myoption = "myvalue" myotheroption = "myothervalue" We support datetimes, timedeltas, ints, and floats (just pass a 'type' kwarg to define). We also accept multi-value options. See the documentation for define() below. """ from __future__ import absolute_import, division, with_statement import datetime import logging import logging.handlers import re import sys import os import time import textwrap from cyclone.escape import _unicode # For pretty log messages, if available try: import curses except ImportError: curses = None class Error(Exception): """Exception raised by errors in the options module.""" pass class _Options(dict): """A collection of options, a dictionary with object-like access. Normally accessed via static functions in the `cyclone.options` module, which reference a global instance. """ def __getattr__(self, name): if isinstance(self.get(name), _Option): return self[name].value() raise AttributeError("Unrecognized option %r" % name) def __setattr__(self, name, value): if isinstance(self.get(name), _Option): return self[name].set(value) raise AttributeError("Unrecognized option %r" % name) def define(self, name, default=None, type=None, help=None, metavar=None, multiple=False, group=None): if name in self: raise Error("Option %r already defined in %s", name, self[name].file_name) frame = sys._getframe(0) options_file = frame.f_code.co_filename file_name = frame.f_back.f_code.co_filename if file_name == options_file: file_name = "" if type is None: if not multiple and default is not None: type = default.__class__ else: type = str if group: group_name = group else: group_name = file_name self[name] = _Option(name, file_name=file_name, default=default, type=type, help=help, metavar=metavar, multiple=multiple, group_name=group_name) def parse_command_line(self, args=None): if args is None: args = sys.argv remaining = [] for i in xrange(1, len(args)): # All things after the last option are command line arguments if not args[i].startswith("-"): remaining = args[i:] break if args[i] == "--": remaining = args[i + 1:] break arg = args[i].lstrip("-") name, equals, value = arg.partition("=") name = name.replace('-', '_') if not name in self: print_help() raise Error('Unrecognized command line option: %r' % name) option = self[name] if not equals: if option.type == bool: value = "true" else: raise Error('Option %r requires a value' % name) option.parse(value) if self.help: print_help() sys.exit(0) # Set up log level and pretty console logging by default if self.logging != 'none': logging.getLogger().setLevel( getattr(logging, self.logging.upper())) enable_pretty_logging() return remaining def parse_config_file(self, path): config = {} execfile(path, config, config) for name in config: if name in self: self[name].set(config[name]) def print_help(self, file=sys.stdout): """Prints all the command line options to stdout.""" print >> file, "Usage: %s [OPTIONS]" % sys.argv[0] print >> file, "\nOptions:\n" by_group = {} for option in self.itervalues(): by_group.setdefault(option.group_name, []).append(option) for filename, o in sorted(by_group.items()): if filename: print >> file, "\n%s options:\n" % os.path.normpath(filename) o.sort(key=lambda option: option.name) for option in o: prefix = option.name if option.metavar: prefix += "=" + option.metavar description = option.help or "" if option.default is not None and option.default != '': description += " (default %s)" % option.default lines = textwrap.wrap(description, 79 - 35) if len(prefix) > 30 or len(lines) == 0: lines.insert(0, '') print >> file, " --%-30s %s" % (prefix, lines[0]) for line in lines[1:]: print >> file, "%-34s %s" % (' ', line) print >> file class _Option(object): def __init__(self, name, default=None, type=basestring, help=None, metavar=None, multiple=False, file_name=None, group_name=None): if default is None and multiple: default = [] self.name = name self.type = type self.help = help self.metavar = metavar self.multiple = multiple self.file_name = file_name self.group_name = group_name self.default = default self._value = None def value(self): return self.default if self._value is None else self._value def parse(self, value): _parse = { datetime.datetime: self._parse_datetime, datetime.timedelta: self._parse_timedelta, bool: self._parse_bool, basestring: self._parse_string, }.get(self.type, self.type) if self.multiple: self._value = [] for part in value.split(","): if self.type in (int, long): # allow ranges of the form X:Y (inclusive at both ends) lo, _, hi = part.partition(":") lo = _parse(lo) hi = _parse(hi) if hi else lo self._value.extend(range(lo, hi + 1)) else: self._value.append(_parse(part)) else: self._value = _parse(value) return self.value() def set(self, value): if self.multiple: if not isinstance(value, list): raise Error("Option %r is required to be a list of %s" % (self.name, self.type.__name__)) for item in value: if item is not None and not isinstance(item, self.type): raise Error("Option %r is required to be a list of %s" % (self.name, self.type.__name__)) else: if value is not None and not isinstance(value, self.type): raise Error("Option %r is required to be a %s (%s given)" % (self.name, self.type.__name__, type(value))) self._value = value # Supported date/time formats in our options _DATETIME_FORMATS = [ "%a %b %d %H:%M:%S %Y", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%dT%H:%M", "%Y%m%d %H:%M:%S", "%Y%m%d %H:%M", "%Y-%m-%d", "%Y%m%d", "%H:%M:%S", "%H:%M", ] def _parse_datetime(self, value): for format in self._DATETIME_FORMATS: try: return datetime.datetime.strptime(value, format) except ValueError: pass raise Error('Unrecognized date/time format: %r' % value) _TIMEDELTA_ABBREVS = [ ('hours', ['h']), ('minutes', ['m', 'min']), ('seconds', ['s', 'sec']), ('milliseconds', ['ms']), ('microseconds', ['us']), ('days', ['d']), ('weeks', ['w']), ] _TIMEDELTA_ABBREV_DICT = dict( (abbrev, full) for full, abbrevs in _TIMEDELTA_ABBREVS for abbrev in abbrevs) _FLOAT_PATTERN = r'[-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][-+]?\d+)?' _TIMEDELTA_PATTERN = re.compile( r'\s*(%s)\s*(\w*)\s*' % _FLOAT_PATTERN, re.IGNORECASE) def _parse_timedelta(self, value): try: sum = datetime.timedelta() start = 0 while start < len(value): m = self._TIMEDELTA_PATTERN.match(value, start) if not m: raise Exception() num = float(m.group(1)) units = m.group(2) or 'seconds' units = self._TIMEDELTA_ABBREV_DICT.get(units, units) sum += datetime.timedelta(**{units: num}) start = m.end() return sum except Exception: raise def _parse_bool(self, value): return value.lower() not in ("false", "0", "f") def _parse_string(self, value): return _unicode(value) options = _Options() """Global options dictionary. Supports both attribute-style and dict-style access. """ def define(name, default=None, type=None, help=None, metavar=None, multiple=False, group=None): """Defines a new command line option. If type is given (one of str, float, int, datetime, or timedelta) or can be inferred from the default, we parse the command line arguments based on the given type. If multiple is True, we accept comma-separated values, and the option value is always a list. For multi-value integers, we also accept the syntax x:y, which turns into range(x, y) - very useful for long integer ranges. help and metavar are used to construct the automatically generated command line help string. The help message is formatted like:: --name=METAVAR help string group is used to group the defined options in logical groups. By default, command line options are grouped by the defined file. Command line option names must be unique globally. They can be parsed from the command line with parse_command_line() or parsed from a config file with parse_config_file. """ return options.define(name, default=default, type=type, help=help, metavar=metavar, multiple=multiple, group=group) def parse_command_line(args=None): """Parses all options given on the command line (defaults to sys.argv). Note that args[0] is ignored since it is the program name in sys.argv. We return a list of all arguments that are not parsed as options. """ return options.parse_command_line(args) def parse_config_file(path): """Parses and loads the Python config file at the given path.""" return options.parse_config_file(path) def print_help(file=sys.stdout): """Prints all the command line options to stdout.""" return options.print_help(file) def enable_pretty_logging(options=options): """Turns on formatted logging output as configured. This is called automatically by `parse_command_line`. """ root_logger = logging.getLogger() if options.log_file_prefix: channel = logging.handlers.RotatingFileHandler( filename=options.log_file_prefix, maxBytes=options.log_file_max_size, backupCount=options.log_file_num_backups) channel.setFormatter(_LogFormatter(color=False)) root_logger.addHandler(channel) if (options.log_to_stderr or (options.log_to_stderr is None and not root_logger.handlers)): # Set up color if we are in a tty and curses is installed color = False if curses and sys.stderr.isatty(): try: curses.setupterm() if curses.tigetnum("colors") > 0: color = True except Exception: pass channel = logging.StreamHandler() channel.setFormatter(_LogFormatter(color=color)) root_logger.addHandler(channel) class _LogFormatter(logging.Formatter): def __init__(self, color, *args, **kwargs): logging.Formatter.__init__(self, *args, **kwargs) self._color = color if color: # The curses module has some str/bytes confusion in # python3. Until version 3.2.3, most methods return # bytes, but only accept strings. In addition, we want to # output these strings with the logging module, which # works with unicode strings. The explicit calls to # unicode() below are harmless in python2 but will do the # right conversion in python 3. fg_color = (curses.tigetstr("setaf") or curses.tigetstr("setf") or "") if (3, 0) < sys.version_info < (3, 2, 3): fg_color = unicode(fg_color, "ascii") self._colors = { logging.DEBUG: unicode(curses.tparm(fg_color, 4), # Blue "ascii"), logging.INFO: unicode(curses.tparm(fg_color, 2), # Green "ascii"), logging.WARNING: unicode(curses.tparm(fg_color, 3), # Yellow "ascii"), logging.ERROR: unicode(curses.tparm(fg_color, 1), # Red "ascii"), } self._normal = unicode(curses.tigetstr("sgr0"), "ascii") def format(self, record): try: record.message = record.getMessage() except Exception, e: record.message = "Bad message (%r): %r" % (e, record.__dict__) assert isinstance(record.message, basestring) # guaranteed by logging record.asctime = time.strftime( "%y%m%d %H:%M:%S", self.converter(record.created)) prefix = '[%(levelname)1.1s %(asctime)s %(module)s:%(lineno)d]' % \ record.__dict__ if self._color: prefix = (self._colors.get(record.levelno, self._normal) + prefix + self._normal) # Encoding notes: The logging module prefers to work with character # strings, but only enforces that log messages are instances of # basestring. In python 2, non-ascii bytestrings will make # their way through the logging framework until they blow up with # an unhelpful decoding error (with this formatter it happens # when we attach the prefix, but there are other opportunities for # exceptions further along in the framework). # # If a byte string makes it this far, convert it to unicode to # ensure it will make it out to the logs. Use repr() as a fallback # to ensure that all byte strings can be converted successfully, # but don't do it by default so we don't add extra quotes to ascii # bytestrings. This is a bit of a hacky place to do this, but # it's worth it since the encoding errors that would otherwise # result are so useless (and cyclone is fond of using utf8-encoded # byte strings whereever possible). try: message = _unicode(record.message) except UnicodeDecodeError: message = repr(record.message) formatted = prefix + " " + message if record.exc_info: if not record.exc_text: record.exc_text = self.formatException(record.exc_info) if record.exc_text: formatted = formatted.rstrip() + "\n" + record.exc_text return formatted.replace("\n", "\n ") # Default options define("help", type=bool, help="show this help information") define("logging", default="info", help=("Set the Python log level. If 'none', cyclone won't touch the " "logging configuration."), metavar="debug|info|warning|error|none") define("log_to_stderr", type=bool, default=None, help=("Send log output to stderr (colorized if possible). " "By default use stderr if --log_file_prefix is not set and " "no other logging is configured.")) define("log_file_prefix", type=str, default=None, metavar="PATH", help=("Path prefix for log files. " "Note that if you are running multiple cyclone processes, " "log_file_prefix must be different for each of them (e.g. " "include the port number)")) define("log_file_max_size", type=int, default=100 * 1000 * 1000, help="max size of log files before rollover") define("log_file_num_backups", type=int, default=10, help="number of log files to keep") python-cyclone-1.1/cyclone/redis.py0000644000175000017500000017252512124336260016501 0ustar lunarlunar# coding: utf-8 # Copyright 2009 Alexandre Fiori # https://github.com/fiorix/txredisapi # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # # Credits: # The Protocol class is an improvement of txRedis' protocol, # by Dorian Raymer and Ludovico Magnocavallo. # # Sharding and Consistent Hashing implementation by Gleicon Moraes. # import bisect import collections import functools import operator import re import types import warnings import zlib import string import hashlib from twisted.internet import defer from twisted.internet import protocol from twisted.internet import reactor from twisted.internet import task from twisted.protocols import basic from twisted.protocols import policies from twisted.python import log from twisted.python.failure import Failure class RedisError(Exception): pass class ConnectionError(RedisError): pass class ResponseError(RedisError): pass class ScriptDoesNotExist(ResponseError): pass class NoScriptRunning(ResponseError): pass class InvalidResponse(RedisError): pass class InvalidData(RedisError): pass class WatchError(RedisError): pass def list_or_args(command, keys, args): oldapi = bool(args) try: iter(keys) if isinstance(keys, (str, unicode)): raise TypeError except TypeError: oldapi = True keys = [keys] if oldapi: warnings.warn(DeprecationWarning( "Passing *args to redis.%s is deprecated. " "Pass an iterable to ``keys`` instead" % command)) keys.extend(args) return keys # Possible first characters in a string containing an integer or a float. _NUM_FIRST_CHARS = frozenset(string.digits + "+-.") class MultiBulkStorage(object): def __init__(self, parent=None): self.items = None self.pending = None self.parent = parent def set_pending(self, pending): if self.pending is None: if pending < 0: self.items = None self.pending = 0 else: self.items = [] self.pending = pending return self else: m = MultiBulkStorage(self) m.set_pending(pending) return m def append(self, item): self.pending -= 1 self.items.append(item) class LineReceiver(protocol.Protocol, basic._PauseableMixin): line_mode = 1 __buffer = '' delimiter = '\r\n' MAX_LENGTH = 16384 def clearLineBuffer(self): b = self.__buffer self.__buffer = "" return b def dataReceived(self, data, unpause=False): if unpause is True: if self.__buffer: self.__buffer = data + self.__buffer else: self.__buffer += data self.resumeProducing() else: self.__buffer = self.__buffer + data while self.line_mode and not self.paused: try: line, self.__buffer = self.__buffer.split(self.delimiter, 1) except ValueError: if len(self.__buffer) > self.MAX_LENGTH: line, self.__buffer = self.__buffer, '' return self.lineLengthExceeded(line) break else: linelength = len(line) if linelength > self.MAX_LENGTH: exceeded = line + self.__buffer self.__buffer = '' return self.lineLengthExceeded(exceeded) why = self.lineReceived(line) if why or self.transport and self.transport.disconnecting: return why else: if not self.paused: data = self.__buffer self.__buffer = '' if data: return self.rawDataReceived(data) def setLineMode(self, extra=''): self.line_mode = 1 if extra: self.pauseProducing() reactor.callLater(0, self.dataReceived, extra, True) def setRawMode(self): self.line_mode = 0 def rawDataReceived(self, data): raise NotImplementedError def lineReceived(self, line): raise NotImplementedError def sendLine(self, line): return self.transport.write(line + self.delimiter) def lineLengthExceeded(self, line): return self.transport.loseConnection() class RedisProtocol(LineReceiver, policies.TimeoutMixin): """ Redis client protocol. """ def __init__(self, charset="utf-8", errors="strict"): self.charset = charset self.errors = errors self.bulk_length = 0 self.bulk_buffer = [] self.post_proc = [] self.multi_bulk = MultiBulkStorage() self.replyQueue = defer.DeferredQueue() self.transactions = 0 self.inTransaction = False self.unwatch_cc = lambda: () self.commit_cc = lambda: () self.script_hashes = set() @defer.inlineCallbacks def connectionMade(self): if self.factory.dbid is not None: try: response = yield self.select(self.factory.dbid) if isinstance(response, ResponseError): raise response except Exception, e: self.factory.continueTrying = False self.transport.loseConnection() msg = "Redis error: could not set dbid=%s: %s" % \ (self.factory.dbid, str(e)) self.factory.connectionError(msg) if self.factory.isLazy: log.msg(msg) defer.returnValue(None) self.connected = 1 self.factory.addConnection(self) def connectionLost(self, why): self.connected = 0 self.script_hashes.clear() self.factory.delConnection(self) LineReceiver.connectionLost(self, why) while self.replyQueue.waiting: self.replyReceived(ConnectionError("Lost connection")) def lineReceived(self, line): """ Reply types: "-" error message "+" single line status reply ":" integer number (protocol level only?) "$" bulk data "*" multi-bulk data """ if line: self.resetTimeout() token, data = line[0], line[1:] else: return if token == "$": # bulk data try: self.bulk_length = long(data) except ValueError: self.replyReceived(InvalidResponse("Cannot convert data " "'%s' to integer" % data)) else: if self.bulk_length == -1: self.bulk_length = 0 self.bulkDataReceived(None) else: self.bulk_length += 2 # 2 == \r\n self.setRawMode() elif token == "*": # multi-bulk data try: n = long(data) except (TypeError, ValueError): self.multi_bulk = MultiBulkStorage() self.replyReceived(InvalidResponse("Cannot convert " "multi-response header " "'%s' to integer" % data)) else: self.multi_bulk = self.multi_bulk.set_pending(n) if n in (0, -1): self.multiBulkDataReceived() elif token == "+": # single line status if data == "QUEUED": self.transactions += 1 self.replyReceived(data) else: if self.multi_bulk.pending: self.handleMultiBulkElement(data) else: self.replyReceived(data) elif token == "-": # error reply = ResponseError(data[4:] if data[:4] == "ERR" else data) if self.multi_bulk.pending: self.handleMultiBulkElement(reply) else: self.replyReceived(reply) elif token == ":": # integer try: reply = int(data) except ValueError: reply = InvalidResponse( "Cannot convert data '%s' to integer" % data) if self.multi_bulk.pending: self.handleMultiBulkElement(reply) else: self.replyReceived(reply) def rawDataReceived(self, data): """ Process and dispatch to bulkDataReceived. """ if self.bulk_length: data, rest = data[:self.bulk_length], data[self.bulk_length:] self.bulk_length -= len(data) else: rest = "" self.bulk_buffer.append(data) if self.bulk_length == 0: bulk_buffer = "".join(self.bulk_buffer)[:-2] self.bulk_buffer = [] self.bulkDataReceived(bulk_buffer) self.setLineMode(extra=rest) def bulkDataReceived(self, data): """ Receipt of a bulk data element. """ el = None if data is not None: if data and data[0] in _NUM_FIRST_CHARS: # Most likely a number try: el = int(data) if data.find('.') == -1 else float(data) except ValueError: pass if el is None: try: el = data.decode(self.charset) except UnicodeDecodeError: el = data if self.multi_bulk.pending or self.multi_bulk.items: self.handleMultiBulkElement(el) else: self.replyReceived(el) def handleMultiBulkElement(self, element): self.multi_bulk.append(element) if not self.multi_bulk.pending: self.multiBulkDataReceived() def multiBulkDataReceived(self): """ Receipt of list or set of bulk data elements. """ while self.multi_bulk.parent and not self.multi_bulk.pending: p = self.multi_bulk.parent p.append(self.multi_bulk.items) self.multi_bulk = p if not self.multi_bulk.pending: reply = self.multi_bulk.items self.multi_bulk = MultiBulkStorage() if self.inTransaction and reply is not None: # watch or multi has been called if self.transactions > 0: self.transactions -= len(reply) # multi: this must be an exec [commit] reply if self.transactions == 0: self.commit_cc() if self.inTransaction: # watch but no multi: process the reply as usual f = self.post_proc[1:] if len(f) == 1 and callable(f[0]): reply = f[0](reply) else: # multi: this must be an exec reply tmp = [] for f, v in zip(self.post_proc[1:], reply): if callable(f): tmp.append(f(v)) else: tmp.append(v) reply = tmp self.post_proc = [] self.replyReceived(reply) def replyReceived(self, reply): """ Complete reply received and ready to be pushed to the requesting function. """ self.replyQueue.put(reply) @staticmethod def handle_reply(r): if isinstance(r, Exception): raise r return r def execute_command(self, *args, **kwargs): if self.connected == 0: raise ConnectionError("Not connected") else: cmds = [] cmd_template = "$%s\r\n%s\r\n" for s in args: if isinstance(s, str): cmd = s elif isinstance(s, unicode): try: cmd = s.encode(self.charset, self.errors) except UnicodeEncodeError, e: raise InvalidData( "Error encoding unicode value '%s': %s" % (repr(s), e)) elif isinstance(s, float): try: cmd = format(s, "f") except NameError: cmd = "%0.6f" % s else: cmd = str(s) cmds.append(cmd_template % (len(cmd), cmd)) self.transport.write("*%s\r\n%s" % (len(cmds), "".join(cmds))) r = self.replyQueue.get().addCallback(self.handle_reply) if self.inTransaction: self.post_proc.append(kwargs.get("post_proc")) else: if "post_proc" in kwargs: f = kwargs["post_proc"] if callable(f): r.addCallback(f) return r ## # REDIS COMMANDS ## # Connection handling def quit(self): """ Close the connection """ self.factory.continueTrying = False return self.execute_command("QUIT") def auth(self, password): """ Simple password authentication if enabled """ return self.execute_command("AUTH", password) def ping(self): """ Ping the server """ return self.execute_command("PING") # Commands operating on all value types def exists(self, key): """ Test if a key exists """ return self.execute_command("EXISTS", key) def delete(self, keys, *args): """ Delete one or more keys """ keys = list_or_args("delete", keys, args) return self.execute_command("DEL", *keys) def type(self, key): """ Return the type of the value stored at key """ return self.execute_command("TYPE", key) def keys(self, pattern="*"): """ Return all the keys matching a given pattern """ return self.execute_command("KEYS", pattern) def randomkey(self): """ Return a random key from the key space """ return self.execute_command("RANDOMKEY") def rename(self, oldkey, newkey): """ Rename the old key in the new one, destroying the newname key if it already exists """ return self.execute_command("RENAME", oldkey, newkey) def renamenx(self, oldkey, newkey): """ Rename the oldname key to newname, if the newname key does not already exist """ return self.execute_command("RENAMENX", oldkey, newkey) def dbsize(self): """ Return the number of keys in the current db """ return self.execute_command("DBSIZE") def expire(self, key, time): """ Set a time to live in seconds on a key """ return self.execute_command("EXPIRE", key, time) def persist(self, key): """ Remove the expire from a key """ return self.execute_command("PERSIST", key) def ttl(self, key): """ Get the time to live in seconds of a key """ return self.execute_command("TTL", key) def select(self, index): """ Select the DB with the specified index """ return self.execute_command("SELECT", index) def move(self, key, dbindex): """ Move the key from the currently selected DB to the dbindex DB """ return self.execute_command("MOVE", key, dbindex) def flush(self, all_dbs=False): warnings.warn(DeprecationWarning( "redis.flush() has been deprecated, " "use redis.flushdb() or redis.flushall() instead")) return all_dbs and self.flushall() or self.flushdb() def flushdb(self): """ Remove all the keys from the currently selected DB """ return self.execute_command("FLUSHDB") def flushall(self): """ Remove all the keys from all the databases """ return self.execute_command("FLUSHALL") # Commands operating on string values def set(self, key, value, preserve=False, getset=False): """ Set a key to a string value """ if preserve: warnings.warn(DeprecationWarning( "preserve option to 'set' is deprecated, " "use redis.setnx() instead")) return self.setnx(key, value) if getset: warnings.warn(DeprecationWarning( "getset option to 'set' is deprecated, " "use redis.getset() instead")) return self.getset(key, value) return self.execute_command("SET", key, value) def get(self, key): """ Return the string value of the key """ return self.execute_command("GET", key) def getbit(self, key, offset): """ Return the bit value at offset in the string value stored at key """ return self.execute_command("GETBIT", key, offset) def getset(self, key, value): """ Set a key to a string returning the old value of the key """ return self.execute_command("GETSET", key, value) def mget(self, keys, *args): """ Multi-get, return the strings values of the keys """ keys = list_or_args("mget", keys, args) return self.execute_command("MGET", *keys) def setbit(self, key, offset, value): """ Sets or clears the bit at offset in the string value stored at key """ if isinstance(value, bool): value = int(value) return self.execute_command("SETBIT", key, offset, value) def setnx(self, key, value): """ Set a key to a string value if the key does not exist """ return self.execute_command("SETNX", key, value) def setex(self, key, time, value): """ Set+Expire combo command """ return self.execute_command("SETEX", key, time, value) def mset(self, mapping): """ Set the respective fields to the respective values. HMSET replaces old values with new values. """ items = [] for pair in mapping.iteritems(): items.extend(pair) return self.execute_command("MSET", *items) def msetnx(self, mapping): """ Set multiple keys to multiple values in a single atomic operation if none of the keys already exist """ items = [] for pair in mapping.iteritems(): items.extend(pair) return self.execute_command("MSETNX", *items) def bitop(self, operation, destkey, *srckeys): """ Perform a bitwise operation between multiple keys and store the result in the destination key. """ srclen = len(srckeys) if srclen == 0: return defer.fail(RedisError("no ``srckeys`` specified")) if isinstance(operation, (str, unicode)): operation = operation.upper() elif operation is operator.and_ or operation is operator.__and__: operation = 'AND' elif operation is operator.or_ or operation is operator.__or__: operation = 'OR' elif operation is operator.__xor__ or operation is operator.xor: operation = 'XOR' elif operation is operator.__not__ or operation is operator.not_: operation = 'NOT' if operation not in ('AND', 'OR', 'XOR', 'NOT'): return defer.fail(InvalidData( "Invalid operation: %s" % operation)) if operation == 'NOT' and srclen > 1: return defer.fail(RedisError( "bitop NOT takes only one ``srckey``")) return self.execute_command('BITOP', operation, destkey, *srckeys) def bitcount(self, key, start=None, end=None): if (end is None and start is not None) or \ (start is None and end is not None): raise RedisError("``start`` and ``end`` must both be specified") if start is not None: t = (start, end) else: t = () return self.execute_command("BITCOUNT", key, *t) def incr(self, key, amount=1): """ Increment the integer value of key """ return self.execute_command("INCRBY", key, amount) def incrby(self, key, amount): """ Increment the integer value of key by integer """ return self.incr(key, amount) def decr(self, key, amount=1): """ Decrement the integer value of key """ return self.execute_command("DECRBY", key, amount) def decrby(self, key, amount): """ Decrement the integer value of key by integer """ return self.decr(key, amount) def append(self, key, value): """ Append the specified string to the string stored at key """ return self.execute_command("APPEND", key, value) def substr(self, key, start, end=-1): """ Return a substring of a larger string """ return self.execute_command("SUBSTR", key, start, end) # Commands operating on lists def push(self, key, value, tail=False): warnings.warn(DeprecationWarning( "redis.push() has been deprecated, " "use redis.lpush() or redis.rpush() instead")) return tail and self.rpush(key, value) or self.lpush(key, value) def rpush(self, key, value): """ Append an element to the tail of the List value at key """ if isinstance(value, tuple) or isinstance(value, list): return self.execute_command("RPUSH", key, *value) else: return self.execute_command("RPUSH", key, value) def lpush(self, key, value): """ Append an element to the head of the List value at key """ if isinstance(value, tuple) or isinstance(value, list): return self.execute_command("LPUSH", key, *value) else: return self.execute_command("LPUSH", key, value) def llen(self, key): """ Return the length of the List value at key """ return self.execute_command("LLEN", key) def lrange(self, key, start, end): """ Return a range of elements from the List at key """ return self.execute_command("LRANGE", key, start, end) def ltrim(self, key, start, end): """ Trim the list at key to the specified range of elements """ return self.execute_command("LTRIM", key, start, end) def lindex(self, key, index): """ Return the element at index position from the List at key """ return self.execute_command("LINDEX", key, index) def lset(self, key, index, value): """ Set a new value as the element at index position of the List at key """ return self.execute_command("LSET", key, index, value) def lrem(self, key, count, value): """ Remove the first-N, last-N, or all the elements matching value from the List at key """ return self.execute_command("LREM", key, count, value) def pop(self, key, tail=False): warnings.warn(DeprecationWarning( "redis.pop() has been deprecated, " "user redis.lpop() or redis.rpop() instead")) return tail and self.rpop(key) or self.lpop(key) def lpop(self, key): """ Return and remove (atomically) the first element of the List at key """ return self.execute_command("LPOP", key) def rpop(self, key): """ Return and remove (atomically) the last element of the List at key """ return self.execute_command("RPOP", key) def blpop(self, keys, timeout=0): """ Blocking LPOP """ if isinstance(keys, (str, unicode)): keys = [keys] else: keys = list(keys) keys.append(timeout) return self.execute_command("BLPOP", *keys) def brpop(self, keys, timeout=0): """ Blocking RPOP """ if isinstance(keys, (str, unicode)): keys = [keys] else: keys = list(keys) keys.append(timeout) return self.execute_command("BRPOP", *keys) def brpoplpush(self, source, destination, timeout = 0): """ Pop a value from a list, push it to another list and return it; or block until one is available. """ return self.execute_command("BRPOPLPUSH", source, destination, timeout) def rpoplpush(self, srckey, dstkey): """ Return and remove (atomically) the last element of the source List stored at srckey and push the same element to the destination List stored at dstkey """ return self.execute_command("RPOPLPUSH", srckey, dstkey) def _make_set(self, result): if isinstance(result, list): return set(result) return result # Commands operating on sets def sadd(self, key, members, *args): """ Add the specified member to the Set value at key """ members = list_or_args("sadd", members, args) return self.execute_command("SADD", key, *members) def srem(self, key, members, *args): """ Remove the specified member from the Set value at key """ members = list_or_args("srem", members, args) return self.execute_command("SREM", key, *members) def spop(self, key): """ Remove and return (pop) a random element from the Set value at key """ return self.execute_command("SPOP", key) def smove(self, srckey, dstkey, member): """ Move the specified member from one Set to another atomically """ return self.execute_command( "SMOVE", srckey, dstkey, member).addCallback(bool) def scard(self, key): """ Return the number of elements (the cardinality) of the Set at key """ return self.execute_command("SCARD", key) def sismember(self, key, value): """ Test if the specified value is a member of the Set at key """ return self.execute_command("SISMEMBER", key, value).addCallback(bool) def sinter(self, keys, *args): """ Return the intersection between the Sets stored at key1, ..., keyN """ keys = list_or_args("sinter", keys, args) return self.execute_command("SINTER", *keys).addCallback( self._make_set) def sinterstore(self, dstkey, keys, *args): """ Compute the intersection between the Sets stored at key1, key2, ..., keyN, and store the resulting Set at dstkey """ keys = list_or_args("sinterstore", keys, args) return self.execute_command("SINTERSTORE", dstkey, *keys) def sunion(self, keys, *args): """ Return the union between the Sets stored at key1, key2, ..., keyN """ keys = list_or_args("sunion", keys, args) return self.execute_command("SUNION", *keys).addCallback( self._make_set) def sunionstore(self, dstkey, keys, *args): """ Compute the union between the Sets stored at key1, key2, ..., keyN, and store the resulting Set at dstkey """ keys = list_or_args("sunionstore", keys, args) return self.execute_command("SUNIONSTORE", dstkey, *keys) def sdiff(self, keys, *args): """ Return the difference between the Set stored at key1 and all the Sets key2, ..., keyN """ keys = list_or_args("sdiff", keys, args) return self.execute_command("SDIFF", *keys).addCallback( self._make_set) def sdiffstore(self, dstkey, keys, *args): """ Compute the difference between the Set key1 and all the Sets key2, ..., keyN, and store the resulting Set at dstkey """ keys = list_or_args("sdiffstore", keys, args) return self.execute_command("SDIFFSTORE", dstkey, *keys) def smembers(self, key): """ Return all the members of the Set value at key """ return self.execute_command("SMEMBERS", key).addCallback( self._make_set) def srandmember(self, key): """ Return a random member of the Set value at key """ return self.execute_command("SRANDMEMBER", key) # Commands operating on sorted zsets (sorted sets) def zadd(self, key, score, member, *args): """ Add the specified member to the Sorted Set value at key or update the score if it already exist """ if args: # Args should be pairs (have even number of elements) if len(args) % 2: return defer.fail(InvalidData( "Invalid number of arguments to ZADD")) else: l = [score, member] l.extend(args) args = l else: args = [score, member] return self.execute_command("ZADD", key, *args) def zrem(self, key, *args): """ Remove the specified member from the Sorted Set value at key """ return self.execute_command("ZREM", key, *args) def zincr(self, key, member): return self.zincrby(key, 1, member) def zdecr(self, key, member): return self.zincrby(key, -1, member) def zincrby(self, key, increment, member): """ If the member already exists increment its score by increment, otherwise add the member setting increment as score """ return self.execute_command("ZINCRBY", key, increment, member) def zrank(self, key, member): """ Return the rank (or index) or member in the sorted set at key, with scores being ordered from low to high """ return self.execute_command("ZRANK", key, member) def zrevrank(self, key, member): """ Return the rank (or index) or member in the sorted set at key, with scores being ordered from high to low """ return self.execute_command("ZREVRANK", key, member) def _handle_withscores(self, r): if isinstance(r, list): # Return a list tuples of form (value, score) return zip(r[::2], r[1::2]) return r def _zrange(self, key, start, end, withscores, reverse): if reverse: cmd = "ZREVRANGE" else: cmd = "ZRANGE" if withscores: pieces = (cmd, key, start, end, "WITHSCORES") else: pieces = (cmd, key, start, end) r = self.execute_command(*pieces) if withscores: r.addCallback(self._handle_withscores) return r def zrange(self, key, start=0, end=-1, withscores=False): """ Return a range of elements from the sorted set at key """ return self._zrange(key, start, end, withscores, False) def zrevrange(self, key, start=0, end=-1, withscores=False): """ Return a range of elements from the sorted set at key, exactly like ZRANGE, but the sorted set is ordered in traversed in reverse order, from the greatest to the smallest score """ return self._zrange(key, start, end, withscores, True) def _zrangebyscore(self, key, min, max, withscores, offset, count, rev): if rev: cmd = "ZREVRANGEBYSCORE" else: cmd = "ZRANGEBYSCORE" if (offset is None) != (count is None): # XNOR return defer.fail(InvalidData( "Invalid count and offset arguments to %s" % cmd)) if withscores: pieces = [cmd, key, min, max, "WITHSCORES"] else: pieces = [cmd, key, min, max] if offset is not None and count is not None: pieces.extend(("LIMIT", offset, count)) r = self.execute_command(*pieces) if withscores: r.addCallback(self._handle_withscores) return r def zrangebyscore(self, key, min='-inf', max='+inf', withscores=False, offset=None, count=None): """ Return all the elements with score >= min and score <= max (a range query) from the sorted set """ return self._zrangebyscore(key, min, max, withscores, offset, count, False) def zrevrangebyscore(self, key, max='+inf', min='-inf', withscores=False, offset=None, count=None): """ ZRANGEBYSCORE in reverse order """ # ZREVRANGEBYSCORE takes max before min return self._zrangebyscore(key, max, min, withscores, offset, count, True) def zcount(self, key, min='-inf', max='+inf'): """ Return the number of elements with score >= min and score <= max in the sorted set """ if min == '-inf' and max == '+inf': return self.zcard(key) return self.execute_command("ZCOUNT", key, min, max) def zcard(self, key): """ Return the cardinality (number of elements) of the sorted set at key """ return self.execute_command("ZCARD", key) def zscore(self, key, element): """ Return the score associated with the specified element of the sorted set at key """ return self.execute_command("ZSCORE", key, element) def zremrangebyrank(self, key, min=0, max=-1): """ Remove all the elements with rank >= min and rank <= max from the sorted set """ return self.execute_command("ZREMRANGEBYRANK", key, min, max) def zremrangebyscore(self, key, min='-inf', max='+inf'): """ Remove all the elements with score >= min and score <= max from the sorted set """ return self.execute_command("ZREMRANGEBYSCORE", key, min, max) def zunionstore(self, dstkey, keys, aggregate=None): """ Perform a union over a number of sorted sets with optional weight and aggregate """ return self._zaggregate("ZUNIONSTORE", dstkey, keys, aggregate) def zinterstore(self, dstkey, keys, aggregate=None): """ Perform an intersection over a number of sorted sets with optional weight and aggregate """ return self._zaggregate("ZINTERSTORE", dstkey, keys, aggregate) def _zaggregate(self, command, dstkey, keys, aggregate): pieces = [command, dstkey, len(keys)] if isinstance(keys, dict): keys, weights = zip(*keys.items()) else: weights = None pieces.extend(keys) if weights: pieces.append("WEIGHTS") pieces.extend(weights) if aggregate: if aggregate is min: aggregate = 'MIN' elif aggregate is max: aggregate = 'MAX' elif aggregate is sum: aggregate = 'SUM' else: err_flag = True if isinstance(aggregate, (str, unicode)): aggregate_u = aggregate.upper() if aggregate_u in ('MIN', 'MAX', 'SUM'): aggregate = aggregate_u err_flag = False if err_flag: return defer.fail(InvalidData( "Invalid aggregate function: %s" % aggregate)) pieces.extend(("AGGREGATE", aggregate)) return self.execute_command(*pieces) # Commands operating on hashes def hset(self, key, field, value): """ Set the hash field to the specified value. Creates the hash if needed """ return self.execute_command("HSET", key, field, value) def hsetnx(self, key, field, value): """ Set the hash field to the specified value if the field does not exist. Creates the hash if needed """ return self.execute_command("HSETNX", key, field, value) def hget(self, key, field): """ Retrieve the value of the specified hash field. """ return self.execute_command("HGET", key, field) def hmget(self, key, fields): """ Get the hash values associated to the specified fields. """ return self.execute_command("HMGET", key, *fields) def hmset(self, key, mapping): """ Set the hash fields to their respective values. """ items = [] for pair in mapping.iteritems(): items.extend(pair) return self.execute_command("HMSET", key, *items) def hincr(self, key, field): return self.hincrby(key, field, 1) def hdecr(self, key, field): return self.hincrby(key, field, -1) def hincrby(self, key, field, integer): """ Increment the integer value of the hash at key on field with integer. """ return self.execute_command("HINCRBY", key, field, integer) def hexists(self, key, field): """ Test for existence of a specified field in a hash """ return self.execute_command("HEXISTS", key, field) def hdel(self, key, fields): """ Remove the specified field or fields from a hash """ if isinstance(fields, (str, unicode)): fields = [fields] else: fields = list(fields) return self.execute_command("HDEL", key, *fields) def hlen(self, key): """ Return the number of items in a hash. """ return self.execute_command("HLEN", key) def hkeys(self, key): """ Return all the fields in a hash. """ return self.execute_command("HKEYS", key) def hvals(self, key): """ Return all the values in a hash. """ return self.execute_command("HVALS", key) def hgetall(self, key): """ Return all the fields and associated values in a hash. """ f = lambda d: dict(zip(d[::2], d[1::2])) return self.execute_command("HGETALL", key, post_proc=f) # Sorting def sort(self, key, start=None, end=None, by=None, get=None, desc=None, alpha=False, store=None): if (start is not None and end is None) or \ (end is not None and start is None): raise RedisError("``start`` and ``end`` must both be specified") pieces = [key] if by is not None: pieces.append("BY") pieces.append(by) if start is not None and end is not None: pieces.append("LIMIT") pieces.append(start) pieces.append(end) if get is not None: pieces.append("GET") pieces.append(get) if desc: pieces.append("DESC") if alpha: pieces.append("ALPHA") if store is not None: pieces.append("STORE") pieces.append(store) return self.execute_command("SORT", *pieces) def _clear_txstate(self): self.inTransaction = False def watch(self, keys): if not self.inTransaction: self.inTransaction = True self.unwatch_cc = self._clear_txstate self.commit_cc = lambda: () if isinstance(keys, (str, unicode)): keys = [keys] d = self.execute_command("WATCH", *keys).addCallback(self._tx_started) return d def unwatch(self): self.unwatch_cc() return self.execute_command("UNWATCH") # Transactions # multi() will return a deferred with a "connection" object # That object must be used for further interactions within # the transaction. At the end, either exec() or discard() # must be executed. def multi(self, keys=None): self.inTransaction = True self.unwatch_cc = lambda: () self.commit_cc = self._clear_txstate if keys is not None: d = self.watch(keys) d.addCallback(lambda _: self.execute_command("MULTI")) else: d = self.execute_command("MULTI") d.addCallback(self._tx_started) return d def _tx_started(self, response): if response != 'OK': raise RedisError('Invalid response: %s' % response) return self def _commit_check(self, response): if response is None: self.transactions = 0 self.inTransaction = False raise WatchError("Transaction failed") else: return response def commit(self): if self.inTransaction is False: raise RedisError("Not in transaction") return self.execute_command("EXEC").addCallback(self._commit_check) def discard(self): if self.inTransaction is False: raise RedisError("Not in transaction") self.post_proc = [] self.transactions = 0 self.inTransaction = False return self.execute_command("DISCARD") # Publish/Subscribe # see the SubscriberProtocol for subscribing to channels def publish(self, channel, message): """ Publish message to a channel """ return self.execute_command("PUBLISH", channel, message) # Persistence control commands def save(self): """ Synchronously save the DB on disk """ return self.execute_command("SAVE") def bgsave(self): """ Asynchronously save the DB on disk """ return self.execute_command("BGSAVE") def lastsave(self): """ Return the UNIX time stamp of the last successfully saving of the dataset on disk """ return self.execute_command("LASTSAVE") def shutdown(self): """ Synchronously save the DB on disk, then shutdown the server """ self.factory.continueTrying = False return self.execute_command("SHUTDOWN") def bgrewriteaof(self): """ Rewrite the append only file in background when it gets too big """ return self.execute_command("BGREWRITEAOF") def _process_info(self, r): keypairs = [x for x in r.split('\r\n') if u':' in x and not x.startswith(u'#')] d = {} for kv in keypairs: k, v = kv.split(u':') d[k] = v return d # Remote server control commands def info(self, type=None): """ Provide information and statistics about the server """ if type is None: return self.execute_command("INFO") else: r = self.execute_command("INFO", type) return r.addCallback(self._process_info) # slaveof is missing # Redis 2.6 scripting commands def _eval(self, script, script_hash, keys, args): n = len(keys) keys_and_args = tuple(keys) + tuple(args) r = self.execute_command("EVAL", script, n, *keys_and_args) if script_hash in self.script_hashes: return r return r.addCallback(self._eval_success, script_hash) def _eval_success(self, r, script_hash): self.script_hashes.add(script_hash) return r def _evalsha_failed(self, err, script, script_hash, keys, args): if err.check(ScriptDoesNotExist): return self._eval(script, script_hash, keys, args) return err def eval(self, script, keys=[], args=[]): h = hashlib.sha1(script).hexdigest() if h in self.script_hashes: return self.evalsha(h, keys, args).addErrback( self._evalsha_failed, script, h, keys, args) return self._eval(script, h, keys, args) def _evalsha_errback(self, err, script_hash): if err.check(ResponseError): if err.value.args[0].startswith(u'NOSCRIPT'): if script_hash in self.script_hashes: self.script_hashes.remove(script_hash) raise ScriptDoesNotExist("No script matching hash: %s found" % script_hash) return err def evalsha(self, sha1_hash, keys=[], args=[]): n = len(keys) keys_and_args = tuple(keys) + tuple(args) r = self.execute_command("EVALSHA", sha1_hash, n, *keys_and_args).addErrback(self._evalsha_errback, sha1_hash) if sha1_hash not in self.script_hashes: r.addCallback(self._eval_success, sha1_hash) return r def _script_exists_success(self, r): l = [bool(x) for x in r] if len(l) == 1: return l[0] else: return l def script_exists(self, *hashes): return self.execute_command("SCRIPT", "EXISTS", post_proc=self._script_exists_success, *hashes) def _script_flush_success(self, r): self.script_hashes.clear() return r def script_flush(self): return self.execute_command("SCRIPT", "FLUSH").addCallback( self._script_flush_success) def _handle_script_kill(self, r): if isinstance(r, Failure): if r.check(ResponseError): if r.value.args[0].startswith(u'NOTBUSY'): raise NoScriptRunning("No script running") else: pass return r def script_kill(self): return self.execute_command("SCRIPT", "KILL").addBoth(self._handle_script_kill) def script_load(self, script): return self.execute_command("SCRIPT", "LOAD", script) class MonitorProtocol(RedisProtocol): """ monitor has the same behavior as subscribe: hold the connection until something happens. take care with the performance impact: http://redis.io/commands/monitor """ def messageReceived(self, message): pass def replyReceived(self, reply): self.messageReceived(reply) def monitor(self): return self.execute_command("MONITOR") def stop(self): self.transport.loseConnection() class SubscriberProtocol(RedisProtocol): def messageReceived(self, pattern, channel, message): pass def replyReceived(self, reply): if isinstance(reply, list): if reply[-3] == u"message": self.messageReceived(None, *reply[-2:]) else: self.replyQueue.put(reply[-3:]) self.messageReceived(*reply[-3:]) elif isinstance(reply, Exception): self.replyQueue.put(reply) def subscribe(self, channels): if isinstance(channels, (str, unicode)): channels = [channels] return self.execute_command("SUBSCRIBE", *channels) def unsubscribe(self, channels): if isinstance(channels, (str, unicode)): channels = [channels] return self.execute_command("UNSUBSCRIBE", *channels) def psubscribe(self, patterns): if isinstance(patterns, (str, unicode)): patterns = [patterns] return self.execute_command("PSUBSCRIBE", *patterns) def punsubscribe(self, patterns): if isinstance(patterns, (str, unicode)): patterns = [patterns] return self.execute_command("PUNSUBSCRIBE", *patterns) class ConnectionHandler(object): def __init__(self, factory): self._factory = factory self._connected = factory.deferred def _wait_pool_cleanup(self, deferred): if self._factory.size == 0: deferred.callback(True) def disconnect(self): self._factory.continueTrying = 0 for conn in self._factory.pool: try: conn.transport.loseConnection() except: pass d = defer.Deferred() t = task.LoopingCall(self._wait_pool_cleanup, d) d.addCallback(lambda ign: t.stop()) t.start(.5) return d def __getattr__(self, method): try: return getattr(self._factory.getConnection, method) except Exception, e: d = defer.Deferred() d.errback(e) return lambda *ign: d def __repr__(self): try: cli = self._factory.pool[0].transport.getPeer() except: return "" else: return "" % \ (cli.host, cli.port, self._factory.size) class UnixConnectionHandler(ConnectionHandler): def __repr__(self): try: cli = self._factory.pool[0].transport.getPeer() except: return "" else: return "" % \ (cli.name, self._factory.size) ShardedMethods = frozenset([ "decr", "delete", "exists", "expire", "get", "get_type", "getset", "hdel", "hexists", "hget", "hgetall", "hincrby", "hkeys", "hlen", "hmget", "hmset", "hset", "hvals", "incr", "lindex", "llen", "lrange", "lrem", "lset", "ltrim", "pop", "publish", "push", "rename", "sadd", "set", "setex", "setnx", "sismember", "smembers", "srem", "ttl", "zadd", "zcard", "zcount", "zdecr", "zincr", "zincrby", "zrange", "zrangebyscore", "zrevrangebyscore", "zrevrank", "zrank", "zrem", "zremrangebyscore", "zremrangebyrank", "zrevrange", "zscore", ]) _findhash = re.compile(r'.+\{(.*)\}.*') class HashRing(object): """Consistent hash for redis API""" def __init__(self, nodes=[], replicas=160): self.nodes = [] self.replicas = replicas self.ring = {} self.sorted_keys = [] for n in nodes: self.add_node(n) def add_node(self, node): self.nodes.append(node) for x in xrange(self.replicas): crckey = zlib.crc32("%s:%d" % (node._factory.uuid, x)) self.ring[crckey] = node self.sorted_keys.append(crckey) self.sorted_keys.sort() def remove_node(self, node): self.nodes.remove(node) for x in xrange(self.replicas): crckey = zlib.crc32("%s:%d" % (node, x)) self.ring.remove(crckey) self.sorted_keys.remove(crckey) def get_node(self, key): n, i = self.get_node_pos(key) return n #self.get_node_pos(key)[0] def get_node_pos(self, key): if len(self.ring) == 0: return [None, None] crc = zlib.crc32(key) idx = bisect.bisect(self.sorted_keys, crc) # prevents out of range index idx = min(idx, (self.replicas * len(self.nodes)) - 1) return [self.ring[self.sorted_keys[idx]], idx] def iter_nodes(self, key): if len(self.ring) == 0: yield None, None node, pos = self.get_node_pos(key) for k in self.sorted_keys[pos:]: yield k, self.ring[k] def __call__(self, key): return self.get_node(key) class ShardedConnectionHandler(object): def __init__(self, connections): if isinstance(connections, defer.DeferredList): self._ring = None connections.addCallback(self._makeRing) else: self._ring = HashRing(connections) def _makeRing(self, connections): connections = map(operator.itemgetter(1), connections) self._ring = HashRing(connections) return self @defer.inlineCallbacks def disconnect(self): if not self._ring: raise ConnectionError("Not connected") for conn in self._ring.nodes: yield conn.disconnect() defer.returnValue(True) def _wrap(self, method, *args, **kwargs): try: key = args[0] assert isinstance(key, (str, unicode)) except: raise ValueError( "Method '%s' requires a key as the first argument" % method) m = _findhash.match(key) if m is not None and len(m.groups()) >= 1: node = self._ring(m.groups()[0]) else: node = self._ring(key) return getattr(node, method)(*args, **kwargs) def __getattr__(self, method): if method in ShardedMethods: return functools.partial(self._wrap, method) else: raise NotImplementedError("Method '%s' cannot be sharded" % method) @defer.inlineCallbacks def mget(self, keys, *args): """ high-level mget, required because of the sharding support """ keys = list_or_args("mget", keys, args) group = collections.defaultdict(lambda: []) for k in keys: node = self._ring(k) group[node].append(k) deferreds = [] for node, keys in group.items(): nd = node.mget(keys) deferreds.append(nd) result = [] response = yield defer.DeferredList(deferreds) for (success, values) in response: if success: result += values defer.returnValue(result) def __repr__(self): nodes = [] for conn in self._ring.nodes: try: cli = conn._factory.pool[0].transport.getPeer() except: pass else: nodes.append("%s:%s/%d" % (cli.host, cli.port, conn._factory.size)) return "" % ", ".join(nodes) class ShardedUnixConnectionHandler(ShardedConnectionHandler): def __repr__(self): nodes = [] for conn in self._ring.nodes: try: cli = conn._factory.pool[0].transport.getPeer() except: pass else: nodes.append("%s/%d" % (cli.name, conn._factory.size)) return "" % ", ".join(nodes) class RedisFactory(protocol.ReconnectingClientFactory): maxDelay = 10 protocol = RedisProtocol def __init__(self, uuid, dbid, poolsize, isLazy=False, handler=ConnectionHandler): if not isinstance(poolsize, int): raise ValueError("Redis poolsize must be an integer, not %s" % repr(poolsize)) if not isinstance(dbid, (int, types.NoneType)): raise ValueError("Redis dbid must be an integer, not %s" % repr(dbid)) self.uuid = uuid self.dbid = dbid self.poolsize = poolsize self.isLazy = isLazy self.idx = 0 self.size = 0 self.pool = [] self.deferred = defer.Deferred() self.handler = handler(self) def addConnection(self, conn): self.pool.append(conn) self.size = len(self.pool) if self.deferred: if self.size == self.poolsize: self.deferred.callback(self.handler) self.deferred = None def delConnection(self, conn): try: self.pool.remove(conn) except Exception, e: log.msg("Could not remove connection from pool: %s" % str(e)) self.size = len(self.pool) def connectionError(self, why): if self.deferred: self.deferred.errback(ValueError(why)) self.deferred = None @property def getConnection(self): if not self.size: raise ConnectionError("Not connected") n = self.size while n: conn = self.pool[self.idx % self.size] self.idx += 1 if conn.inTransaction is False: return conn n -= 1 raise RedisError("In transaction") class SubscriberFactory(RedisFactory): protocol = SubscriberProtocol def __init__(self, isLazy=False, handler=ConnectionHandler): RedisFactory.__init__(self, None, None, 1, isLazy=isLazy, handler=handler) class MonitorFactory(RedisFactory): protocol = MonitorProtocol def __init__(self, isLazy=False, handler=ConnectionHandler): RedisFactory.__init__(self, None, None, 1, isLazy=isLazy, handler=handler) def makeConnection(host, port, dbid, poolsize, reconnect, isLazy): uuid = "%s:%s" % (host, port) factory = RedisFactory(uuid, dbid, poolsize, isLazy, ConnectionHandler) factory.continueTrying = reconnect for x in xrange(poolsize): reactor.connectTCP(host, port, factory) if isLazy: return factory.handler else: return factory.deferred def makeShardedConnection(hosts, dbid, poolsize, reconnect, isLazy): err = "Please use a list or tuple of host:port for sharded connections" if not isinstance(hosts, (list, tuple)): raise ValueError(err) connections = [] for item in hosts: try: host, port = item.split(":") port = int(port) except: raise ValueError(err) c = makeConnection(host, port, dbid, poolsize, reconnect, isLazy) connections.append(c) if isLazy: return ShardedConnectionHandler(connections) else: deferred = defer.DeferredList(connections) ShardedConnectionHandler(deferred) return deferred def Connection(host="localhost", port=6379, dbid=None, reconnect=True): return makeConnection(host, port, dbid, 1, reconnect, False) def lazyConnection(host="localhost", port=6379, dbid=None, reconnect=True): return makeConnection(host, port, dbid, 1, reconnect, True) def ConnectionPool(host="localhost", port=6379, dbid=None, poolsize=10, reconnect=True): return makeConnection(host, port, dbid, poolsize, reconnect, False) def lazyConnectionPool(host="localhost", port=6379, dbid=None, poolsize=10, reconnect=True): return makeConnection(host, port, dbid, poolsize, reconnect, True) def ShardedConnection(hosts, dbid=None, reconnect=True): return makeShardedConnection(hosts, dbid, 1, reconnect, False) def lazyShardedConnection(hosts, dbid=None, reconnect=True): return makeShardedConnection(hosts, dbid, 1, reconnect, True) def ShardedConnectionPool(hosts, dbid=None, poolsize=10, reconnect=True): return makeShardedConnection(hosts, dbid, poolsize, reconnect, False) def lazyShardedConnectionPool(hosts, dbid=None, poolsize=10, reconnect=True): return makeShardedConnection(hosts, dbid, poolsize, reconnect, True) def makeUnixConnection(path, dbid, poolsize, reconnect, isLazy): factory = RedisFactory(path, dbid, poolsize, isLazy, UnixConnectionHandler) factory.continueTrying = reconnect for x in xrange(poolsize): reactor.connectUNIX(path, factory) if isLazy: return factory.handler else: return factory.deferred def makeShardedUnixConnection(paths, dbid, poolsize, reconnect, isLazy): err = "Please use a list or tuple of paths for sharded unix connections" if not isinstance(paths, (list, tuple)): raise ValueError(err) connections = [] for path in paths: c = makeUnixConnection(path, dbid, poolsize, reconnect, isLazy) connections.append(c) if isLazy: return ShardedUnixConnectionHandler(connections) else: deferred = defer.DeferredList(connections) ShardedUnixConnectionHandler(deferred) return deferred def UnixConnection(path="/tmp/redis.sock", dbid=None, reconnect=True): return makeUnixConnection(path, dbid, 1, reconnect, False) def lazyUnixConnection(path="/tmp/redis.sock", dbid=None, reconnect=True): return makeUnixConnection(path, dbid, 1, reconnect, True) def UnixConnectionPool(path="/tmp/redis.sock", dbid=None, poolsize=10, reconnect=True): return makeUnixConnection(path, dbid, poolsize, reconnect, False) def lazyUnixConnectionPool(path="/tmp/redis.sock", dbid=None, poolsize=10, reconnect=True): return makeUnixConnection(path, dbid, poolsize, reconnect, True) def ShardedUnixConnection(paths, dbid=None, reconnect=True): return makeShardedUnixConnection(paths, dbid, 1, reconnect, False) def lazyShardedUnixConnection(paths, dbid=None, reconnect=True): return makeShardedUnixConnection(paths, dbid, 1, reconnect, True) def ShardedUnixConnectionPool(paths, dbid=None, poolsize=10, reconnect=True): return makeShardedUnixConnection(paths, dbid, poolsize, reconnect, False) def lazyShardedUnixConnectionPool(paths, dbid=None, poolsize=10, reconnect=True): return makeShardedUnixConnection(paths, dbid, poolsize, reconnect, True) __all__ = [ Connection, lazyConnection, ConnectionPool, lazyConnectionPool, ShardedConnection, lazyShardedConnection, ShardedConnectionPool, lazyShardedConnectionPool, UnixConnection, lazyUnixConnection, UnixConnectionPool, lazyUnixConnectionPool, ShardedUnixConnection, lazyShardedUnixConnection, ShardedUnixConnectionPool, lazyShardedUnixConnectionPool, ] __author__ = "Alexandre Fiori" __version__ = version = "1.1" python-cyclone-1.1/cyclone/sse.py0000644000175000017500000000633412124336260016157 0ustar lunarlunar# coding: utf-8 # # Copyright 2010 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """`Server-sent events `_ is a technology for providing push notifications from a server to a browser client in the form of DOM events. For more information, check out the `SEE demo `_. """ from cyclone import escape from cyclone.web import RequestHandler from twisted.python import log class SSEHandler(RequestHandler): """Subclass this class and define `bind` and `unbind` to get notified when a new client connects or disconnects, respectively. Once connected, you may send events to the browser via `sendEvent`. """ def __init__(self, application, request): RequestHandler.__init__(self, application, request) self.transport = request.connection.transport self._auto_finish = False def sendEvent(self, message, event=None, eid=None, retry=None): """ sendEvent is the single method to send events to clients. Parameters: message: the event itself event: optional event name eid: optional event id to be used as Last-Event-ID header or e.lastEventId property retry: set the retry timeout in ms. default 3 secs. """ if isinstance(message, dict): message = escape.json_encode(message) if isinstance(message, unicode): message = message.encode("utf-8") assert isinstance(message, str) if eid: self.transport.write("id: %s\n" % eid) if event: self.transport.write("event: %s\n" % event) if retry: self.transport.write("retry: %s\n" % retry) self.transport.write("data: %s\n\n" % message) def _execute(self, transforms, *args, **kwargs): self._transforms = [] # transforms if self.settings.get("debug"): log.msg("SSE connection from %s" % self.request.remote_ip) self.set_header("Content-Type", "text/event-stream") self.set_header("Cache-Control", "no-cache") self.set_header("Connection", "keep-alive") self.flush() self.request.connection.setRawMode() self.notifyFinish().addCallback(self.on_connection_closed) self.bind() def on_connection_closed(self, *args, **kwargs): if self.settings.get("debug"): log.msg("SSE client disconnected %s" % self.request.remote_ip) self.unbind() def bind(self): """Gets called when a new client connects.""" pass def unbind(self): """Gets called when an existing client disconnects.""" pass python-cyclone-1.1/cyclone/template.py0000644000175000017500000007470012124336260017202 0ustar lunarlunar#!/usr/bin/env python # # Copyright 2012 Alexandre Fiori # based on the original Tornado by Facebook # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """A simple template system that compiles templates to Python code. Basic usage looks like:: t = template.Template("{{ myvalue }}") print t.generate(myvalue="XXX") Loader is a class that loads templates from a root directory and caches the compiled templates:: loader = template.Loader("/home/btaylor") print loader.load("test.html").generate(myvalue="XXX") We compile all templates to raw Python. Error-reporting is currently... uh, interesting. Syntax for the templates:: ### base.html {% block title %}Default title{% end %}
    {% for student in students %} {% block student %}
  • {{ escape(student.name) }}
  • {% end %} {% end %}
### bold.html {% extends "base.html" %} {% block title %}A bolder title{% end %} {% block student %}
  • {{ escape(student.name) }}
  • {% end %} Unlike most other template systems, we do not put any restrictions on the expressions you can include in your statements. if and for blocks get translated exactly into Python, you can do complex expressions like:: {% for student in [p for p in people if p.student and p.age > 23] %}
  • {{ escape(student.name) }}
  • {% end %} Translating directly to Python means you can apply functions to expressions easily, like the escape() function in the examples above. You can pass functions in to your template just like any other variable:: ### Python code def add(x, y): return x + y template.execute(add=add) ### The template {{ add(1, 2) }} We provide the functions escape(), url_escape(), json_encode(), and squeeze() to all templates by default. Typical applications do not create `Template` or `Loader` instances by hand, but instead use the `render` and `render_string` methods of `cyclone.web.RequestHandler`, which load templates automatically based on the ``template_path`` `Application` setting. Syntax Reference ---------------- Template expressions are surrounded by double curly braces: ``{{ ... }}``. The contents may be any python expression, which will be escaped according to the current autoescape setting and inserted into the output. Other template directives use ``{% %}``. These tags may be escaped as ``{{!`` and ``{%!`` if you need to include a literal ``{{`` or ``{%`` in the output. To comment out a section so that it is omitted from the output, surround it with ``{# ... #}``. ``{% apply *function* %}...{% end %}`` Applies a function to the output of all template code between ``apply`` and ``end``:: {% apply linkify %}{{name}} said: {{message}}{% end %} Note that as an implementation detail apply blocks are implemented as nested functions and thus may interact strangely with variables set via ``{% set %}``, or the use of ``{% break %}`` or ``{% continue %}`` within loops. ``{% autoescape *function* %}`` Sets the autoescape mode for the current file. This does not affect other files, even those referenced by ``{% include %}``. Note that autoescaping can also be configured globally, at the `Application` or `Loader`.:: {% autoescape xhtml_escape %} {% autoescape None %} ``{% block *name* %}...{% end %}`` Indicates a named, replaceable block for use with ``{% extends %}``. Blocks in the parent template will be replaced with the contents of the same-named block in a child template.:: {% block title %}Default title{% end %} {% extends "base.html" %} {% block title %}My page title{% end %} ``{% comment ... %}`` A comment which will be removed from the template output. Note that there is no ``{% end %}`` tag; the comment goes from the word ``comment`` to the closing ``%}`` tag. ``{% extends *filename* %}`` Inherit from another template. Templates that use ``extends`` should contain one or more ``block`` tags to replace content from the parent template. Anything in the child template not contained in a ``block`` tag will be ignored. For an example, see the ``{% block %}`` tag. ``{% for *var* in *expr* %}...{% end %}`` Same as the python ``for`` statement. ``{% break %}`` and ``{% continue %}`` may be used inside the loop. ``{% from *x* import *y* %}`` Same as the python ``import`` statement. ``{% if *condition* %}...{% elif *condition* %}...{% else %}...{% end %}`` Conditional statement - outputs the first section whose condition is true. (The ``elif`` and ``else`` sections are optional) ``{% import *module* %}`` Same as the python ``import`` statement. ``{% include *filename* %}`` Includes another template file. The included file can see all the local variables as if it were copied directly to the point of the ``include`` directive (the ``{% autoescape %}`` directive is an exception). Alternately, ``{% module Template(filename, **kwargs) %}`` may be used to include another template with an isolated namespace. ``{% module *expr* %}`` Renders a `~cyclone.web.UIModule`. The output of the ``UIModule`` is not escaped:: {% module Template("foo.html", arg=42) %} ``{% raw *expr* %}`` Outputs the result of the given expression without autoescaping. ``{% set *x* = *y* %}`` Sets a local variable. ``{% try %}...{% except %}...{% finally %}...{% else %}...{% end %}`` Same as the python ``try`` statement. ``{% while *condition* %}... {% end %}`` Same as the python ``while`` statement. ``{% break %}`` and ``{% continue %}`` may be used inside the loop. """ from __future__ import absolute_import, division, with_statement import datetime import linecache import os.path import posixpath import re import sys import threading import traceback import types from cStringIO import StringIO from cyclone import escape from cyclone.util import ObjectDict from cyclone.util import bytes_type from cyclone.util import unicode_type _DEFAULT_AUTOESCAPE = "xhtml_escape" _UNSET = object() class Template(object): """A compiled template. We compile into Python from the given template_string. You can generate the template from variables with generate(). """ def __init__(self, template_string, name="", loader=None, compress_whitespace=None, autoescape=_UNSET): self.name = name if compress_whitespace is None: compress_whitespace = name.endswith(".html") or \ name.endswith(".js") if autoescape is not _UNSET: self.autoescape = autoescape elif loader: self.autoescape = loader.autoescape else: self.autoescape = _DEFAULT_AUTOESCAPE self.namespace = loader.namespace if loader else {} reader = _TemplateReader(name, escape.native_str(template_string)) try: self.file = _File(self, _parse(reader, self)) self.code = self._generate_python(loader, compress_whitespace) except ParseError, e: raise TemplateError("Error parsing template %s, line %d: %s" % (name, reader.line, str(e))) self.loader = loader try: # Under python2.5, the fake filename used here must match # the module name used in __name__ below. self.compiled = compile( escape.to_unicode(self.code), "%s.generated.py" % self.name.replace('.', '_'), "exec") except Exception: raise TemplateError("Error compiling template " + name + ":\n" + _format_code(self.code).rstrip()) def generate(self, **kwargs): """Generate this template with the given arguments.""" namespace = { "escape": escape.xhtml_escape, "xhtml_escape": escape.xhtml_escape, "url_escape": escape.url_escape, "json_encode": escape.json_encode, "squeeze": escape.squeeze, "linkify": escape.linkify, "datetime": datetime, "_utf8": escape.utf8, # for internal use "_string_types": (unicode_type, bytes_type), # __name__ and __loader__ allow the traceback mechanism to find # the generated source code. "__name__": self.name.replace('.', '_'), "__loader__": ObjectDict(get_source=lambda name: self.code), } namespace.update(self.namespace) namespace.update(kwargs) exec self.compiled in namespace execute = namespace["_execute"] # Clear the traceback module's cache of source data now that # we've generated a new template (mainly for this module's # unittests, where different tests reuse the same name). linecache.clearcache() try: return execute() except: raise TemplateError("Error executing template " + self.name + ":\n" + _format_code(traceback.format_exception(*sys.exc_info()))) def _generate_python(self, loader, compress_whitespace): buffer = StringIO() try: # named_blocks maps from names to _NamedBlock objects named_blocks = {} ancestors = self._get_ancestors(loader) ancestors.reverse() for ancestor in ancestors: ancestor.find_named_blocks(loader, named_blocks) self.file.find_named_blocks(loader, named_blocks) writer = _CodeWriter(buffer, named_blocks, loader, ancestors[0].template, compress_whitespace) ancestors[0].generate(writer) return buffer.getvalue() finally: buffer.close() def _get_ancestors(self, loader): ancestors = [self.file] for chunk in self.file.body.chunks: if isinstance(chunk, _ExtendsBlock): if not loader: raise ParseError("{% extends %} block found, but no " "template loader") template = loader.load(chunk.name, self.name) ancestors.extend(template._get_ancestors(loader)) return ancestors class BaseLoader(object): """Base class for template loaders.""" def __init__(self, autoescape=_DEFAULT_AUTOESCAPE, namespace=None): """Creates a template loader. root_directory may be the empty string if this loader does not use the filesystem. autoescape must be either None or a string naming a function in the template namespace, such as "xhtml_escape". """ self.autoescape = autoescape self.namespace = namespace or {} self.templates = {} # self.lock protects self.templates. It's a reentrant lock # because templates may load other templates via `include` or # `extends`. Note that thanks to the GIL this code would be safe # even without the lock, but could lead to wasted work as multiple # threads tried to compile the same template simultaneously. self.lock = threading.RLock() def reset(self): """Resets the cache of compiled templates.""" with self.lock: self.templates = {} def resolve_path(self, name, parent_path=None): """Converts a possibly-relative path to absolute (used internally).""" raise NotImplementedError() def load(self, name, parent_path=None): """Loads a template.""" name = self.resolve_path(name, parent_path=parent_path) with self.lock: if name not in self.templates: self.templates[name] = self._create_template(name) return self.templates[name] def _create_template(self, name): raise NotImplementedError() class Loader(BaseLoader): """A template loader that loads from a single root directory. You must use a template loader to use template constructs like {% extends %} and {% include %}. Loader caches all templates after they are loaded the first time. """ def __init__(self, root_directory, **kwargs): super(Loader, self).__init__(**kwargs) self.root = os.path.abspath(root_directory) def resolve_path(self, name, parent_path=None): if parent_path and not parent_path.startswith("<") and \ not parent_path.startswith("/") and \ not name.startswith("/"): current_path = os.path.join(self.root, parent_path) file_dir = os.path.dirname(os.path.abspath(current_path)) relative_path = os.path.abspath(os.path.join(file_dir, name)) if relative_path.startswith(self.root): name = relative_path[len(self.root) + 1:] return name def _create_template(self, name): path = os.path.join(self.root, name) f = open(path, "rb") template = Template(f.read(), name=name, loader=self) f.close() return template class DictLoader(BaseLoader): """A template loader that loads from a dictionary.""" def __init__(self, dict, **kwargs): super(DictLoader, self).__init__(**kwargs) self.dict = dict def resolve_path(self, name, parent_path=None): if parent_path and not parent_path.startswith("<") and \ not parent_path.startswith("/") and \ not name.startswith("/"): file_dir = posixpath.dirname(parent_path) name = posixpath.normpath(posixpath.join(file_dir, name)) return name def _create_template(self, name): return Template(self.dict[name], name=name, loader=self) class _Node(object): def each_child(self): return () def generate(self, writer): raise NotImplementedError() def find_named_blocks(self, loader, named_blocks): for child in self.each_child(): child.find_named_blocks(loader, named_blocks) class _File(_Node): def __init__(self, template, body): self.template = template self.body = body self.line = 0 def generate(self, writer): writer.write_line("def _execute():", self.line) with writer.indent(): writer.write_line("_buffer = []", self.line) writer.write_line("_append = _buffer.append", self.line) self.body.generate(writer) writer.write_line("return _utf8('').join(_buffer)", self.line) def each_child(self): return (self.body,) class _ChunkList(_Node): def __init__(self, chunks): self.chunks = chunks def generate(self, writer): for chunk in self.chunks: chunk.generate(writer) def each_child(self): return self.chunks class _NamedBlock(_Node): def __init__(self, name, body, template, line): self.name = name self.body = body self.template = template self.line = line def each_child(self): return (self.body,) def generate(self, writer): block = writer.named_blocks[self.name] with writer.include(block.template, self.line): block.body.generate(writer) def find_named_blocks(self, loader, named_blocks): named_blocks[self.name] = self _Node.find_named_blocks(self, loader, named_blocks) class _ExtendsBlock(_Node): def __init__(self, name): self.name = name class _IncludeBlock(_Node): def __init__(self, name, reader, line): self.name = name self.template_name = reader.name self.line = line def find_named_blocks(self, loader, named_blocks): included = loader.load(self.name, self.template_name) included.file.find_named_blocks(loader, named_blocks) def generate(self, writer): included = writer.loader.load(self.name, self.template_name) with writer.include(included, self.line): included.file.body.generate(writer) class _ApplyBlock(_Node): def __init__(self, method, line, body=None): self.method = method self.line = line self.body = body def each_child(self): return (self.body,) def generate(self, writer): method_name = "apply%d" % writer.apply_counter writer.apply_counter += 1 writer.write_line("def %s():" % method_name, self.line) with writer.indent(): writer.write_line("_buffer = []", self.line) writer.write_line("_append = _buffer.append", self.line) self.body.generate(writer) writer.write_line("return _utf8('').join(_buffer)", self.line) writer.write_line("_append(_utf8(%s(%s())))" % ( self.method, method_name), self.line) class _ControlBlock(_Node): def __init__(self, statement, line, body=None): self.statement = statement self.line = line self.body = body def each_child(self): return (self.body,) def generate(self, writer): writer.write_line("%s:" % self.statement, self.line) with writer.indent(): self.body.generate(writer) # Just in case the body was empty writer.write_line("pass", self.line) class _IntermediateControlBlock(_Node): def __init__(self, statement, line): self.statement = statement self.line = line def generate(self, writer): # In case the previous block was empty writer.write_line("pass", self.line) writer.write_line("%s:" % self.statement, self.line, writer.indent_size() - 1) class _Statement(_Node): def __init__(self, statement, line): self.statement = statement self.line = line def generate(self, writer): writer.write_line(self.statement, self.line) class _Expression(_Node): def __init__(self, expression, line, raw=False): self.expression = expression self.line = line self.raw = raw def generate(self, writer): writer.write_line("_tmp = %s" % self.expression, self.line) writer.write_line("if isinstance(_tmp, _string_types):" " _tmp = _utf8(_tmp)", self.line) writer.write_line("else: _tmp = _utf8(str(_tmp))", self.line) if not self.raw and writer.current_template.autoescape is not None: # In python3 functions like xhtml_escape return unicode, # so we have to convert to utf8 again. writer.write_line("_tmp = _utf8(%s(_tmp))" % writer.current_template.autoescape, self.line) writer.write_line("_append(_tmp)", self.line) class _Module(_Expression): def __init__(self, expression, line): super(_Module, self).__init__("_modules." + expression, line, raw=True) class _Text(_Node): def __init__(self, value, line): self.value = value self.line = line def generate(self, writer): value = self.value # Compress lots of white space to a single character. If the whitespace # breaks a line, have it continue to break a line, but just with a # single \n character if writer.compress_whitespace and "
    " not in value:
                value = re.sub(r"([\t ]+)", " ", value)
                value = re.sub(r"(\s*\n\s*)", "\n", value)
    
            if value:
                writer.write_line('_append(%r)' % escape.utf8(value), self.line)
    
    
    class ParseError(Exception):
        """Raised for template syntax errors."""
        pass
    
    
    class TemplateError(Exception):
        pass
    
    
    class _CodeWriter(object):
        def __init__(self, file, named_blocks, loader, current_template,
                     compress_whitespace):
            self.file = file
            self.named_blocks = named_blocks
            self.loader = loader
            self.current_template = current_template
            self.compress_whitespace = compress_whitespace
            self.apply_counter = 0
            self.include_stack = []
            self._indent = 0
    
        def indent_size(self):
            return self._indent
    
        def indent(self):
            class Indenter(object):
                def __enter__(_):
                    self._indent += 1
                    return self
    
                def __exit__(_, *args):
                    assert self._indent > 0
                    self._indent -= 1
    
            return Indenter()
    
        def include(self, template, line):
            self.include_stack.append((self.current_template, line))
            self.current_template = template
    
            class IncludeTemplate(object):
                def __enter__(_):
                    return self
    
                def __exit__(_, *args):
                    self.current_template = self.include_stack.pop()[0]
    
            return IncludeTemplate()
    
        def write_line(self, line, line_number, indent=None):
            if indent is None:
                indent = self._indent
            line_comment = '  # %s:%d' % (self.current_template.name, line_number)
            if self.include_stack:
                ancestors = ["%s:%d" % (tmpl.name, lineno)
                             for (tmpl, lineno) in self.include_stack]
                line_comment += ' (via %s)' % ', '.join(reversed(ancestors))
            self.file.write("    " * indent + line + line_comment + "\n")
    
    
    class _TemplateReader(object):
        def __init__(self, name, text):
            self.name = name
            self.text = text
            self.line = 1
            self.pos = 0
    
        def find(self, needle, start=0, end=None):
            assert start >= 0, start
            pos = self.pos
            start += pos
            if end is None:
                index = self.text.find(needle, start)
            else:
                end += pos
                assert end >= start
                index = self.text.find(needle, start, end)
            if index != -1:
                index -= pos
            return index
    
        def consume(self, count=None):
            if count is None:
                count = len(self.text) - self.pos
            newpos = self.pos + count
            self.line += self.text.count("\n", self.pos, newpos)
            s = self.text[self.pos:newpos]
            self.pos = newpos
            return s
    
        def remaining(self):
            return len(self.text) - self.pos
    
        def __len__(self):
            return self.remaining()
    
        def __getitem__(self, key):
            if type(key) is slice:
                size = len(self)
                start, stop, step = key.indices(size)
                if start is None:
                    start = self.pos
                else:
                    start += self.pos
                if stop is not None:
                    stop += self.pos
                return self.text[slice(start, stop, step)]
            elif key < 0:
                return self.text[key]
            else:
                return self.text[self.pos + key]
    
        def __str__(self):
            return self.text[self.pos:]
    
    
    def _format_code(code):
        lines = code if isinstance(code, types.ListType) else code.splitlines()
        format = "%%%dd  %%s\n" % len(repr(len(lines) + 1))
        return "".join([format % (i + 1, line) for (i, line) in enumerate(lines)])
    
    
    def _parse(reader, template, in_block=None, in_loop=None):
        body = _ChunkList([])
        while True:
            # Find next template directive
            curly = 0
            while True:
                curly = reader.find("{", curly)
                if curly == -1 or curly + 1 == reader.remaining():
                    # EOF
                    if in_block:
                        raise ParseError("Missing {%% end %%} block for %s" %
                                         in_block)
                    body.chunks.append(_Text(reader.consume(), reader.line))
                    return body
                # If the first curly brace is not the start of a special token,
                # start searching from the character after it
                if reader[curly + 1] not in ("{", "%", "#"):
                    curly += 1
                    continue
                # When there are more than 2 curlies in a row, use the
                # innermost ones.  This is useful when generating languages
                # like latex where curlies are also meaningful
                if (curly + 2 < reader.remaining() and
                    reader[curly + 1] == '{' and reader[curly + 2] == '{'):
                    curly += 1
                    continue
                break
    
            # Append any text before the special token
            if curly > 0:
                cons = reader.consume(curly)
                body.chunks.append(_Text(cons, reader.line))
    
            start_brace = reader.consume(2)
            line = reader.line
    
            # Template directives may be escaped as "{{!" or "{%!".
            # In this case output the braces and consume the "!".
            # This is especially useful in conjunction with jquery templates,
            # which also use double braces.
            if reader.remaining() and reader[0] == "!":
                reader.consume(1)
                body.chunks.append(_Text(start_brace, line))
                continue
    
            # Comment
            if start_brace == "{#":
                end = reader.find("#}")
                if end == -1:
                    raise ParseError("Missing end expression #} on line %d" % line)
                contents = reader.consume(end).strip()
                reader.consume(2)
                continue
    
            # Expression
            if start_brace == "{{":
                end = reader.find("}}")
                if end == -1:
                    raise ParseError("Missing end expression }} on line %d" % line)
                contents = reader.consume(end).strip()
                reader.consume(2)
                if not contents:
                    raise ParseError("Empty expression on line %d" % line)
                body.chunks.append(_Expression(contents, line))
                continue
    
            # Block
            assert start_brace == "{%", start_brace
            end = reader.find("%}")
            if end == -1:
                raise ParseError("Missing end block %%} on line %d" % line)
            contents = reader.consume(end).strip()
            reader.consume(2)
            if not contents:
                raise ParseError("Empty block tag ({%% %%}) on line %d" % line)
    
            operator, space, suffix = contents.partition(" ")
            suffix = suffix.strip()
    
            # Intermediate ("else", "elif", etc) blocks
            intermediate_blocks = {
                "else": set(["if", "for", "while", "try"]),
                "elif": set(["if"]),
                "except": set(["try"]),
                "finally": set(["try"]),
            }
            allowed_parents = intermediate_blocks.get(operator)
            if allowed_parents is not None:
                if not in_block:
                    raise ParseError("%s outside %s block" %
                                (operator, allowed_parents))
                if in_block not in allowed_parents:
                    raise ParseError("%s block cannot be attached to %s block" %
                                     (operator, in_block))
                body.chunks.append(_IntermediateControlBlock(contents, line))
                continue
    
            # End tag
            elif operator == "end":
                if not in_block:
                    raise ParseError("Extra {%% end %%} block on line %d" % line)
                return body
    
            elif operator in ("extends", "include", "set", "import", "from",
                              "comment", "autoescape", "raw", "module"):
                if operator == "comment":
                    continue
                if operator == "extends":
                    suffix = suffix.strip('"').strip("'")
                    if not suffix:
                        raise ParseError("extends missing file path on line %d" %
                                         line)
                    block = _ExtendsBlock(suffix)
                elif operator in ("import", "from"):
                    if not suffix:
                        raise ParseError("import missing statement on line %d" %
                                         line)
                    block = _Statement(contents, line)
                elif operator == "include":
                    suffix = suffix.strip('"').strip("'")
                    if not suffix:
                        raise ParseError("include missing file path on line %d" %
                                         line)
                    block = _IncludeBlock(suffix, reader, line)
                elif operator == "set":
                    if not suffix:
                        raise ParseError("set missing statement on line %d" % line)
                    block = _Statement(suffix, line)
                elif operator == "autoescape":
                    fn = suffix.strip()
                    if fn == "None":
                        fn = None
                    template.autoescape = fn
                    continue
                elif operator == "raw":
                    block = _Expression(suffix, line, raw=True)
                elif operator == "module":
                    block = _Module(suffix, line)
                body.chunks.append(block)
                continue
    
            elif operator in ("apply", "block", "try", "if", "for", "while"):
                # parse inner body recursively
                if operator in ("for", "while"):
                    block_body = _parse(reader, template, operator, operator)
                elif operator == "apply":
                    # apply creates a nested function so syntactically it's not
                    # in the loop.
                    block_body = _parse(reader, template, operator, None)
                else:
                    block_body = _parse(reader, template, operator, in_loop)
    
                if operator == "apply":
                    if not suffix:
                        raise ParseError("apply missing method name on line %d" %
                                         line)
                    block = _ApplyBlock(suffix, line, block_body)
                elif operator == "block":
                    if not suffix:
                        raise ParseError("block missing name on line %d" % line)
                    block = _NamedBlock(suffix, block_body, template, line)
                else:
                    block = _ControlBlock(contents, line, block_body)
                body.chunks.append(block)
                continue
    
            elif operator in ("break", "continue"):
                if not in_loop:
                    raise ParseError("%s outside %s block" % (operator,
                                     set(["for", "while"])))
                body.chunks.append(_Statement(contents, line))
                continue
    
            else:
                raise ParseError("unknown operator: %r" % operator)
    python-cyclone-1.1/cyclone/util.py0000644000175000017500000000525512124336260016343 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    from twisted.python import log
    
    
    def _emit(self, eventDict):
        text = log.textFromEventDict(eventDict)
        if not text:
            return
    
        #print "hello? '%s'" % repr(text)
        timeStr = self.formatTime(eventDict['time'])
        #fmtDict = {'system': eventDict['system'],
        #            'text': text.replace("\n", "\n\t")}
        #msgStr = log._safeFormat("[%(system)s] %(text)s\n", fmtDict)
    
        log.util.untilConcludes(self.write, "%s %s\n" % (timeStr,
                                                text.replace("\n", "\n\t")))
        log.util.untilConcludes(self.flush)  # Hoorj!
    
    
    # monkey patch, sorry
    log.FileLogObserver.emit = _emit
    
    
    class ObjectDict(dict):
        """Makes a dictionary behave like an object."""
        def __getattr__(self, name):
            try:
                return self[name]
            except KeyError:
                raise AttributeError(name)
    
        def __setattr__(self, name, value):
            self[name] = value
    
    
    def import_object(name):
        """Imports an object by name.
    
        import_object('x.y.z') is equivalent to 'from x.y import z'.
    
        >>> import cyclone.escape
        >>> import_object('cyclone.escape') is cyclone.escape
        True
        >>> import_object('cyclone.escape.utf8') is cyclone.escape.utf8
        True
        """
        parts = name.split('.')
        obj = __import__('.'.join(parts[:-1]), None, None, [parts[-1]], 0)
        method = getattr(obj, parts[-1], None)
        if method:
            return method
        else:
            raise ImportError("No method named %s" % parts[-1])
    
    
    # Fake byte literal support:  In python 2.6+, you can say b"foo" to get
    # a byte literal (str in 2.x, bytes in 3.x).  There's no way to do this
    # in a way that supports 2.5, though, so we need a function wrapper
    # to convert our string literals.  b() should only be applied to literal
    # latin1 strings.  Once we drop support for 2.5, we can remove this function
    # and just use byte literals.
    #def b(s):
    #    return s
    #
    #def u(s):
    #    return s.decode('unicode_escape')
    
    bytes_type = str
    unicode_type = unicode
    basestring_type = basestring
    
    
    def doctests():
        import doctest
        return doctest.DocTestSuite()
    python-cyclone-1.1/cyclone/httpserver.py0000644000175000017500000003317012124336260017571 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """A non-blocking, single-threaded HTTP server.
    
    Typical applications have little direct interaction with the `HTTPConnection`
    class, which is the HTTP parser executed on incoming connections.
    
    It is a protocol class that inherits Twisted's `LineReceiver
    `_, and is usually created by
    `cyclone.web.Application`, our connection factory.
    
    This module also defines the `HTTPRequest` class which is exposed via
    `cyclone.web.RequestHandler.request`.
    """
    
    from __future__ import absolute_import, division, with_statement
    
    import Cookie
    import socket
    import time
    
    from io import BytesIO as StringIO
    from tempfile import TemporaryFile
    from twisted.python import log
    from twisted.protocols import basic
    from twisted.internet import address
    from twisted.internet import defer
    from twisted.internet import interfaces
    
    from cyclone.escape import utf8, native_str, parse_qs_bytes
    from cyclone import httputil
    from cyclone.util import bytes_type
    
    
    class _BadRequestException(Exception):
        """Exception class for malformed HTTP requests."""
        pass
    
    
    class HTTPConnection(basic.LineReceiver):
        """Handles a connection to an HTTP client, executing HTTP requests.
    
        We parse HTTP headers and bodies, and execute the request callback
        until the HTTP conection is closed.
    
        If ``xheaders`` is ``True``, we support the ``X-Real-Ip`` and ``X-Scheme``
        headers, which override the remote IP and HTTP scheme for all requests.
        These headers are useful when running Tornado behind a reverse proxy or
        load balancer.
        """
        delimiter = "\r\n"
    
        def connectionMade(self):
            self._headersbuffer = []
            self._contentbuffer = None
            self._finish_callback = None
            self.no_keep_alive = False
            self.content_length = None
            self.request_callback = self.factory
            self.xheaders = self.factory.settings.get('xheaders', False)
            self._request = None
            self._request_finished = False
    
        def connectionLost(self, reason):
            if self._finish_callback:
                self._finish_callback.callback(reason.getErrorMessage())
                self._finish_callback = None
    
        def notifyFinish(self):
            if self._finish_callback is None:
                self._finish_callback = defer.Deferred()
            return self._finish_callback
    
        def lineReceived(self, line):
            if line:
                self._headersbuffer.append(line + self.delimiter)
            else:
                buff = "".join(self._headersbuffer)
                self._headersbuffer = []
                self._on_headers(buff)
    
        def rawDataReceived(self, data):
            if self.content_length is not None:
                data, rest = data[:self.content_length], data[self.content_length:]
                self.content_length -= len(data)
            else:
                rest = ''
    
            self._contentbuffer.write(data)
            if self.content_length == 0:
                self._contentbuffer.seek(0, 0)
                self._on_request_body(self._contentbuffer.read())
                self.content_length = self._contentbuffer = None
                self.setLineMode(rest)
    
        def write(self, chunk):
            assert self._request, "Request closed"
            self.transport.write(chunk)
    
        def finish(self):
            assert self._request, "Request closed"
            self._request_finished = True
            self._finish_request()
    
        def _on_write_complete(self):
            if self._request_finished:
                self._finish_request()
    
        def _finish_request(self):
            if self.no_keep_alive:
                disconnect = True
            else:
                connection_header = self._request.headers.get("Connection")
                if self._request.supports_http_1_1():
                    disconnect = connection_header == "close"
                elif ("Content-Length" in self._request.headers
                        or self._request.method in ("HEAD", "GET")):
                    disconnect = connection_header != "Keep-Alive"
                else:
                    disconnect = True
    
            if self._finish_callback:
                self._finish_callback.callback(None)
                self._finish_callback = None
            self._request = None
            self._request_finished = False
            if disconnect is True:
                self.transport.loseConnection()
    
        def _on_headers(self, data):
            try:
                data = native_str(data.decode("latin1"))
                eol = data.find("\r\n")
                start_line = data[:eol]
                try:
                    method, uri, version = start_line.split(" ")
                except ValueError:
                    raise _BadRequestException("Malformed HTTP request line")
                if not version.startswith("HTTP/"):
                    raise _BadRequestException(
                            "Malformed HTTP version in HTTP Request-Line")
                headers = httputil.HTTPHeaders.parse(data[eol:])
                self._request = HTTPRequest(
                    connection=self, method=method, uri=uri, version=version,
                    headers=headers, remote_ip=self._remote_ip)
    
                content_length = int(headers.get("Content-Length", 0))
                if content_length:
                    if headers.get("Expect") == "100-continue":
                        self.transport.write("HTTP/1.1 100 (Continue)\r\n\r\n")
    
                    if content_length < 100000:
                        self._contentbuffer = StringIO()
                    else:
                        self._contentbuffer = TemporaryFile()
    
                    self.content_length = content_length
                    self.setRawMode()
                    return
    
                self.request_callback(self._request)
            except _BadRequestException, e:
                log.msg("Malformed HTTP request from %s: %s", self._remote_ip, e)
                self.transport.loseConnection()
    
        def _on_request_body(self, data):
            self._request.body = data
            content_type = self._request.headers.get("Content-Type", "")
            if self._request.method in ("POST", "PATCH", "PUT"):
                if content_type.startswith("application/x-www-form-urlencoded"):
                    arguments = parse_qs_bytes(native_str(self._request.body))
                    for name, values in arguments.iteritems():
                        values = [v for v in values if v]
                        if values:
                            self._request.arguments.setdefault(name,
                                                               []).extend(values)
                elif content_type.startswith("multipart/form-data"):
                    fields = content_type.split(";")
                    for field in fields:
                        k, sep, v, = field.strip().partition("=")
                        if k == "boundary" and v:
                            httputil.parse_multipart_form_data(
                                utf8(v), data,
                                self._request.arguments,
                                self._request.files)
                            break
                    else:
                        log.msg("Invalid multipart/form-data")
            self.request_callback(self._request)
    
        @property
        def _remote_ip(self):
            peer = self.transport.getPeer()
            if isinstance(peer, address.UNIXAddress):
                remote_ip = "unix:%s" % self.transport.getHost().name
            else:
                remote_ip = self.transport.getPeer().host
            return remote_ip
    
    class HTTPRequest(object):
        """A single HTTP request.
    
        All attributes are type `str` unless otherwise noted.
    
        .. attribute:: method
    
           HTTP request method, e.g. "GET" or "POST"
    
        .. attribute:: uri
    
           The requested uri.
    
        .. attribute:: path
    
           The path portion of `uri`
    
        .. attribute:: query
    
           The query portion of `uri`
    
        .. attribute:: version
    
           HTTP version specified in request, e.g. "HTTP/1.1"
    
        .. attribute:: headers
    
           `HTTPHeader` dictionary-like object for request headers.  Acts like
           a case-insensitive dictionary with additional methods for repeated
           headers.
    
        .. attribute:: body
    
           Request body, if present, as a byte string.
    
        .. attribute:: remote_ip
    
           Client's IP address as a string.  If `HTTPConnection.xheaders` is set,
           will pass along the real IP address provided by a load balancer
           in the ``X-Real-Ip`` header
    
        .. attribute:: protocol
    
           The protocol used, either "http" or "https".
           If `HTTPConnection.xheaders` is set, will pass along the protocol used
           by a load balancer if
           reported via an ``X-Scheme`` header.
    
        .. attribute:: host
    
           The requested hostname, usually taken from the ``Host`` header.
    
        .. attribute:: arguments
    
           GET/POST arguments are available in the arguments property, which
           maps arguments names to lists of values (to support multiple values
           for individual names). Names are of type `str`, while arguments
           are byte strings.  Note that this is different from
           `RequestHandler.get_argument`, which returns argument values as
           unicode strings.
    
        .. attribute:: files
    
           File uploads are available in the files property, which maps file
           names to lists of :class:`HTTPFile`.
    
        .. attribute:: connection
    
           An HTTP request is attached to a single HTTP connection, which can
           be accessed through the "connection" attribute. Since connections
           are typically kept open in HTTP/1.1, multiple requests can be handled
           sequentially on a single connection.
        """
        def __init__(self, method, uri, version="HTTP/1.0", headers=None,
                     body=None, remote_ip=None, protocol=None, host=None,
                     files=None, connection=None):
            self.method = method
            self.uri = uri
            self.version = version
            self.headers = headers or httputil.HTTPHeaders()
            self.body = body or ""
            if connection and connection.xheaders:
                # Squid uses X-Forwarded-For, others use X-Real-Ip
                self.remote_ip = self.headers.get(
                    "X-Real-Ip", self.headers.get("X-Forwarded-For", remote_ip))
                if not self._valid_ip(self.remote_ip):
                    self.remote_ip = remote_ip
                # AWS uses X-Forwarded-Proto
                self.protocol = self.headers.get("X-Scheme",
                                self.headers.get("X-Forwarded-Proto", protocol))
                if self.protocol not in ("http", "https"):
                    self.protocol = "http"
            else:
                self.remote_ip = remote_ip
                if connection and interfaces.ISSLTransport.providedBy(
                                                            connection.transport):
                    self.protocol = "https"
                else:
                    self.protocol = "http"
            self.host = host or self.headers.get("Host") or "127.0.0.1"
            self.files = files or {}
            self.connection = connection
            self._start_time = time.time()
            self._finish_time = None
    
            self.path, sep, self.query = uri.partition("?")
            self.arguments = parse_qs_bytes(self.query, keep_blank_values=True)
    
        def supports_http_1_1(self):
            """Returns True if this request supports HTTP/1.1 semantics"""
            return self.version == "HTTP/1.1"
    
        @property
        def cookies(self):
            """A dictionary of Cookie.Morsel objects."""
            if not hasattr(self, "_cookies"):
                self._cookies = Cookie.SimpleCookie()
                if "Cookie" in self.headers:
                    try:
                        self._cookies.load(
                            native_str(self.headers["Cookie"]))
                    except Exception:
                        self._cookies = {}
            return self._cookies
    
        def write(self, chunk):
            """Writes the given chunk to the response stream."""
            assert isinstance(chunk, bytes_type)
            self.connection.write(chunk)
    
        def finish(self):
            """Finishes this HTTP request on the open connection."""
            self.connection.finish()
            self._finish_time = time.time()
    
        def full_url(self):
            """Reconstructs the full URL for this request."""
            return self.protocol + "://" + self.host + self.uri
    
        def request_time(self):
            """Returns the amount of time it took for this request to execute."""
            if self._finish_time is None:
                return time.time() - self._start_time
            else:
                return self._finish_time - self._start_time
    
        def notifyFinish(self):
            """Returns a Deferred object, which is fired when the request is
            finished and the connection is closed.
            """
            return self.connection.notifyFinish()
    
        def __repr__(self):
            attrs = ("protocol", "host", "method", "uri", "version", "remote_ip",
                     "body")
            args = ", ".join(["%s=%r" % (n, getattr(self, n)) for n in attrs])
            return "%s(%s, headers=%s)" % (
                self.__class__.__name__, args, dict(self.headers))
    
        def _valid_ip(self, ip):
            try:
                res = socket.getaddrinfo(ip, 0, socket.AF_UNSPEC,
                                         socket.SOCK_STREAM,
                                         0, socket.AI_NUMERICHOST)
                return bool(res)
            except socket.gaierror, e:
                if e.args[0] == socket.EAI_NONAME:
                    return False
                raise
            return True
    python-cyclone-1.1/cyclone/web.py0000644000175000017500000025004012124336260016135 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """
    The cyclone web framework looks a bit like web.py (http://webpy.org/) or
    Google's webapp (http://code.google.com/appengine/docs/python/tools/webapp/),
    but with additional tools and optimizations to take advantage of the
    non-blocking web server and tools.
    
    Here is the canonical "Hello, world" example app::
    
        import cyclone.web
        from twisted.internet import reactor
    
        class MainHandler(tornado.web.RequestHandler):
            def get(self):
                self.write("Hello, world")
    
        if __name__ == "__main__":
            application = tornado.web.Application([
                (r"/", MainHandler),
            ])
            reactor.listenTCP(8888, application)
            reactor.run()
    
    See the cyclone walkthrough on http://cyclone.io for more details and a good
    getting started guide.
    
    Thread-safety notes
    -------------------
    
    In general, methods on RequestHandler and elsewhere in cyclone are not
    thread-safe.  In particular, methods such as write(), finish(), and
    flush() must only be called from the main thread.  For more information on
    using threads, please check the twisted documentation:
    http://twistedmatrix.com/documents/current/core/howto/threading.html
    """
    
    from __future__ import absolute_import, division, with_statement
    
    import Cookie
    import base64
    import binascii
    import calendar
    import datetime
    import email.utils
    import functools
    import gzip
    import hashlib
    import hmac
    import httplib
    import itertools
    import mimetypes
    import numbers
    import os.path
    import re
    import stat
    import sys
    import threading
    import time
    import traceback
    import types
    import urllib
    import urlparse
    import uuid
    
    import cyclone
    from cyclone import escape
    from cyclone import httpserver
    from cyclone import locale
    from cyclone import template
    from cyclone.escape import utf8, _unicode
    from cyclone.util import ObjectDict
    from cyclone.util import bytes_type
    from cyclone.util import import_object
    from cyclone.util import unicode_type
    
    from cStringIO import StringIO as BytesIO  # python 2
    from twisted.python import failure
    from twisted.python import log
    from twisted.internet import defer
    from twisted.internet import protocol
    from twisted.internet import reactor
    
    
    class RequestHandler(object):
        """Subclass this class and define get() or post() to make a handler.
    
        If you want to support more methods than the standard GET/HEAD/POST, you
        should override the class variable SUPPORTED_METHODS in your
        RequestHandler class.
    
        If you want lists to be serialized when calling self.write() set
        serialize_lists to True.
        This may have some security implications if you are not protecting against
        XSRF with other means (such as a XSRF token).
        More details on this vulnerability here:
        http://haacked.com/archive/2008/11/20/anatomy-of-a-subtle-json-vulnerability.aspx
        """
        SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PATCH", "PUT",
                             "OPTIONS")
    
        serialize_lists = False
        no_keep_alive = False
        xsrf_cookie_name = "_xsrf"
        _template_loaders = {}  # {path: template.BaseLoader}
        _template_loader_lock = threading.Lock()
    
        def __init__(self, application, request, **kwargs):
            super(RequestHandler, self).__init__()
    
            self.application = application
            self.request = request
            self._headers_written = False
            self._finished = False
            self._auto_finish = True
            self._transforms = None  # will be set in _execute
            self.path_args = None
            self.path_kwargs = None
            self.ui = ObjectDict((n, self._ui_method(m)) for n, m in
                         application.ui_methods.items())
            # UIModules are available as both `modules` and `_modules` in the
            # template namespace.  Historically only `modules` was available
            # but could be clobbered by user additions to the namespace.
            # The template {% module %} directive looks in `_modules` to avoid
            # possible conflicts.
            self.ui["_modules"] = ObjectDict((n, self._ui_module(n, m)) for n, m in
                                     application.ui_modules.items())
            self.ui["modules"] = self.ui["_modules"]
            self.clear()
            self.request.connection.no_keep_alive = self.no_keep_alive
            self.initialize(**kwargs)
    
        def initialize(self):
            """Hook for subclass initialization.
    
            A dictionary passed as the third argument of a url spec will be
            supplied as keyword arguments to initialize().
    
            Example::
    
                class ProfileHandler(RequestHandler):
                    def initialize(self, database):
                        self.database = database
    
                    def get(self, username):
                        ...
    
                app = Application([
                    (r'/user/(.*)', ProfileHandler, dict(database=database)),
                    ])
            """
            pass
    
        @property
        def settings(self):
            """An alias for `self.application.settings`."""
            return self.application.settings
    
        def head(self, *args, **kwargs):
            raise HTTPError(405)
    
        def get(self, *args, **kwargs):
            raise HTTPError(405)
    
        def post(self, *args, **kwargs):
            raise HTTPError(405)
    
        def delete(self, *args, **kwargs):
            raise HTTPError(405)
    
        def patch(self, *args, **kwargs):
            raise HTTPError(405)
    
        def put(self, *args, **kwargs):
            raise HTTPError(405)
    
        def options(self, *args, **kwargs):
            raise HTTPError(405)
    
        def prepare(self):
            """Called at the beginning of a request before `get`/`post`/etc.
    
            Override this method to perform common initialization regardless
            of the request method.
            """
            pass
    
        def on_finish(self):
            """Called after the end of a request.
    
            Override this method to perform cleanup, logging, etc.
            This method is a counterpart to `prepare`.  ``on_finish`` may
            not produce any output, as it is called after the response
            has been sent to the client.
            """
            pass
    
        def on_connection_close(self, *args, **kwargs):
            """Called in async handlers if the client closed the connection.
    
            Override this to clean up resources associated with
            long-lived connections.  Note that this method is called only if
            the connection was closed during asynchronous processing; if you
            need to do cleanup after every request override `on_finish`
            instead.
    
            Proxies may keep a connection open for a time (perhaps
            indefinitely) after the client has gone away, so this method
            may not be called promptly after the end user closes their
            connection.
            """
            pass
    
        def clear(self):
            """Resets all headers and content for this response."""
            # The performance cost of cyclone.httputil.HTTPHeaders is significant
            # (slowing down a benchmark with a trivial handler by more than 10%),
            # and its case-normalization is not generally necessary for
            # headers we generate on the server side, so use a plain dict
            # and list instead.
            self._headers = {
                "Server": "cyclone/%s" % cyclone.version,
                "Content-Type": "text/html; charset=UTF-8",
                "Date": datetime.datetime.utcnow().strftime(
                                                    "%a, %d %b %Y %H:%M:%S GMT"),
            }
            self._list_headers = []
            self.set_default_headers()
            if not self.request.supports_http_1_1():
                if self.request.headers.get("Connection") == "Keep-Alive":
                    self.set_header("Connection", "Keep-Alive")
            self._write_buffer = []
            self._status_code = 200
            self._reason = httplib.responses[200]
    
        def set_default_headers(self):
            """Override this to set HTTP headers at the beginning of the request.
    
            For example, this is the place to set a custom ``Server`` header.
            Note that setting such headers in the normal flow of request
            processing may not do what you want, since headers may be reset
            during error handling.
            """
            pass
    
        def set_status(self, status_code, reason=None):
            """Sets the status code for our response.
    
            :arg int status_code: Response status code. If `reason` is ``None``,
                it must be present in `httplib.responses`.
            :arg string reason: Human-readable reason phrase describing the status
                code. If ``None``, it will be filled in from `httplib.responses`.
            """
            self._status_code = status_code
            if reason is not None:
                self._reason = escape.native_str(reason)
            else:
                try:
                    self._reason = httplib.responses[status_code]
                except KeyError:
                    raise ValueError("unknown status code %d", status_code)
    
        def get_status(self):
            """Returns the status code for our response."""
            return self._status_code
    
        def set_header(self, name, value):
            """Sets the given response header name and value.
    
            If a datetime is given, we automatically format it according to the
            HTTP specification. If the value is not a string, we convert it to
            a string. All header values are then encoded as UTF-8.
            """
            self._headers[name] = self._convert_header_value(value)
    
        def add_header(self, name, value):
            """Adds the given response header and value.
    
            Unlike `set_header`, `add_header` may be called multiple times
            to return multiple values for the same header.
            """
            self._list_headers.append((name, self._convert_header_value(value)))
    
        def clear_header(self, name):
            """Clears an outgoing header, undoing a previous `set_header` call.
    
            Note that this method does not apply to multi-valued headers
            set by `add_header`.
            """
            if name in self._headers:
                del self._headers[name]
    
        def _convert_header_value(self, value):
            if isinstance(value, bytes_type):
                pass
            elif isinstance(value, unicode_type):
                value = value.encode("utf-8")
            elif isinstance(value, numbers.Integral):
                # return immediately since we know the converted value will be safe
                return str(value)
            elif isinstance(value, datetime.datetime):
                t = calendar.timegm(value.utctimetuple())
                return email.utils.formatdate(t, localtime=False, usegmt=True)
            else:
                raise TypeError("Unsupported header value %r" % value)
            # If \n is allowed into the header, it is possible to inject
            # additional headers or split the request. Also cap length to
            # prevent obviously erroneous values.
            if len(value) > 4000 or re.search(r"[\x00-\x1f]", value):
                raise ValueError("Unsafe header value %r", value)
            return value
    
        _ARG_DEFAULT = []
    
        def get_argument(self, name, default=_ARG_DEFAULT, strip=True):
            """Returns the value of the argument with the given name.
    
            If default is not provided, the argument is considered to be
            required, and we throw an HTTP 400 exception if it is missing.
    
            If the argument appears in the url more than once, we return the
            last value.
    
            The returned value is always unicode.
            """
            args = self.get_arguments(name, strip=strip)
            if not args:
                if default is self._ARG_DEFAULT:
                    raise HTTPError(400, "Missing argument " + name)
                return default
            return args[-1]
    
        def get_arguments(self, name, strip=True):
            """Returns a list of the arguments with the given name.
    
            If the argument is not present, returns an empty list.
    
            The returned values are always unicode.
            """
            values = []
            for v in self.request.arguments.get(name, []):
                v = self.decode_argument(v, name=name)
                if isinstance(v, unicode_type):
                    # Get rid of any weird control chars (unless decoding gave
                    # us bytes, in which case leave it alone)
                    v = re.sub(r"[\x00-\x08\x0e-\x1f]", " ", v)
                if strip:
                    v = v.strip()
                values.append(v)
            return values
    
        def decode_argument(self, value, name=None):
            """Decodes an argument from the request.
    
            The argument has been percent-decoded and is now a byte string.
            By default, this method decodes the argument as utf-8 and returns
            a unicode string, but this may be overridden in subclasses.
    
            This method is used as a filter for both get_argument() and for
            values extracted from the url and passed to get()/post()/etc.
    
            The name of the argument is provided if known, but may be None
            (e.g. for unnamed groups in the url regex).
            """
            return _unicode(value)
    
        @property
        def cookies(self):
            return self.request.cookies
    
        def get_cookie(self, name, default=None):
            """Gets the value of the cookie with the given name, else default."""
            if self.request.cookies is not None and name in self.request.cookies:
                return self.request.cookies[name].value
            return default
    
        def set_cookie(self, name, value, domain=None, expires=None, path="/",
                       expires_days=None, **kwargs):
            """Sets the given cookie name/value with the given options.
    
            Additional keyword arguments are set on the Cookie.Morsel directly.
            See http://docs.python.org/library/cookie.html#morsel-objects
            for available attributes.
            """
            # The cookie library only accepts type str, in both python 2 and 3
            name = escape.native_str(name)
            value = escape.native_str(value)
            if re.search(r"[\x00-\x20]", name + value):
                # Don't let us accidentally inject bad stuff
                raise ValueError("Invalid cookie %r: %r" % (name, value))
            if not hasattr(self, "_new_cookie"):
                self._new_cookie = Cookie.SimpleCookie()
            if name in self._new_cookie:
                del self._new_cookie[name]
            self._new_cookie[name] = value
            morsel = self._new_cookie[name]
            if domain:
                morsel["domain"] = domain
            if expires_days is not None and not expires:
                expires = datetime.datetime.utcnow() + datetime.timedelta(
                                                            days=expires_days)
            if expires:
                timestamp = calendar.timegm(expires.utctimetuple())
                morsel["expires"] = email.utils.formatdate(
                                        timestamp, localtime=False, usegmt=True)
            if path:
                morsel["path"] = path
            for k, v in kwargs.items():
                if k == 'max_age':
                    k = 'max-age'
                morsel[k] = v
    
        def clear_cookie(self, name, path="/", domain=None):
            """Deletes the cookie with the given name."""
            expires = datetime.datetime.utcnow() - datetime.timedelta(days=365)
            self.set_cookie(name, value="", path=path, expires=expires,
                            domain=domain)
    
        def clear_all_cookies(self):
            """Deletes all the cookies the user sent with this request."""
            for name in self.request.cookies.iterkeys():
                self.clear_cookie(name)
    
        def set_secure_cookie(self, name, value, expires_days=30, **kwargs):
            """Signs and timestamps a cookie so it cannot be forged.
    
            You must specify the ``cookie_secret`` setting in your Application
            to use this method. It should be a long, random sequence of bytes
            to be used as the HMAC secret for the signature.
    
            To read a cookie set with this method, use `get_secure_cookie()`.
    
            Note that the ``expires_days`` parameter sets the lifetime of the
            cookie in the browser, but is independent of the ``max_age_days``
            parameter to `get_secure_cookie`.
    
            Secure cookies may contain arbitrary byte values, not just unicode
            strings (unlike regular cookies)
            """
            self.set_cookie(name, self.create_signed_value(name, value),
                            expires_days=expires_days, **kwargs)
    
        def create_signed_value(self, name, value):
            """Signs and timestamps a string so it cannot be forged.
    
            Normally used via set_secure_cookie, but provided as a separate
            method for non-cookie uses.  To decode a value not stored
            as a cookie use the optional value argument to get_secure_cookie.
            """
            self.require_setting("cookie_secret", "secure cookies")
            return create_signed_value(self.application.settings["cookie_secret"],
                                       name, value)
    
        def get_secure_cookie(self, name, value=None, max_age_days=31):
            """Returns the given signed cookie if it validates, or None.
    
            The decoded cookie value is returned as a byte string (unlike
            `get_cookie`).
            """
            self.require_setting("cookie_secret", "secure cookies")
            if value is None:
                value = self.get_cookie(name)
            return decode_signed_value(self.application.settings["cookie_secret"],
                                       name, value, max_age_days=max_age_days)
    
        def redirect(self, url, permanent=False, status=None):
            """Sends a redirect to the given (optionally relative) URL.
    
            If the ``status`` argument is specified, that value is used as the
            HTTP status code; otherwise either 301 (permanent) or 302
            (temporary) is chosen based on the ``permanent`` argument.
            The default is 302 (temporary).
            """
            if self._headers_written:
                raise Exception("Cannot redirect after headers have been written")
            if status is None:
                status = 301 if permanent else 302
            else:
                assert isinstance(status, types.IntType) and 300 <= status <= 399
            self.set_status(status)
            # Remove whitespace
            url = re.sub(r"[\x00-\x20]+", "", utf8(url))
            self.set_header("Location", urlparse.urljoin(utf8(self.request.uri),
                                                         url))
            self.finish()
    
        def write(self, chunk):
            """Writes the given chunk to the output buffer.
    
            To write the output to the network, use the flush() method below.
    
            If the given chunk is a dictionary, we write it as JSON and set
            the Content-Type of the response to be application/json.
            (if you want to send JSON as a different Content-Type, call
            set_header *after* calling write()).
    
            Note that lists are not converted to JSON because of a potential
            cross-site security vulnerability.  All JSON output should be
            wrapped in a dictionary.  More details at
            http://haacked.com/archive/2008/11/20/\
                anatomy-of-a-subtle-json-vulnerability.aspx
            """
            if self._finished:
                raise RuntimeError("Cannot write() after finish().  May be caused "
                                   "by using async operations without the "
                                   "@asynchronous decorator.")
            if isinstance(chunk, types.DictType) or \
                    (self.serialize_lists and isinstance(chunk, types.ListType)):
                chunk = escape.json_encode(chunk)
                self.set_header("Content-Type", "application/json")
            chunk = utf8(chunk)
            self._write_buffer.append(chunk)
    
        def render(self, template_name, **kwargs):
            """Renders the template with the given arguments as the response."""
            html = self.render_string(template_name, **kwargs)
    
            # Insert the additional JS and CSS added by the modules on the page
            js_embed = []
            js_files = []
            css_embed = []
            css_files = []
            html_heads = []
            html_bodies = []
            for module in getattr(self, "_active_modules", {}).values():
                embed_part = module.embedded_javascript()
                if embed_part:
                    js_embed.append(utf8(embed_part))
                file_part = module.javascript_files()
                if file_part:
                    if isinstance(file_part, (unicode_type, bytes_type)):
                        js_files.append(file_part)
                    else:
                        js_files.extend(file_part)
                embed_part = module.embedded_css()
                if embed_part:
                    css_embed.append(utf8(embed_part))
                file_part = module.css_files()
                if file_part:
                    if isinstance(file_part, (unicode_type, bytes_type)):
                        css_files.append(file_part)
                    else:
                        css_files.extend(file_part)
                head_part = module.html_head()
                if head_part:
                    html_heads.append(utf8(head_part))
                body_part = module.html_body()
                if body_part:
                    html_bodies.append(utf8(body_part))
    
            def is_absolute(path):
                return any(path.startswith(x) for x in ["/", "http:", "https:"])
            if js_files:
                # Maintain order of JavaScript files given by modules
                paths = []
                unique_paths = set()
                for path in js_files:
                    if not is_absolute(path):
                        path = self.static_url(path)
                    if path not in unique_paths:
                        paths.append(path)
                        unique_paths.add(path)
                js = ''.join(''
                             for p in paths)
                sloc = html.rindex('')
                html = html[:sloc] + utf8(js) + '\n' + html[sloc:]
            if js_embed:
                js = ''
                sloc = html.rindex('')
                html = html[:sloc] + js + '\n' + html[sloc:]
            if css_files:
                paths = []
                unique_paths = set()
                for path in css_files:
                    if not is_absolute(path):
                        path = self.static_url(path)
                    if path not in unique_paths:
                        paths.append(path)
                        unique_paths.add(path)
                css = ''.join(''
                              for p in paths)
                hloc = html.index('')
                html = html[:hloc] + utf8(css) + '\n' + html[hloc:]
            if css_embed:
                css = ''
                hloc = html.index('')
                html = html[:hloc] + css + '\n' + html[hloc:]
            if html_heads:
                hloc = html.index('')
                html = html[:hloc] + ''.join(html_heads) + '\n' + html[hloc:]
            if html_bodies:
                hloc = html.index('')
                html = html[:hloc] + ''.join(html_bodies) + '\n' + html[hloc:]
            self.finish(html)
    
        def render_string(self, template_name, **kwargs):
            """Generate the given template with the given arguments.
    
            We return the generated string. To generate and write a template
            as a response, use render() above.
            """
            # If no template_path is specified, use the path of the calling file
            template_path = self.get_template_path()
            if not template_path:
                frame = sys._getframe(0)
                web_file = frame.f_code.co_filename
                while frame.f_code.co_filename == web_file:
                    frame = frame.f_back
                template_path = os.path.dirname(frame.f_code.co_filename)
            with RequestHandler._template_loader_lock:
                if template_path not in RequestHandler._template_loaders:
                    loader = self.create_template_loader(template_path)
                    RequestHandler._template_loaders[template_path] = loader
                else:
                    loader = RequestHandler._template_loaders[template_path]
            t = loader.load(template_name)
            namespace = self.get_template_namespace()
            namespace.update(kwargs)
            return t.generate(**namespace)
    
        def get_template_namespace(self):
            """Returns a dictionary to be used as the default template namespace.
    
            May be overridden by subclasses to add or modify values.
    
            The results of this method will be combined with additional
            defaults in the `tornado.template` module and keyword arguments
            to `render` or `render_string`.
            """
            namespace = dict(
                handler=self,
                request=self.request,
                current_user=self.current_user,
                locale=self.locale,
                _=self.locale.translate,
                static_url=self.static_url,
                xsrf_form_html=self.xsrf_form_html,
                reverse_url=self.reverse_url
            )
            namespace.update(self.ui)
            return namespace
    
        def create_template_loader(self, template_path):
            """Returns a new template loader for the given path.
    
            May be overridden by subclasses.  By default returns a
            directory-based loader on the given path, using the
            ``autoescape`` application setting.  If a ``template_loader``
            application setting is supplied, uses that instead.
            """
            settings = self.application.settings
            if "template_loader" in settings:
                return settings["template_loader"]
            kwargs = {}
            if "autoescape" in settings:
                # autoescape=None means "no escaping", so we have to be sure
                # to only pass this kwarg if the user asked for it.
                kwargs["autoescape"] = settings["autoescape"]
            return template.Loader(template_path, **kwargs)
    
        def flush(self, include_footers=False):
            """Flushes the current output buffer to the network."""
            chunk = "".join(self._write_buffer)
            self._write_buffer = []
            if not self._headers_written:
                self._headers_written = True
                for transform in self._transforms:
                    self._status_code, self._headers, chunk = \
                        transform.transform_first_chunk(
                        self._status_code, self._headers, chunk, include_footers)
                headers = self._generate_headers()
            else:
                for transform in self._transforms:
                    chunk = transform.transform_chunk(chunk, include_footers)
                headers = ""
    
            # Ignore the chunk and only write the headers for HEAD requests
            if self.request.method == "HEAD":
                if headers:
                    self.request.write(headers)
                return
    
            if headers or chunk:
                self.request.write(headers + chunk)
    
        def notifyFinish(self):
            """Returns a deferred, which is fired when the request is terminated
            and the connection is closed.
            """
            return self.request.notifyFinish()
    
        def finish(self, chunk=None):
            """Finishes this response, ending the HTTP request."""
            if self._finished:
                raise RuntimeError("finish() called twice.  May be caused "
                                   "by using async operations without the "
                                   "@asynchronous decorator.")
    
            if chunk is not None:
                self.write(chunk)
    
            # Automatically support ETags and add the Content-Length header if
            # we have not flushed any content yet.
            if not self._headers_written:
                if (self._status_code == 200 and
                    self.request.method in ("GET", "HEAD") and
                    "Etag" not in self._headers):
                    etag = self.compute_etag()
                    if etag is not None:
                        self.set_header("Etag", etag)
                        inm = self.request.headers.get("If-None-Match")
                        if inm and inm.find(etag) != -1:
                            self._write_buffer = []
                            self.set_status(304)
                if self._status_code == 304:
                    assert not self._write_buffer, "Cannot send body with 304"
                    self._clear_headers_for_304()
                elif "Content-Length" not in self._headers:
                    content_length = sum(len(part) for part in self._write_buffer)
                    self.set_header("Content-Length", content_length)
    
            self.flush(include_footers=True)
            self.request.finish()
            self._log()
            self._finished = True
            self.on_finish()
    
        def send_error(self, status_code=500, **kwargs):
            """Sends the given HTTP error code to the browser.
    
            If `flush()` has already been called, it is not possible to send
            an error, so this method will simply terminate the response.
            If output has been written but not yet flushed, it will be discarded
            and replaced with the error page.
    
            Override `write_error()` to customize the error page that is returned.
            Additional keyword arguments are passed through to `write_error`.
            """
            if self._headers_written:
                log.msg("Cannot send error response after headers written")
                if not self._finished:
                    self.finish()
                return
            self.clear()
    
            reason = None
            if "exc_info" in kwargs:
                e = kwargs["exc_info"][1]
                if isinstance(e, HTTPError) and e.reason:
                    reason = e.reason
            elif "exception" in kwargs:
                e = kwargs["exception"]
                if isinstance(e, HTTPAuthenticationRequired):
                    args = ",".join(['%s="%s"' % (k, v)
                                     for k, v in e.kwargs.items()])
                    self.set_header("WWW-Authenticate", "%s %s" %
                                    (e.auth_type, args))
    
            self.set_status(status_code, reason=reason)
            try:
                self.write_error(status_code, **kwargs)
            except Exception, e:
                log.msg("Uncaught exception in write_error: " + str(e))
            if not self._finished:
                self.finish()
    
        def write_error(self, status_code, **kwargs):
            """Override to implement custom error pages.
    
            ``write_error`` may call `write`, `render`, `set_header`, etc
            to produce output as usual.
    
            If this error was caused by an uncaught exception (including
            HTTPError), an ``exc_info`` triple will be available as
            ``kwargs["exc_info"]``.  Note that this exception may not be
            the "current" exception for purposes of methods like
            ``sys.exc_info()`` or ``traceback.format_exc``.
    
            For historical reasons, if a method ``get_error_html`` exists,
            it will be used instead of the default ``write_error`` implementation.
            ``get_error_html`` returned a string instead of producing output
            normally, and had different semantics for exception handling.
            Users of ``get_error_html`` are encouraged to convert their code
            to override ``write_error`` instead.
            """
            if hasattr(self, 'get_error_html'):
                if 'exc_info' in kwargs:
                    exc_info = kwargs.pop('exc_info')
                    kwargs['exception'] = exc_info[1]
                    try:
                        # Put the traceback into sys.exc_info()
                        raise exc_info[0], exc_info[1], exc_info[2]
                    except Exception:
                        self.finish(self.get_error_html(status_code, **kwargs))
                else:
                    self.finish(self.get_error_html(status_code, **kwargs))
                return
            if self.settings.get("debug") and "exc_info" in kwargs:
                # in debug mode, try to send a traceback
                self.set_header('Content-Type', 'text/plain')
                for line in traceback.format_exception(*kwargs["exc_info"]):
                    self.write(line)
                self.finish()
            else:
                self.finish("%(code)d: %(message)s"
                            "%(code)d: %(message)s" %
                            {"code": status_code, "message": self._reason})
    
        @property
        def locale(self):
            """The local for the current session.
    
            Determined by either get_user_locale, which you can override to
            set the locale based on, e.g., a user preference stored in a
            database, or get_browser_locale, which uses the Accept-Language
            header.
            """
            if not hasattr(self, "_locale"):
                self._locale = self.get_user_locale()
                if not self._locale:
                    self._locale = self.get_browser_locale()
                    assert self._locale
            return self._locale
    
        def get_user_locale(self):
            """Override to determine the locale from the authenticated user.
    
            If None is returned, we fall back to get_browser_locale().
    
            This method should return a cyclone.locale.Locale object,
            most likely obtained via a call like cyclone.locale.get("en")
            """
            return None
    
        def get_browser_locale(self, default="en_US"):
            """Determines the user's locale from Accept-Language header.
    
            See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
            """
            if "Accept-Language" in self.request.headers:
                languages = self.request.headers["Accept-Language"].split(",")
                locales = []
                for language in languages:
                    parts = language.strip().split(";")
                    if len(parts) > 1 and parts[1].startswith("q="):
                        try:
                            score = float(parts[1][2:])
                        except (ValueError, TypeError):
                            score = 0.0
                    else:
                        score = 1.0
                    locales.append((parts[0], score))
                if locales:
                    locales.sort(key=lambda pair: pair[1], reverse=True)
                    codes = [l[0] for l in locales]
                    return locale.get(*codes)
            return locale.get(default)
    
        @property
        def current_user(self):
            """The authenticated user for this request.
    
            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
    
        def get_login_url(self):
            """Override to customize the login URL based on the request.
    
            By default, we use the 'login_url' application setting.
            """
            self.require_setting("login_url", "@cyclone.web.authenticated")
            return self.application.settings["login_url"]
    
        def get_template_path(self):
            """Override to customize template path for each handler.
    
            By default, we use the 'template_path' application setting.
            Return None to load templates relative to the calling file.
            """
            return self.application.settings.get("template_path")
    
        @property
        def xsrf_token(self):
            """The XSRF-prevention token for the current user/session.
    
            To prevent cross-site request forgery, we set an '_xsrf' cookie
            and include the same '_xsrf' value as an argument with all POST
            requests. If the two do not match, we reject the form submission
            as a potential forgery.
    
            See http://en.wikipedia.org/wiki/Cross-site_request_forgery
            """
            if not hasattr(self, "_xsrf_token"):
                token = self.get_cookie(self.xsrf_cookie_name)
                if not token:
                    token = binascii.b2a_hex(uuid.uuid4().bytes)
                    expires_days = 30 if self.current_user else None
                    self.set_cookie(self.xsrf_cookie_name, token, expires_days=expires_days)
                self._xsrf_token = token
            return self._xsrf_token
    
        def check_xsrf_cookie(self):
            """Verifies that the '_xsrf' cookie matches the '_xsrf' argument.
    
            To prevent cross-site request forgery, we set an '_xsrf'
            cookie and include the same value as a non-cookie
            field with all POST requests. If the two do not match, we
            reject the form submission as a potential forgery.
    
            The _xsrf value may be set as either a form field named _xsrf
            or in a custom HTTP header named X-XSRFToken or X-CSRFToken
            (the latter is accepted for compatibility with Django).
    
            See http://en.wikipedia.org/wiki/Cross-site_request_forgery
    
            Prior to release 1.1.1, this check was ignored if the HTTP header
            "X-Requested-With: XMLHTTPRequest" was present.  This exception
            has been shown to be insecure and has been removed.  For more
            information please see
            http://www.djangoproject.com/weblog/2011/feb/08/security/
            http://weblog.rubyonrails.org/2011/2/8/\
                csrf-protection-bypass-in-ruby-on-rails
            """
            token = (self.get_argument(self.xsrf_cookie_name, None) or
                     self.request.headers.get("X-Xsrftoken") or
                     self.request.headers.get("X-Csrftoken"))
            if not token:
                raise HTTPError(403, "'_xsrf' argument missing from POST")
            if self.xsrf_token != token:
                raise HTTPError(403, "XSRF cookie does not match POST argument")
    
        def xsrf_form_html(self):
            """An HTML  element to be included with all POST forms.
    
            It defines the _xsrf input value, which we check on all POST
            requests to prevent cross-site request forgery. If you have set
            the 'xsrf_cookies' application setting, you must include this
            HTML within all of your HTML forms.
    
            See check_xsrf_cookie() above for more information.
            """
            return ''
    
        def static_url(self, path, include_host=None):
            """Returns a static URL for the given relative static file path.
    
            This method requires you set the 'static_path' setting in your
            application (which specifies the root directory of your static
            files).
    
            We append ?v= to the returned URL, which makes our
            static file handler set an infinite expiration header on the
            returned content. The signature is based on the content of the
            file.
    
            By default this method returns URLs relative to the current
            host, but if ``include_host`` is true the URL returned will be
            absolute.  If this handler has an ``include_host`` attribute,
            that value will be used as the default for all `static_url`
            calls that do not pass ``include_host`` as a keyword argument.
            """
            self.require_setting("static_path", "static_url")
            static_handler_class = self.settings.get(
                "static_handler_class", StaticFileHandler)
    
            if include_host is None:
                include_host = getattr(self, "include_host", False)
    
            if include_host:
                base = self.request.protocol + "://" + self.request.host + \
                       static_handler_class.make_static_url(self.settings, path)
            else:
                base = static_handler_class.make_static_url(self.settings, path)
            return base
    
        def async_callback(self, callback, *args, **kwargs):
            """Obsolete - catches exceptions from the wrapped function.
    
            This function is unnecessary since Tornado 1.1.
            """
            if callback is None:
                return None
            if args or kwargs:
                callback = functools.partial(callback, *args, **kwargs)
    
            def wrapper(*args, **kwargs):
                try:
                    return callback(*args, **kwargs)
                except Exception, e:
                    if self._headers_written:
                        log.msg("Exception after headers written: " + e)
                    else:
                        self._handle_request_exception(e)
            return wrapper
    
        def require_setting(self, name, feature="this feature"):
            """Raises an exception if the given app setting is not defined."""
            if not self.application.settings.get(name):
                raise Exception("You must define the '%s' setting in your "
                                "application to use %s" % (name, feature))
    
        def reverse_url(self, name, *args):
            """Alias for `Application.reverse_url`."""
            return self.application.reverse_url(name, *args)
    
        def compute_etag(self):
            """Computes the etag header to be used for this request.
    
            May be overridden to provide custom etag implementations,
            or may return None to disable cyclone's default etag support.
            """
            hasher = hashlib.sha1()
            for part in self._write_buffer:
                hasher.update(part)
            return '"' + hasher.hexdigest() + '"'
    
        def _execute(self, transforms, *args, **kwargs):
            """Executes this request with the given output transforms."""
            self._transforms = transforms
            try:
                if self.request.method not in self.SUPPORTED_METHODS:
                    raise HTTPError(405)
                self.path_args = [self.decode_argument(arg) for arg in args]
                self.path_kwargs = dict((k, self.decode_argument(v, name=k))
                                        for (k, v) in kwargs.items())
                # If XSRF cookies are turned on, reject form submissions without
                # the proper cookie
                if self.request.method not in ("GET", "HEAD", "OPTIONS") and \
                        self.application.settings.get("xsrf_cookies"):  # is True
                    if not getattr(self, "no_xsrf", False):
                        self.check_xsrf_cookie()
                defer.maybeDeferred(self.prepare).addCallbacks(
                        self._execute_handler,
                        lambda f: self._handle_request_exception(f.value),
                        callbackArgs=(args, kwargs))
            except Exception, e:
                self._handle_request_exception(e)
    
        def _deferred_handler(self, function, *args, **kwargs):
            try:
                result = function(*args, **kwargs)
            except:
                return defer.fail(failure.Failure(
                                  captureVars=defer.Deferred.debug))
            else:
                if isinstance(result, defer.Deferred):
                    return result
                elif isinstance(result, types.GeneratorType):
                    # This may degrade performance a bit, but at least avoid the
                    # server from breaking when someone call yield without
                    # decorating their handler with @inlineCallbacks.
                    log.msg("[warning] %s.%s() returned a generator. "
                            "Perhaps it should be decorated with "
                            "@inlineCallbacks." % (self.__class__.__name__,
                                                   self.request.method.lower()))
                    return self._deferred_handler(defer.inlineCallbacks(function),
                                                  *args, **kwargs)
                elif isinstance(result, failure.Failure):
                    return defer.fail(result)
                else:
                    return defer.succeed(result)
    
        def _execute_handler(self, r, args, kwargs):
            if not self._finished:
                args = [self.decode_argument(arg) for arg in args]
                kwargs = dict((k, self.decode_argument(v, name=k))
                                for (k, v) in kwargs.iteritems())
                function = getattr(self, self.request.method.lower())
                #d = defer.maybeDeferred(function, *args, **kwargs)
                d = self._deferred_handler(function, *args, **kwargs)
                d.addCallbacks(self._execute_success, self._execute_failure)
                self.notifyFinish().addCallback(self.on_connection_close)
    
        def _execute_success(self, ign):
            if self._auto_finish and not self._finished:
                return self.finish()
    
        def _execute_failure(self, err):
            return self._handle_request_exception(err)
    
        def _generate_headers(self):
            reason = self._reason
            lines = [utf8(self.request.version + " " +
                          str(self._status_code) +
                          " " + reason)]
            lines.extend([(utf8(n) + ": " + utf8(v)) for n, v in
                      itertools.chain(self._headers.items(), self._list_headers)])
            if hasattr(self, "_new_cookie"):
                for cookie in self._new_cookie.values():
                    lines.append(utf8("Set-Cookie: " + cookie.OutputString(None)))
            return "\r\n".join(lines) + "\r\n\r\n"
    
        def _log(self):
            """Logs the current request.
    
            Sort of deprecated since this functionality was moved to the
            Application, but left in place for the benefit of existing apps
            that have overridden this method.
            """
            self.application.log_request(self)
    
        def _request_summary(self):
            return self.request.method + " " + self.request.uri + " (" + \
                    self.request.remote_ip + ")"
    
        def _handle_request_exception(self, e):
            try:
                # These are normally twisted.python.failure.Failure
                if isinstance(e.value, (template.TemplateError,
                                        HTTPError, HTTPAuthenticationRequired)):
                    e = e.value
            except:
                pass
    
            if isinstance(e, template.TemplateError):
                log.msg(str(e))
                self.send_error(500, exception=e)
            elif isinstance(e, (HTTPError, HTTPAuthenticationRequired)):
                if e.log_message and self.settings.get("debug") is True:
                    log.msg(str(e))
    
                if e.status_code not in httplib.responses:
                    log.msg("Bad HTTP status code: " + repr(e.status_code))
                    e.status_code = 500
    
                self.send_error(e.status_code, exception=e)
            else:
                log.msg("Uncaught exception\n" + str(e))
                if self.settings.get("debug"):
                    log.msg(repr(self.request))
    
                self.send_error(500, exception=e)
    
        def _ui_module(self, name, module):
            def render(*args, **kwargs):
                if not hasattr(self, "_active_modules"):
                    self._active_modules = {}
                if name not in self._active_modules:
                    self._active_modules[name] = module(self)
                rendered = self._active_modules[name].render(*args, **kwargs)
                return rendered
            return render
    
        def _ui_method(self, method):
            return lambda *args, **kwargs: method(self, *args, **kwargs)
    
        def _clear_headers_for_304(self):
            # 304 responses should not contain entity headers (defined in
            # http://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.1)
            # not explicitly allowed by
            # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
            headers = ["Allow", "Content-Encoding", "Content-Language",
                       "Content-Length", "Content-MD5", "Content-Range",
                       "Content-Type", "Last-Modified"]
            for h in headers:
                self.clear_header(h)
    
    
    def asynchronous(method):
        """Wrap request handler methods with this if they are asynchronous.
    
        If this decorator is given, the response is not finished when the
        method returns. It is up to the request handler to call self.finish()
        to terminate the HTTP request. Without this decorator, the request is
        automatically finished when the get() or post() method returns. ::
    
            from twisted.internet import reactor
    
            class MyRequestHandler(web.RequestHandler):
                @web.asynchronous
                def get(self):
                    self.write("Processing your request...")
                    reactor.callLater(5, self.do_something)
    
                def do_something(self):
                    self.finish("done!")
    
        It may be used for Comet and similar push techniques.
        http://en.wikipedia.org/wiki/Comet_(programming)
        """
        @functools.wraps(method)
        def wrapper(self, *args, **kwargs):
            self._auto_finish = False
            return method(self, *args, **kwargs)
        return wrapper
    
    
    def removeslash(method):
        """Use this decorator to remove trailing slashes from the request path.
    
        For example, a request to ``'/foo/'`` would redirect to ``'/foo'`` with
        this decorator. Your request handler mapping should use a regular
        expression like ``r'/foo/*'`` in conjunction with using the decorator.
        """
        @functools.wraps(method)
        def wrapper(self, *args, **kwargs):
            if self.request.path.endswith("/"):
                if self.request.method in ("GET", "HEAD", "POST", "PUT", "DELETE"):
                    uri = self.request.path.rstrip("/")
                    if uri:  # don't try to redirect '/' to ''
                        if self.request.query:
                            uri = uri + "?" + self.request.query
                    self.redirect(uri, permanent=True)
                    return
                else:
                    raise HTTPError(404)
            return method(self, *args, **kwargs)
        return wrapper
    
    
    def addslash(method):
        """Use this decorator to add a missing trailing slash to the request path.
    
        For example, a request to '/foo' would redirect to '/foo/' with this
        decorator. Your request handler mapping should use a regular expression
        like r'/foo/?' in conjunction with using the decorator.
        """
        @functools.wraps(method)
        def wrapper(self, *args, **kwargs):
            if not self.request.path.endswith("/"):
                if self.request.method in ("GET", "HEAD", "POST", "PUT", "DELETE"):
                    uri = self.request.path + "/"
                    if self.request.query:
                        uri = uri + "?" + self.request.query
                    self.redirect(uri, permanent=True)
                    return
                raise HTTPError(404)
            return method(self, *args, **kwargs)
        return wrapper
    
    
    class Application(protocol.ServerFactory):
        """A collection of request handlers that make up a web application.
    
        Instances of this class are callable and can be passed directly to
        HTTPServer to serve the application::
    
            application = web.Application([
                (r"/", MainPageHandler),
            ])
            reactor.listenTCP(8888, application)
            reactor.run()
    
        The constructor for this class takes in a list of URLSpec objects
        or (regexp, request_class) tuples. When we receive requests, we
        iterate over the list in order and instantiate an instance of the
        first request class whose regexp matches the request path.
    
        Each tuple can contain an optional third element, which should be a
        dictionary if it is present. That dictionary is passed as keyword
        arguments to the contructor of the handler. This pattern is used
        for the StaticFileHandler below (note that a StaticFileHandler
        can be installed automatically with the static_path setting described
        below)::
    
            application = web.Application([
                (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
            ])
    
        We support virtual hosts with the add_handlers method, which takes in
        a host regular expression as the first argument::
    
            application.add_handlers(r"www\.myhost\.com", [
                (r"/article/([0-9]+)", ArticleHandler),
            ])
    
        You can serve static files by sending the static_path setting as a
        keyword argument. We will serve those files from the /static/ URI
        (this is configurable with the static_url_prefix setting),
        and we will serve /favicon.ico and /robots.txt from the same directory.
        A custom subclass of StaticFileHandler can be specified with the
        static_handler_class setting.
    
        .. attribute:: settings
    
           Additonal keyword arguments passed to the constructor are saved in the
           `settings` dictionary, and are often referred to in documentation as
           "application settings".
        """
        protocol = httpserver.HTTPConnection
    
        def __init__(self, handlers=None, default_host="",
                     transforms=None, **settings):
            if transforms is None:
                self.transforms = []
                if settings.get("gzip"):
                    self.transforms.append(GZipContentEncoding)
                self.transforms.append(ChunkedTransferEncoding)
            else:
                self.transforms = transforms
            self.handlers = []
            self.named_handlers = {}
            self.default_host = default_host
            self.settings = ObjectDict(settings)
            self.ui_modules = {"linkify": _linkify,
                               "xsrf_form_html": _xsrf_form_html,
                               "Template": TemplateModule}
            self.ui_methods = {}
            self._load_ui_modules(settings.get("ui_modules", {}))
            self._load_ui_methods(settings.get("ui_methods", {}))
            if "static_path" in self.settings:
                path = self.settings["static_path"]
                handlers = list(handlers or [])
                static_url_prefix = settings.get("static_url_prefix",
                                                 "/static/")
                static_handler_class = settings.get("static_handler_class",
                                                    StaticFileHandler)
                static_handler_args = settings.get("static_handler_args", {})
                static_handler_args["path"] = path
                for pattern in [re.escape(static_url_prefix) + r"(.*)",
                                r"/(favicon\.ico)", r"/(robots\.txt)"]:
                    handlers.insert(0, (pattern, static_handler_class,
                                        static_handler_args))
            if handlers:
                self.add_handlers(".*$", handlers)
    
        def add_handlers(self, host_pattern, host_handlers):
            """Appends the given handlers to our handler list.
    
            Host patterns are processed sequentially in the order they were
            added. All matching patterns will be considered.
            """
            if not host_pattern.endswith("$"):
                host_pattern += "$"
            handlers = []
            # The handlers with the wildcard host_pattern are a special
            # case - they're added in the constructor but should have lower
            # precedence than the more-precise handlers added later.
            # If a wildcard handler group exists, it should always be last
            # in the list, so insert new groups just before it.
            if self.handlers and self.handlers[-1][0].pattern == '.*$':
                self.handlers.insert(-1, (re.compile(host_pattern), handlers))
            else:
                self.handlers.append((re.compile(host_pattern), handlers))
    
            for spec in host_handlers:
                if isinstance(spec, types.TupleType):
                    assert len(spec) in (2, 3)
                    pattern = spec[0]
                    handler = spec[1]
    
                    if isinstance(handler, types.StringType):
                        # import the Module and instantiate the class
                        # Must be a fully qualified name (module.ClassName)
                        try:
                            handler = import_object(handler)
                        except ImportError, e:
                            reactor.callWhenRunning(log.msg,
                                "Unable to load handler '%s' for "
                                "'%s': %s" % (handler, pattern, e))
                            continue
    
                    if len(spec) == 3:
                        kwargs = spec[2]
                    else:
                        kwargs = {}
                    spec = URLSpec(pattern, handler, kwargs)
                handlers.append(spec)
                if spec.name:
                    if spec.name in self.named_handlers:
                        log.msg("Multiple handlers named %s; "
                                "replacing previous value" % spec.name)
                    self.named_handlers[spec.name] = spec
    
        def add_transform(self, transform_class):
            """Adds the given OutputTransform to our transform list."""
            self.transforms.append(transform_class)
    
        def _get_host_handlers(self, request):
            host = request.host.lower().split(':')[0]
            matches = []
            for pattern, handlers in self.handlers:
                if pattern.match(host):
                    matches.extend(handlers)
            # Look for default host if not behind load balancer (for debugging)
            if not matches and "X-Real-Ip" not in request.headers:
                for pattern, handlers in self.handlers:
                    if pattern.match(self.default_host):
                        matches.extend(handlers)
            return matches or None
    
        def _load_ui_methods(self, methods):
            if isinstance(methods, types.ModuleType):
                self._load_ui_methods(dict((n, getattr(methods, n))
                                           for n in dir(methods)))
            elif isinstance(methods, types.ListType):
                for m in methods:
                    self._load_ui_methods(m)
            else:
                for name, fn in methods.items():
                    if not name.startswith("_") and hasattr(fn, "__call__") \
                       and name[0].lower() == name[0]:
                        self.ui_methods[name] = fn
    
        def _load_ui_modules(self, modules):
            if isinstance(modules, types.ModuleType):
                self._load_ui_modules(dict((n, getattr(modules, n))
                                           for n in dir(modules)))
            elif isinstance(modules, types.ListType):
                for m in modules:
                    self._load_ui_modules(m)
            else:
                assert isinstance(modules, types.DictType)
                for name, cls in modules.items():
                    try:
                        if issubclass(cls, UIModule):
                            self.ui_modules[name] = cls
                    except TypeError:
                        pass
    
        def __call__(self, request):
            """Called by HTTPServer to execute the request."""
            transforms = [t(request) for t in self.transforms]
            handler = None
            args = []
            kwargs = {}
            handlers = self._get_host_handlers(request)
            if not handlers:
                handler = RedirectHandler(self, request,
                                          url="http://" + self.default_host + "/")
            else:
                for spec in handlers:
                    match = spec.regex.match(request.path)
                    if match:
                        handler = spec.handler_class(self, request, **spec.kwargs)
                        if spec.regex.groups:
                            # None-safe wrapper around url_unescape to handle
                            # unmatched optional groups correctly
                            def unquote(s):
                                if s is None:
                                    return s
                                return escape.url_unescape(s, encoding=None)
                            # Pass matched groups to the handler.  Since
                            # match.groups() includes both named and
                            # unnamed groups,we want to use either groups
                            # or groupdict but not both.
                            # Note that args are passed as bytes so the handler can
                            # decide what encoding to use.
    
                            if spec.regex.groupindex:
                                kwargs = dict((str(k), unquote(v))
                                    for (k, v) in match.groupdict().items())
                            else:
                                args = [unquote(s) for s in match.groups()]
                        break
                if not handler:
                    handler = ErrorHandler(self, request, status_code=404)
    
            # In debug mode, re-compile templates and reload static files on every
            # request so you don't need to restart to see changes
            if self.settings.get("debug"):
                with RequestHandler._template_loader_lock:
                    for loader in RequestHandler._template_loaders.values():
                        loader.reset()
                StaticFileHandler.reset()
    
            handler._execute(transforms, *args, **kwargs)
            return handler
    
        def reverse_url(self, name, *args):
            """Returns a URL path for handler named `name`
    
            The handler must be added to the application as a named URLSpec.
    
            Args will be substituted for capturing groups in the URLSpec regex.
            They will be converted to strings if necessary, encoded as utf8,
            and url-escaped.
            """
            if name in self.named_handlers:
                return self.named_handlers[name].reverse(*args)
            raise KeyError("%s not found in named urls" % name)
    
        def log_request(self, handler):
            """Writes a completed HTTP request to the logs.
    
            By default writes to the python root logger.  To change
            this behavior either subclass Application and override this method,
            or pass a function in the application settings dictionary as
            'log_function'.
            """
            if "log_function" in self.settings:
                self.settings["log_function"](handler)
                return
    
            request_time = 1000.0 * handler.request.request_time()
            log.msg("[" + handler.request.protocol + "] " +
                    str(handler.get_status()) + " " + handler._request_summary() +
                    " %.2fms" % request_time)
    
    
    class HTTPError(Exception):
        """An exception that will turn into an HTTP error response.
    
        :arg int status_code: HTTP status code.  Must be listed in
            `httplib.responses` unless the ``reason`` keyword argument is given.
        :arg string log_message: Message to be written to the log for this error
            (will not be shown to the user unless the `Application` is in debug
            mode).  May contain ``%s``-style placeholders, which will be filled
            in with remaining positional parameters.
        :arg string reason: Keyword-only argument.  The HTTP "reason" phrase
            to pass in the status line along with ``status_code``.  Normally
            determined automatically from ``status_code``, but can be used
            to use a non-standard numeric code.
        """
        def __init__(self, status_code, log_message=None, *args, **kwargs):
            self.status_code = status_code
            self.log_message = log_message
            self.args = args
            self.reason = kwargs.get("reason", None)
    
        def __str__(self):
            if self.log_message:
                return self.log_message % self.args
            else:
                return self.reason or \
                       httplib.responses.get(self.status_code, "Unknown")
    
    
    class HTTPAuthenticationRequired(HTTPError):
        """An exception that will turn into an HTTP 401, Authentication Required.
    
        The arguments are used to compose the ``WWW-Authenticate`` header.
        See http://en.wikipedia.org/wiki/Basic_access_authentication for details.
    
        :arg string auth_type: Authentication type (``Basic``, ``Digest``, etc)
        :arg string realm: Realm (Usually displayed by the browser)
        """
        def __init__(self, log_message=None,
                     auth_type="Basic", realm="Restricted Access", **kwargs):
            self.status_code = 401
            self.log_message = log_message
            self.auth_type = auth_type
            self.kwargs = kwargs
            self.kwargs["realm"] = realm
    
    
    class ErrorHandler(RequestHandler):
        """Generates an error response with status_code for all requests."""
        def initialize(self, status_code):
            self.set_status(status_code)
    
        def prepare(self):
            raise HTTPError(self._status_code)
    
        def check_xsrf_cookie(self):
            # POSTs to an ErrorHandler don't actually have side effects,
            # so we don't need to check the xsrf token.  This allows POSTs
            # to the wrong url to return a 404 instead of 403.
            pass
    
    
    class RedirectHandler(RequestHandler):
        """Redirects the client to the given URL for all GET requests.
    
        You should provide the keyword argument "url" to the handler, e.g.::
    
            application = web.Application([
                (r"/oldpath", web.RedirectHandler, {"url": "/newpath"}),
            ])
        """
        def initialize(self, url, permanent=True):
            self._url = url
            self._permanent = permanent
    
        def get(self):
            self.redirect(self._url, permanent=self._permanent)
    
    
    class StaticFileHandler(RequestHandler):
        """A simple handler that can serve static content from a directory.
    
        To map a path to this handler for a static data directory /var/www,
        you would add a line to your application like::
    
            application = web.Application([
                (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
            ])
    
        The local root directory of the content should be passed as the "path"
        argument to the handler.
    
        To support aggressive browser caching, if the argument "v" is given
        with the path, we set an infinite HTTP expiration header. So, if you
        want browsers to cache a file indefinitely, send them to, e.g.,
        /static/images/myimage.png?v=xxx. Override ``get_cache_time`` method for
        more fine-grained cache control.
        """
        CACHE_MAX_AGE = 86400 * 365 * 10  # 10 years
    
        _static_hashes = {}
        _lock = threading.Lock()  # protects _static_hashes
    
        def initialize(self, path, default_filename=None):
            self.root = "%s%s" % (os.path.abspath(path), os.path.sep)
            self.default_filename = default_filename
    
        @classmethod
        def reset(cls):
            with cls._lock:
                cls._static_hashes = {}
    
        def head(self, path):
            self.get(path, include_body=False)
    
        def get(self, path, include_body=True):
            path = self.parse_url_path(path)
            abspath = os.path.abspath(os.path.join(self.root, path))
            # os.path.abspath strips a trailing /
            # it needs to be temporarily added back for requests to root/
            if not (abspath + os.path.sep).startswith(self.root):
                raise HTTPError(403, "%s is not in root static directory", path)
            if os.path.isdir(abspath) and self.default_filename is not None:
                # need to look at the request.path here for when path is empty
                # but there is some prefix to the path that was already
                # trimmed by the routing
                if not self.request.path.endswith("/"):
                    self.redirect("%s/" % self.request.path)
                abspath = os.path.join(abspath, self.default_filename)
            if not os.path.exists(abspath):
                raise HTTPError(404)
            if not os.path.isfile(abspath):
                raise HTTPError(403, "%s is not a file", path)
    
            stat_result = os.stat(abspath)
            modified = datetime.datetime.fromtimestamp(stat_result[stat.ST_MTIME])
    
            self.set_header("Last-Modified", modified)
    
            mime_type, encoding = mimetypes.guess_type(abspath)
            if mime_type:
                self.set_header("Content-Type", mime_type)
    
            cache_time = self.get_cache_time(path, modified, mime_type)
    
            if cache_time > 0:
                self.set_header("Expires", "%s%s" % (datetime.datetime.utcnow(),
                                           datetime.timedelta(seconds=cache_time)))
                self.set_header("Cache-Control", "max-age=%s" % str(cache_time))
    
            self.set_extra_headers(path)
    
            # Check the If-Modified-Since, and don't send the result if the
            # content has not been modified
            ims_value = self.request.headers.get("If-Modified-Since")
            if ims_value is not None:
                date_tuple = email.utils.parsedate(ims_value)
                if_since = datetime.datetime.fromtimestamp(time.mktime(date_tuple))
                if if_since >= modified:
                    self.set_status(304)
                    return
    
            with open(abspath, "rb") as file:
                data = file.read()
                if include_body:
                    self.write(data)
                else:
                    assert self.request.method == "HEAD"
                    self.set_header("Content-Length", len(data))
    
        def set_extra_headers(self, path):
            """For subclass to add extra headers to the response"""
            pass
    
        def get_cache_time(self, path, modified, mime_type):
            """Override to customize cache control behavior.
    
            Return a positive number of seconds to trigger aggressive caching or 0
            to mark resource as cacheable, only.
    
            By default returns cache expiry of 10 years for resources requested
            with "v" argument.
            """
            return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0
    
        @classmethod
        def make_static_url(cls, settings, path):
            """Constructs a versioned url for the given path.
    
            This method may be overridden in subclasses (but note that it is
            a class method rather than an instance method).
    
            ``settings`` is the `Application.settings` dictionary.  ``path``
            is the static path being requested.  The url returned should be
            relative to the current host.
            """
            static_url_prefix = settings.get('static_url_prefix', '/static/')
            version_hash = cls.get_version(settings, path)
            if version_hash:
                return "%s%s?v=%s" % (static_url_prefix, path, version_hash)
            return "%s%s" % (static_url_prefix, path)
    
        @classmethod
        def get_version(cls, settings, path):
            """Generate the version string to be used in static URLs.
    
            This method may be overridden in subclasses (but note that it
            is a class method rather than a static method).  The default
            implementation uses a hash of the file's contents.
    
            ``settings`` is the `Application.settings` dictionary and ``path``
            is the relative location of the requested asset on the filesystem.
            The returned value should be a string, or ``None`` if no version
            could be determined.
            """
            abs_path = os.path.join(settings["static_path"], path)
            with cls._lock:
                hashes = cls._static_hashes
                if abs_path not in hashes:
                    try:
                        f = open(abs_path, "rb")
                        hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
                        f.close()
                    except Exception:
                        log.msg("Could not open static file %r" % path)
                        hashes[abs_path] = None
                hsh = hashes.get(abs_path)
                if hsh:
                    return hsh[:5]
            return None
    
        def parse_url_path(self, url_path):
            """Converts a static URL path into a filesystem path.
    
            ``url_path`` is the path component of the URL with
            ``static_url_prefix`` removed.  The return value should be
            filesystem path relative to ``static_path``.
            """
            if os.path.sep != "/":
                url_path = url_path.replace("/", os.path.sep)
            return url_path
    
    
    class FallbackHandler(RequestHandler):
        """A RequestHandler that wraps another HTTP server callback.
    
        Tornado has this to combine RequestHandlers and WSGI handlers, but it's
        not supported in cyclone and is just here for compatibily purposes.
        """
        def initialize(self, fallback):
            self.fallback = fallback
    
        def prepare(self):
            self.fallback(self.request)
            self._finished = True
    
    
    class OutputTransform(object):
        """A transform modifies the result of an HTTP request (e.g., GZip encoding)
    
        A new transform instance is created for every request. See the
        ChunkedTransferEncoding example below if you want to implement a
        new Transform.
        """
        def __init__(self, request):
            pass
    
        def transform_first_chunk(self, status_code, headers, chunk, finishing):
            return status_code, headers, chunk
    
        def transform_chunk(self, chunk, finishing):
            return chunk
    
    
    class GZipContentEncoding(OutputTransform):
        """Applies the gzip content encoding to the response.
    
        See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
        """
        CONTENT_TYPES = set([
            "text/plain", "text/html", "text/css", "text/xml",
            "application/javascript", "application/x-javascript",
            "application/xml", "application/atom+xml",
            "text/javascript", "application/json", "application/xhtml+xml"])
        MIN_LENGTH = 5
    
        def __init__(self, request):
            self._gzipping = request.supports_http_1_1() and \
                "gzip" in request.headers.get("Accept-Encoding", [])
    
        def transform_first_chunk(self, status_code, headers, chunk, finishing):
            if 'Vary' in headers:
                headers['Vary'] += ', Accept-Encoding'
            else:
                headers['Vary'] = 'Accept-Encoding'
            if self._gzipping:
                ctype = _unicode(headers.get("Content-Type", "")).split(";")[0]
                self._gzipping = (ctype in self.CONTENT_TYPES) and \
                    (not finishing or len(chunk) >= self.MIN_LENGTH) and \
                    (finishing or "Content-Length" not in headers) and \
                    ("Content-Encoding" not in headers)
            if self._gzipping:
                headers["Content-Encoding"] = "gzip"
                self._gzip_value = BytesIO()
                self._gzip_file = gzip.GzipFile(mode="w", fileobj=self._gzip_value)
                chunk = self.transform_chunk(chunk, finishing)
                if "Content-Length" in headers:
                    headers["Content-Length"] = str(len(chunk))
            return status_code, headers, chunk
    
        def transform_chunk(self, chunk, finishing):
            if self._gzipping:
                self._gzip_file.write(chunk)
                if finishing:
                    self._gzip_file.close()
                else:
                    self._gzip_file.flush()
                chunk = self._gzip_value.getvalue()
                self._gzip_value.truncate(0)
                self._gzip_value.seek(0)
            return chunk
    
    
    class ChunkedTransferEncoding(OutputTransform):
        """Applies the chunked transfer encoding to the response.
    
        See http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
        """
        def __init__(self, request):
            self._chunking = request.supports_http_1_1()
    
        def transform_first_chunk(self, status_code, headers, chunk, finishing):
            # 304 responses have no body (not even a zero-length body), and so
            # should not have either Content-Length or Transfer-Encoding headers.
            if self._chunking and status_code != 304:
                # No need to chunk the output if a Content-Length is specified
                if "Content-Length" in headers or "Transfer-Encoding" in headers:
                    self._chunking = False
                else:
                    headers["Transfer-Encoding"] = "chunked"
                    chunk = self.transform_chunk(chunk, finishing)
            return status_code, headers, chunk
    
        def transform_chunk(self, block, finishing):
            if self._chunking:
                # Don't write out empty chunks because that means END-OF-STREAM
                # with chunked encoding
                if block:
                    block = "%s\r\n%s\r\n" % (utf8("%x" % len(block)), block)
                if finishing:
                    block = "%s0\r\n\r\n" % block
            return block
    
    
    def authenticated(method):
        """Decorate methods with this to require that the user be logged in."""
        @functools.wraps(method)
        def wrapper(self, *args, **kwargs):
            if not self.current_user:
                if self.request.method in ("GET", "HEAD"):
                    url = self.get_login_url()
                    if "?" not in url:
                        if urlparse.urlsplit(url).scheme:
                            # if login url is absolute, make next absolute too
                            next_url = self.request.full_url()
                        else:
                            next_url = self.request.uri
                        url = "%s?%s" % (url,
                                         urllib.urlencode(dict(next=next_url)))
                    return self.redirect(url)
                raise HTTPError(403)
            return method(self, *args, **kwargs)
        return wrapper
    
    
    class UIModule(object):
        """A UI re-usable, modular unit on a page.
    
        UI modules often execute additional queries, and they can include
        additional CSS and JavaScript that will be included in the output
        page, which is automatically inserted on page render.
        """
        def __init__(self, handler):
            self.handler = handler
            self.request = handler.request
            self.ui = handler.ui
            self.current_user = handler.current_user
            self.locale = handler.locale
    
        def render(self, *args, **kwargs):
            """Overridden in subclasses to return this module's output."""
            raise NotImplementedError()
    
        def embedded_javascript(self):
            """Returns a JavaScript string that will be embedded in the page."""
            return None
    
        def javascript_files(self):
            """Returns a list of JavaScript files required by this module."""
            return None
    
        def embedded_css(self):
            """Returns a CSS string that will be embedded in the page."""
            return None
    
        def css_files(self):
            """Returns a list of CSS files required by this module."""
            return None
    
        def html_head(self):
            """Returns a CSS string that will be put in the  element"""
            return None
    
        def html_body(self):
            """Returns an HTML string that will be put in the  element"""
            return None
    
        def render_string(self, path, **kwargs):
            """Renders a template and returns it as a string."""
            return self.handler.render_string(path, **kwargs)
    
    
    class _linkify(UIModule):
        def render(self, text, **kwargs):
            return escape.linkify(text, **kwargs)
    
    
    class _xsrf_form_html(UIModule):
        def render(self):
            return self.handler.xsrf_form_html()
    
    
    class TemplateModule(UIModule):
        """UIModule that simply renders the given template.
    
        {% module Template("foo.html") %} is similar to {% include "foo.html" %},
        but the module version gets its own namespace (with kwargs passed to
        Template()) instead of inheriting the outer template's namespace.
    
        Templates rendered through this module also get access to UIModule's
        automatic javascript/css features.  Simply call set_resources
        inside the template and give it keyword arguments corresponding to
        the methods on UIModule: {{ set_resources(js_files=static_url("my.js")) }}
        Note that these resources are output once per template file, not once
        per instantiation of the template, so they must not depend on
        any arguments to the template.
        """
        def __init__(self, handler):
            super(TemplateModule, self).__init__(handler)
            # keep resources in both a list and a dict to preserve order
            self._resource_list = []
            self._resource_dict = {}
    
        def render(self, path, **kwargs):
            def set_resources(**kwargs):
                if path not in self._resource_dict:
                    self._resource_list.append(kwargs)
                    self._resource_dict[path] = kwargs
                else:
                    if self._resource_dict[path] != kwargs:
                        raise ValueError("set_resources called with different "
                                         "resources for the same template")
                return ""
            return self.render_string(path, set_resources=set_resources,
                                      **kwargs)
    
        def _get_resources(self, key):
            return (r[key] for r in self._resource_list if key in r)
    
        def embedded_javascript(self):
            return "\n".join(self._get_resources("embedded_javascript"))
    
        def javascript_files(self):
            result = []
            for f in self._get_resources("javascript_files"):
                if isinstance(f, (unicode, bytes_type)):
                    result.append(f)
                else:
                    result.extend(f)
            return result
    
        def embedded_css(self):
            return "\n".join(self._get_resources("embedded_css"))
    
        def css_files(self):
            result = []
            for f in self._get_resources("css_files"):
                if isinstance(f, (unicode, bytes_type)):
                    result.append(f)
                else:
                    result.extend(f)
            return result
    
        def html_head(self):
            return "".join(self._get_resources("html_head"))
    
        def html_body(self):
            return "".join(self._get_resources("html_body"))
    
    
    class URLSpec(object):
        """Specifies mappings between URLs and handlers."""
        def __init__(self, pattern, handler_class, kwargs=None, name=None):
            """Creates a URLSpec.
    
            Parameters:
    
            pattern: Regular expression to be matched.  Any groups in the regex
                will be passed in to the handler's get/post/etc methods as
                arguments.
    
            handler_class: RequestHandler subclass to be invoked.
    
            kwargs (optional): A dictionary of additional arguments to be passed
                to the handler's constructor.
    
            name (optional): A name for this handler.  Used by
                Application.reverse_url.
            """
            if not pattern.endswith('$'):
                pattern += '$'
            self.regex = re.compile(pattern)
            assert len(self.regex.groupindex) in (0, self.regex.groups), \
                ("groups in url regexes must either be all named or all "
                 "positional: %r" % self.regex.pattern)
            self.handler_class = handler_class
            self.kwargs = kwargs or {}
            self.name = name
            self._path, self._group_count = self._find_groups()
    
        def __repr__(self):
            return '%s(%r, %s, kwargs=%r, name=%r)' % \
                    (self.__class__.__name__, self.regex.pattern,
                     self.handler_class, self.kwargs, self.name)
    
        def _find_groups(self):
            """Returns a tuple (reverse string, group count) for a url.
    
            For example: Given the url pattern /([0-9]{4})/([a-z-]+)/, this method
            would return ('/%s/%s/', 2).
            """
            pattern = self.regex.pattern
            if pattern.startswith('^'):
                pattern = pattern[1:]
            if pattern.endswith('$'):
                pattern = pattern[:-1]
    
            if self.regex.groups != pattern.count('('):
                # The pattern is too complicated for our simplistic matching,
                # so we can't support reversing it.
                return (None, None)
    
            pieces = []
            for fragment in pattern.split('('):
                if ')' in fragment:
                    paren_loc = fragment.index(')')
                    if paren_loc >= 0:
                        pieces.append('%s' + fragment[paren_loc + 1:])
                else:
                    pieces.append(fragment)
    
            return (''.join(pieces), self.regex.groups)
    
        def reverse(self, *args):
            assert self._path is not None, \
                "Cannot reverse url regex " + self.regex.pattern
            assert len(args) == self._group_count, "required number of arguments "\
                "not found"
            if not len(args):
                return self._path
            converted_args = []
            for a in args:
                if not isinstance(a, (unicode_type, bytes_type)):
                    a = str(a)
                converted_args.append(escape.url_escape(utf8(a)))
            return self._path % tuple(converted_args)
    
    url = URLSpec
    
    
    def _time_independent_equals(a, b):
        if len(a) != len(b):
            return False
        result = 0
        if isinstance(a[0], types.IntType):  # python3 byte strings
            for x, y in zip(a, b):
                result |= x ^ y
        else:  # python2
            for x, y in zip(a, b):
                result |= ord(x) ^ ord(y)
        return result == 0
    
    
    def create_signed_value(secret, name, value):
        timestamp = utf8(str(int(time.time())))
        value = base64.b64encode(utf8(value))
        signature = _create_signature(secret, name, value, timestamp)
        value = "|".join([value, timestamp, signature])
        return value
    
    
    def decode_signed_value(secret, name, value, max_age_days=31):
        if not value:
            return None
        parts = utf8(value).split("|")
        if len(parts) != 3:
            return None
        signature = _create_signature(secret, name, parts[0], parts[1])
        if not _time_independent_equals(parts[2], signature):
            log.msg("Invalid cookie signature %r" % value)
            return None
        timestamp = int(parts[1])
        if timestamp < time.time() - max_age_days * 86400:
            log.msg("Expired cookie %r" % value)
            return None
        if timestamp > time.time() + 31 * 86400:
            # _cookie_signature does not hash a delimiter between the
            # parts of the cookie, so an attacker could transfer trailing
            # digits from the payload to the timestamp without altering the
            # signature.  For backwards compatibility, sanity-check timestamp
            # here instead of modifying _cookie_signature.
            log.msg("Cookie timestamp in future; possible tampering %r" % value)
            return None
        if parts[1].startswith("0"):
            log.msg("Tampered cookie %r" % value)
        try:
            return base64.b64decode(parts[0])
        except Exception:
            return None
    
    
    def _create_signature(secret, *parts):
        hash = hmac.new(utf8(secret), digestmod=hashlib.sha1)
        for part in parts:
            hash.update(utf8(part))
        return utf8(hash.hexdigest())
    python-cyclone-1.1/cyclone/httputil.py0000644000175000017500000002552612124336260017246 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """HTTP utility code shared by clients and servers."""
    
    from __future__ import absolute_import, division, with_statement
    
    import re
    
    from cyclone.util import ObjectDict
    from cyclone.escape import native_str
    from cyclone.escape import parse_qs_bytes
    from cyclone.escape import utf8
    
    from twisted.python import log
    from urllib import urlencode  # py2
    
    
    class HTTPHeaders(dict):
        """A dictionary that maintains Http-Header-Case for all keys.
    
        Supports multiple values per key via a pair of new methods,
        add() and get_list().  The regular dictionary interface returns a single
        value per key, with multiple values joined by a comma.
    
        >>> h = HTTPHeaders({"content-type": "text/html"})
        >>> list(h.keys())
        ['Content-Type']
        >>> h["Content-Type"]
        'text/html'
    
        >>> h.add("Set-Cookie", "A=B")
        >>> h.add("Set-Cookie", "C=D")
        >>> h["set-cookie"]
        'A=B,C=D'
        >>> h.get_list("set-cookie")
        ['A=B', 'C=D']
    
        >>> for (k,v) in sorted(h.get_all()):
        ...    print('%s: %s' % (k,v))
        ...
        Content-Type: text/html
        Set-Cookie: A=B
        Set-Cookie: C=D
        """
        def __init__(self, *args, **kwargs):
            # Don't pass args or kwargs to dict.__init__, as it will bypass
            # our __setitem__
            dict.__init__(self)
            self._as_list = {}
            self._last_key = None
            if (len(args) == 1 and len(kwargs) == 0 and
                isinstance(args[0], HTTPHeaders)):
                # Copy constructor
                for k, v in args[0].get_all():
                    self.add(k, v)
            else:
                # Dict-style initialization
                self.update(*args, **kwargs)
    
        # new public methods
    
        def add(self, name, value):
            """Adds a new value for the given key."""
            norm_name = HTTPHeaders._normalize_name(name)
            self._last_key = norm_name
            if norm_name in self:
                # bypass our override of __setitem__ since it modifies _as_list
                dict.__setitem__(self, norm_name, self[norm_name] + ',' + value)
                self._as_list[norm_name].append(value)
            else:
                self[norm_name] = value
    
        def get_list(self, name):
            """Returns all values for the given header as a list."""
            norm_name = HTTPHeaders._normalize_name(name)
            return self._as_list.get(norm_name, [])
    
        def get_all(self):
            """Returns an iterable of all (name, value) pairs.
    
            If a header has multiple values, multiple pairs will be
            returned with the same name.
            """
            for name, list in self._as_list.items():
                for value in list:
                    yield (name, value)
    
        def parse_line(self, line):
            """Updates the dictionary with a single header line.
    
            >>> h = HTTPHeaders()
            >>> h.parse_line("Content-Type: text/html")
            >>> h.get('content-type')
            'text/html'
            """
            if line[0].isspace():
                # continuation of a multi-line header
                new_part = ' ' + line.lstrip()
                self._as_list[self._last_key][-1] += new_part
                dict.__setitem__(self, self._last_key,
                                 self[self._last_key] + new_part)
            else:
                name, value = line.split(":", 1)
                self.add(name, value.strip())
    
        @classmethod
        def parse(cls, headers):
            """Returns a dictionary from HTTP header text.
    
            >>> h = HTTPHeaders.parse(
                "Content-Type: text/html\\r\\nContent-Length: 42\\r\\n")
            >>> sorted(h.items())
            [('Content-Length', '42'), ('Content-Type', 'text/html')]
            """
            h = cls()
            for line in headers.splitlines():
                if line:
                    h.parse_line(line)
            return h
    
        # dict implementation overrides
    
        def __setitem__(self, name, value):
            norm_name = HTTPHeaders._normalize_name(name)
            dict.__setitem__(self, norm_name, value)
            self._as_list[norm_name] = [value]
    
        def __getitem__(self, name):
            return dict.__getitem__(self, HTTPHeaders._normalize_name(name))
    
        def __delitem__(self, name):
            norm_name = HTTPHeaders._normalize_name(name)
            dict.__delitem__(self, norm_name)
            del self._as_list[norm_name]
    
        def __contains__(self, name):
            norm_name = HTTPHeaders._normalize_name(name)
            return dict.__contains__(self, norm_name)
    
        def get(self, name, default=None):
            return dict.get(self, HTTPHeaders._normalize_name(name), default)
    
        def update(self, *args, **kwargs):
            # dict.update bypasses our __setitem__
            for k, v in dict(*args, **kwargs).items():
                self[k] = v
    
        def copy(self):
            # default implementation returns dict(self), not the subclass
            return HTTPHeaders(self)
    
        _NORMALIZED_HEADER_RE = \
            re.compile(r'^[A-Z0-9][a-z0-9]*(-[A-Z0-9][a-z0-9]*)*$')
        _normalized_headers = {}
    
        @staticmethod
        def _normalize_name(name):
            """Converts a name to Http-Header-Case.
    
            >>> HTTPHeaders._normalize_name("coNtent-TYPE")
            'Content-Type'
            """
            try:
                return HTTPHeaders._normalized_headers[name]
            except KeyError:
                if HTTPHeaders._NORMALIZED_HEADER_RE.match(name):
                    normalized = name
                else:
                    normalized = "-".join(
                        [w.capitalize() for w in name.split("-")])
                HTTPHeaders._normalized_headers[name] = normalized
                return normalized
    
    
    def url_concat(url, args):
        """Concatenate url and argument dictionary regardless of whether
        url has existing query parameters.
    
        >>> url_concat("http://example.com/foo?a=b", dict(c="d"))
        'http://example.com/foo?a=b&c=d'
        """
        if not args:
            return url
        if url[-1] not in ('?', '&'):
            url += '&' if ('?' in url) else '?'
        return url + urlencode(args)
    
    
    class HTTPFile(ObjectDict):
        """Represents an HTTP file. For backwards compatibility, its instance
        attributes are also accessible as dictionary keys.
    
        :ivar filename:
        :ivar body:
        :ivar content_type: The content_type comes from the provided HTTP header
            and should not be trusted outright given that it can be easily forged.
        """
        pass
    
    
    def parse_body_arguments(content_type, body, arguments, files):
        """Parses a form request body.
    
        Supports "application/x-www-form-urlencoded" and "multipart/form-data".
        The content_type parameter should be a string and body should be
        a byte string.  The arguments and files parameters are dictionaries
        that will be updated with the parsed contents.
        """
        if content_type.startswith("application/x-www-form-urlencoded"):
            uri_arguments = parse_qs_bytes(native_str(body))
            for name, values in uri_arguments.items():
                values = [v for v in values if v]
                if values:
                    arguments.setdefault(name, []).extend(values)
        elif content_type.startswith("multipart/form-data"):
            fields = content_type.split(";")
            for field in fields:
                k, sep, v = field.strip().partition("=")
                if k == "boundary" and v:
                    parse_multipart_form_data(utf8(v), body, arguments, files)
                    break
            else:
                log.msg("Invalid multipart/form-data")
    
    
    def parse_multipart_form_data(boundary, data, arguments, files):
        """Parses a multipart/form-data body.
    
        The boundary and data parameters are both byte strings.
        The dictionaries given in the arguments and files parameters
        will be updated with the contents of the body.
        """
        # The standard allows for the boundary to be quoted in the header,
        # although it's rare (it happens at least for google app engine
        # xmpp).  I think we're also supposed to handle backslash-escapes
        # here but I'll save that until we see a client that uses them
        # in the wild.
        if boundary.startswith('"') and boundary.endswith('"'):
            boundary = boundary[1:-1]
        final_boundary_index = data.rfind("--" + boundary + "--")
        if final_boundary_index == -1:
            log.msg("Invalid multipart/form-data: no final boundary")
            return
        parts = data[:final_boundary_index].split("--" + boundary + "\r\n")
        for part in parts:
            if not part:
                continue
            eoh = part.find("\r\n\r\n")
            if eoh == -1:
                log.msg("multipart/form-data missing headers")
                continue
            headers = HTTPHeaders.parse(part[:eoh].decode("utf-8"))
            disp_header = headers.get("Content-Disposition", "")
            disposition, disp_params = _parse_header(disp_header)
            if disposition != "form-data" or not part.endswith("\r\n"):
                log.msg("Invalid multipart/form-data")
                continue
            value = part[eoh + 4:-2]
            if not disp_params.get("name"):
                log.msg("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(HTTPFile(
                    filename=disp_params["filename"], body=value,
                    content_type=ctype))
            else:
                arguments.setdefault(name, []).append(value)
    
    
    # _parseparam and _parse_header are copied and modified from python2.7's cgi.py
    # The original 2.7 version of this code did not correctly support some
    # combinations of semicolons and double quotes.
    def _parseparam(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(line):
        """Parse a Content-type like header.
    
        Return the main content-type and a dictionary of options.
    
        """
        parts = _parseparam(';' + line)
        key = next(parts)
        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
    
    
    def doctests():
        import doctest
        return doctest.DocTestSuite()
    python-cyclone-1.1/cyclone/sqlite.py0000644000175000017500000000577112124336260016672 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """An inline SQLite helper class.
    
    All queries run inline, temporarily blocking the execution. Please make sure
    you understand the limitations of using SQLite like this.
    
    Example::
    
        import cyclone.web
        import cyclone.sqlite
    
        class SQLiteMixin(object):
            sqlite = cyclone.sqlite.InlineSQLite("mydb.sqlite")
    
        class MyRequestHandler(cyclone.web.RequestHandler):
            def get(self):
                rs = self.sqlite.runQuery("SELECT 1")
                ...
    
    There is no ``Deferred`` responses, and no need to ``yield`` anything.
    """
    
    import sqlite3
    
    
    class InlineSQLite:
        """An inline SQLite instance"""
        def __init__(self, dbname=":memory:", autoCommit=True):
            """Create new SQLite instance."""
            self.autoCommit = autoCommit
            self.conn = sqlite3.connect(dbname)
            self.curs = self.conn.cursor()
    
        def runQuery(self, query, *args, **kwargs):
            """Use this function to execute queries that return a result set,
            like ``SELECT``.
    
            Example (with variable substitution)::
    
                sqlite.runQuery("SELECT * FROM asd WHERE x=? and y=?", [x, y])
            """
            self.curs.execute(query, *args, **kwargs)
            return [row for row in self.curs]
    
        def runOperation(self, command, *args, **kwargs):
            """Use this function to execute queries that do NOT return a result
            set, like ``INSERT``, ``UPDATE`` and ``DELETE``.
    
            Example::
    
                sqlite.runOperation("CREATE TABLE asd (x int, y text)")
                sqlite.runOperation("INSERT INTO asd VALUES (?, ?)", [x, y])
            """
            self.curs.execute(command, *args, **kwargs)
            if self.autoCommit is True:
                self.conn.commit()
    
        def runOperationMany(self, command, *args, **kwargs):
            """Same as `runOperation`, but for multiple rows.
    
            Example::
    
                sqlite.runOperationMany("INSERT INTO asd VALUES (?, ?)", [
                                            [x1, y1], [x2, y2], [x3, y3]
                                        ])
            """
            self.curs.executemany(command, *args, **kwargs)
            if self.autoCommit is True:
                self.conn.commit()
    
        def commit(self):
            """Commits pending transactions"""
            self.conn.commit()
    
        def rollback(self):
            """Gives up pending transactions"""
            self.conn.rollback()
    
        def close(self):
            """Destroys the instance"""
            self.conn.close()
    python-cyclone-1.1/cyclone/app.py0000644000175000017500000002314712124336260016146 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """
    Command-line tool for creating cyclone applications out of the box. ::
    
        usage: cyclone app [options]
        Options:
         -h --help              Show this help.
         -n --new               Dumps a sample server code to stdout.
         -p --project=NAME      Create new cyclone project.
         -g --git               Use in conjunction with -p to make it a git \
    repository.
         -m --modname=NAME      Use another name for the module \
    [default: project_name]
         -v --version=VERSION   Set project version [default: 0.1]
         -s --set-pkg-version   Set version on package name [default: False]
         -t --target=PATH       Set path where project is created \
    [default: current directory]
         -l --license=FILE      Append the following license file \
    [default: Apache 2]
         -a --appskel=SKEL      Set the application skeleton [default: default]
    
        SKEL:
          default              Basic cyclone project
          signup               Basic sign up/in/out, password reset, etc
          foreman              Create a foreman based project \
    (suited to run on heroku and other PaaS)
    
        Examples:
         For a simple hello world:
         $ cyclone app -n > hello.py
    
         For a project that requires sign up:
         $ cyclone app --project=foobar --appskel=signup
    """
    
    from __future__ import with_statement
    import base64
    import getopt
    import os
    import re
    import string
    import sys
    import uuid
    import zipfile
    from datetime import datetime
    
    DEFAULT_LICENSE = """\
    # Copyright %(year)s Foo Bar
    # Powered by cyclone
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    """
    
    SAMPLE_SERVER = """\
    #
    # Start the server:
    #   cyclone run server.py
    
    import cyclone.web
    
    
    class MainHandler(cyclone.web.RequestHandler):
        def get(self):
            self.write("Hello, world")
    
    
    class Application(cyclone.web.Application):
        def __init__(self):
            handlers = [
                (r"/", MainHandler),
            ]
    
            settings = dict(
                xheaders=False,
                static_path="./static",
                templates_path="./templates",
            )
    
            cyclone.web.Application.__init__(self, handlers, **settings)\
    """
    
    
    def new_project(**kwargs):
        zf = kwargs["skel"]
        dst = kwargs["project_path"]
    
        os.mkdir(dst, 0755)
        for n in zf.namelist():
            mod = n.replace("modname", kwargs["modname"])
            if n[-1] in (os.path.sep, "\\", "/"):
                os.mkdir(os.path.join(dst, mod), 0755)
            else:
                ext = n.rsplit(".", 1)[-1]
                fd = open(os.path.join(dst, mod), "w", 0644)
                if ext in ("conf", "html", "txt", "py", "md", "sh", "d") or \
                        n in ("Procfile"):
                    #print "patching: %s" % n
                    fd.write(string.Template(zf.read(n)).substitute(kwargs))
                else:
                    fd.write(zf.read(n))
                fd.close()
    
        # make sure we can actually run start.sh
        if os.path.exists(os.path.join(dst, "start.sh")):
            os.chmod(os.path.join(dst, "start.sh"), 0755)
    
        if kwargs["use_git"] is True:
            os.chdir(kwargs["project_path"])
            os.system("git init")
            os.system("git add .gitignore")
    
    
    def usage(version):
        print("""\
    usage: cyclone app [options]
    Options:
     -h --help              Show this help.
     -n --new               Dumps a sample server code to stdout.
     -p --project=NAME      Create new cyclone project.
     -g --git               Use in conjunction with -p to make it a git repository.
     -m --modname=NAME      Use another name for the module [default: project_name]
     -v --version=VERSION   Set project version [default: %s]
     -s --set-pkg-version   Set version on package name [default: False]
     -t --target=PATH       Set path where project is created \
    [default: current directory]
     -l --license=FILE      Append the following license file [default: Apache 2]
     -a --appskel=SKEL      Set the application skeleton [default: default]
    
    SKEL:
      default              Basic cyclone project
      signup               Basic sign up/in/out, password reset, etc
      foreman              Create a foreman based project \
    (suited to run on heroku and other PaaS)
    
    Examples:
     For a simple hello world:
     $ cyclone app -n > hello.py
    
     For a project that requires sign up:
     $ cyclone app --project=foobar --appskel=signup""" % (version))
        sys.exit(0)
    
    
    def main():
        project = None
        modname = None
        use_git = False
        set_pkg_version = False
        default_version, version = "0.1", None
        default_target, target = os.getcwd(), None
        license_file = None
        skel = "default"
    
        shortopts = "hgsnp:m:v:t:l:a:"
        longopts = ["help", "new", "git", "set-pkg-version",
                     "project=", "modname=", "version=", "target=", "license=",
                     "appskel="]
        try:
            opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts)
        except getopt.GetoptError:
            usage(default_version)
    
        for o, a in opts:
            if o in ("-h", "--help"):
                usage(default_version)
    
            if o in ("-n", "--new"):
                print "%s%s" % (DEFAULT_LICENSE % {"year": datetime.now().year},
                                SAMPLE_SERVER)
                sys.exit(1)
    
            if o in ("-g", "--git"):
                use_git = True
    
            if o in ("-s", "--set-pkg-version"):
                set_pkg_version = True
    
            elif o in ("-p", "--project"):
                project = a
    
            elif o in ("-m", "--modname"):
                modname = a
    
            elif o in ("-v", "--version"):
                version = a
    
            elif o in ("-t", "--target"):
                target = a
    
            elif o in ("-l", "--license"):
                license_file = a
    
            elif o in ("-a", "--appskel"):
                if a in ("default", "foreman", "signup"):
                    skel = a
                else:
                    print("Invalid appskel name: %s" % a)
                    sys.exit(1)
    
        if license_file is None:
            license = DEFAULT_LICENSE % {"year": datetime.now().year}
        else:
            with open(license_file) as f:
                license = f.read()
    
        if project is None:
            usage(default_version)
        elif not re.match(r"^[0-9a-z][0-9a-z_-]+$", project, re.I):
            print("Invalid project name.")
            sys.exit(1)
    
        mod_is_proj = False
        if modname is None:
            mod_is_proj = True
            modname = project
    
        if modname in ("frontend", "tools", "twisted"):
            if mod_is_proj is True:
                print("Please specify a different modname, using "
                      "--modname=name. '%s' is not allowed." % modname)
            else:
                print("Please specify a different modname. "
                      "'%s' is not allowed." % modname)
            sys.exit(1)
    
        if not re.match(r"^[0-9a-z_]+$", modname, re.I):
            print("Invalid module name.")
            sys.exit(1)
    
        if version is None:
            version = default_version
    
        if target is None:
            target = default_target
    
        if not (os.access(target, os.W_OK) and os.access(target, os.X_OK)):
            print("Cannot create project on target directory "
                  "'%s': permission denied" % target)
            sys.exit(1)
    
        name = "Foo Bar"
        email = "root@localhost"
        if use_git is True:
            with os.popen("git config --list") as fd:
                for line in fd:
                    line = line.replace("\r", "").replace("\n", "")
                    try:
                        k, v = line.split("=", 1)
                    except:
                        continue
    
                    if k == "user.name":
                        name = v
                    elif k == "user.email":
                        email = v
    
        skel = zipfile.ZipFile(open(
            os.path.join(os.path.dirname(os.path.abspath(__file__)),
                         "appskel_%s.zip" % skel), "rb"))
    
        if set_pkg_version is True:
            project_name = "%s-%s" % (project, version)
        else:
            project_name = project
    
        project_path = os.path.join(target, project_name)
        if os.path.exists(project_path):
            print("Directory '%s' already exists. Either remove it, or set a "
                  "different project name. "
                  "e.g.: python -m cyclone.app -p %sz" % (project_path,
                                                          project_name))
            sys.exit(1)
    
        new_project(skel=skel,
                    name=name,
                    email=email,
                    project=project,
                    project_name=project_name,
                    project_path=project_path,
                    modname=modname,
                    version=version,
                    target=target,
                    use_git=use_git,
                    license=license,
                    cookie_secret=base64.b64encode(uuid.uuid4().bytes +
                                                   uuid.uuid4().bytes))
    
    
    if __name__ == "__main__":
        main()
    python-cyclone-1.1/cyclone/xmlrpc.py0000644000175000017500000000757212124336260016677 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """Server-side implementation of the XML-RPC protocol.
    
    `XML-RPC `_ is a remote procedure call
    protocol which uses XML to encode its calls and HTTP as a transport mechanism.
    
    For more information, check out the `RPC demo
    `_.
    """
    
    import xmlrpclib
    
    from twisted.internet import defer
    from cyclone.web import RequestHandler
    
    
    class XmlrpcRequestHandler(RequestHandler):
        """Subclass this class and define xmlrpc_* to make a handler.
    
        Example::
    
            class MyRequestHandler(XmlrpcRequestHandler):
                allowNone = True
    
                def xmlrpc_echo(self, text):
                    return text
    
                def xmlrpc_sort(self, items):
                    return sorted(items)
    
                @defer.inlineCallbacks
                def xmlrpc_geoip_lookup(self, address):
                    response = yield cyclone.httpclient.fetch(
                        "http://freegeoip.net/xml/%s" % address.encode("utf-8"))
                    defer.returnValue(response.body)
        """
    
        FAILURE = 8002
        NOT_FOUND = 8001
        separator = "."
        allowNone = False
    
        def post(self):
            self._auto_finish = False
            self.set_header("Content-Type", "text/xml")
            try:
                args, functionPath = xmlrpclib.loads(self.request.body)
            except Exception, e:
                f = xmlrpclib.Fault(self.FAILURE,
                                    "Can't deserialize input: %s" % e)
                self._cbRender(f)
            else:
                try:
                    function = self._getFunction(functionPath)
                except xmlrpclib.Fault, f:
                    self._cbRender(f)
                else:
                    d = defer.maybeDeferred(function, *args)
                    d.addCallback(self._cbRender)
                    d.addErrback(self._ebRender)
    
        def _getFunction(self, functionPath):
            if functionPath.find(self.separator) != -1:
                prefix, functionPath = functionPath.split(self.separator, 1)
                handler = self.getSubHandler(prefix)
                if handler is None:
                    raise xmlrpclib.Fault(self.NOT_FOUND,
                        "no such subHandler %s" % prefix)
                return self._getFunction(functionPath)
    
            f = getattr(self, "xmlrpc_%s" % functionPath, None)
            if f is None:
                raise xmlrpclib.Fault(self.NOT_FOUND,
                    "function %s not found" % functionPath)
            elif not callable(f):
                raise xmlrpclib.Fault(self.NOT_FOUND,
                    "function %s not callable" % functionPath)
            else:
                return f
    
        def _cbRender(self, result):
            if not isinstance(result, xmlrpclib.Fault):
                result = (result,)
    
            try:
                s = xmlrpclib.dumps(result,
                    methodresponse=True, allow_none=self.allowNone)
            except Exception, e:
                f = xmlrpclib.Fault(self.FAILURE, "can't serialize output: %s" % e)
                s = xmlrpclib.dumps(f,
                    methodresponse=True, allow_none=self.allowNone)
    
            self.finish(s)
    
        def _ebRender(self, failure):
            if isinstance(failure.value, xmlrpclib.Fault):
                s = failure.value
            else:
                s = xmlrpclib.Fault(self.FAILURE, "error")
    
            self.finish(xmlrpclib.dumps(s, methodresponse=True))
    python-cyclone-1.1/cyclone/appskel_signup.zip0000644000175000017500000014404312124336260020563 0ustar  lunarlunarPK
    z®‹A”‰•$
    .gitignoreUT	HñÇPʶ3Quxõ*.swp
    *.pyc
    dropin.cache
    PK
    z®‹A	frontend/UT	HñÇPÿ¶3QuxõPK
    z®‹Afrontend/locale/UT	HñÇPÿ¶3QuxõPK
    z®‹Afrontend/locale/es_ES/UT	HñÇPÿ¶3QuxõPK
    ¢:“A"frontend/locale/es_ES/LC_MESSAGES/UT	/±ÑPÿ¶3QuxõPK¨:“Aý{ÇÆ(d,frontend/locale/es_ES/LC_MESSAGES/modname.moUT	;±ÑPʶ3QuxõuVKŒÅ.‚™egÇá•ð
    1µ$Æ<ãÙMÐįÍoâDz;â\HMwíLáî®qUõ¬%G'NA‘"$XE‘ðø@’KG9FÉ!Ê-HÉ•K¤\àÀ÷wUÏÃ^ZÓûUWýõ×ÿøþ¿ößwï{“áiâ}ï[·0vŠÆ·²â9¼±“ÀÇ€sÀx'оæßÎßî~´Ïëûx?ð_aßÿƒÜÜm~þ.à·±ÄNﮆù€S`
    øÚm^ϯ·ö½Ö¯ü$¬ÿøMà?ƒ¾ÿ~¼x ÂØðÉŠ·g¥âåÄ©Š×߯x?óŠ×û«ðý*ð[À7*Þïß?¬x;ÿü>ð3à}ÀÊœ×{8œóúÞ®Rü€ŸB.ÎÜîó!çý¾«ó>>¿¼>ïÏýtÞÛó9ðŠ3pX­z{ŽT}¼ƒôUýzüð7U¯ÿZûSØ÷ààÿªÞ¾à㸴àåŸx~ÁÛµµàãÑ]ðqýe˜}ÁÛû[àÀ?þgÁû¿¯æù´¿æãt¸æyr¢æõÿ8àóVóú_¬y{/Õ¼_¯¿|³æíücÍÛÿ· ÷%®°{4"þâ~¿ñçià_€¿Þ
     ü…Í,ÔÆÃaLz9óvÓCv?ÆGñ6˜·áÎ0GçÝÆs²óáû±€TnûÙ䡜ç¦æ(¯Û:ó§‡êönæ¹\>§ÆÃGÃøqæc@|$Nf>†‡ð¢ŠÉO+m?¶MpåD—ʯ¸¯–•“έ[ؾÔhVÑ{²PoûpòŠ;Ц¤²ñ¨'BöôVûtý©‰½-M}5‹4‘¬ÅŸê(Weg—Ù@‰Xó¼l±S0R£kX0ižX){g¤ˆ
    v*Ç}ÐiK›AR˜i‡V‰
    *R¢ÐÈ€;£“¨.5¨Y‰\Ž+øex°å‚"I›7fÄG:H$èþ	QXSׂT”§”J]ðmÓèä8Ô'*f´Û‘üÕ€ˆkî`àÏ€~+^’|H½br`ù"¦ÒÑ`×wObÓ@fŠB¿™—>÷·Šjþåœêc|# øDz4YS´nd±¢ÍŸr èJðù&mAMN¡x¹ÐS¸_Þ
    ¦(QoÀ^|PK¡:“A÷9D_ÅR,frontend/locale/es_ES/LC_MESSAGES/modname.poUT	.±ÑPʶ3Quxõ•XÛnÛF}÷WLlmÑÈ¡¨‹eÕuê8Nm v_ZèËŠK“»ÌîRý7~ÌCŠ|@èÇ:KЉ¤†-‘3gfçrfÖ[à%^ ‚æSG`0Œfp{ce”(>øñð'p®›KÓË«×pÃúësmŸÄ}ˆ…
    ÌŒ Yˆp…F`:}–›‹˜w˦©™ƒ?1á+„·\*{7öϧߦ!ãÁ¶'Ãý©q’ÝØz7ñý}²ê)÷asÓ~ ÛöÓæ¹’Ð3¿ó*Í¥ƒ³Ýý[лŒ¤2S«ÖyOuçJŽ!}uþþªs¨R輡³SsûÓ·7vŽ3pœ¥pçç\7Ⱥà8ãôççBüÓ¦s¥˜ÐT©ÆßqØ¥ž˜ÆŸ¥‹§'§G噺Ûø¡á'	üd^Rî¸ø¼Sͯ×Wo;£UYëË
    ªÎ‘ð¤ÏÅt£	7VfckÚsLùÀE›qo˜úÀód,ª!-]ž¢ ©(Ó<²Âš‘vr¤3ªŠRûL†…Í*»¹Ê9ÓúN*¿T³ÇQLãâŸfsýA®{"æ, ¢n–‘ð|ñ@oe3ÈÎ:Hô˜#Z‹KµãPÔ¤øÁ@ÈŒ7+!©fÒ@.a5Iß¹ð¸ÍiôrèK6GÿY‰õ{LRÌ—ÏšõŠ(]G>s©wà™˜N|ÏT£â¨Ÿ+¾az6‘¬’+6	P5‡t×Y+¨J$cl+ž]7×Êû¢T;ñ¹›ÕºNaí’Ø
    d\5‡Jµœ®[VÞk)
    ‰³ÈÊ(c‰mI•ó.Ÿ6÷8l/zÍúNI`›×ĎƲ©/½8¤ X®dpÇ0>ÆÜ»Í¬	¼ƒ(#ºí½‰Ú'F	 ‘1LÑ¡q…¨5Št|›0…â]
    ÇW§ï
    KÛ+üym¸ek@M‡ÉE$=uŠGå@®PDŒsi]HÈ[™¡$Îgp#UÈìÙ	É_C‚Z'âÂȤ>¶(¸¸ÐŠcêðÿ0[ÖŸÐ.ˆ•ÕàÛÃkÚõæ(xÛdèã;%êœl‹nõì½ýU¦ÎùÙ‡vF¾Œs.%Ö–~-”Ïr^r.Åé{‰vèºkD›TgK;ËÖù5äÚ+Z*¦ý½árjI†îhlà›v„G+ºÔ9̨jåÆZrW)—/aUîlQ²±ZvLÅP~«ÍfëãS{©ò>xêJm÷GÅ¥*u–ÿwBRµ·›"#U²Ò†;FÒ^2ä$àS+ÁåF^kô1¢Åî­Íg'Àb0gIMåé;ÖfÁæÆPK
    z®‹Afrontend/locale/pt_BR/UT	HñÇPÿ¶3QuxõPK
    ¢:“A"frontend/locale/pt_BR/LC_MESSAGES/UT	0±ÑPÿ¶3QuxõPK¥:“ALÍÚ
    ,frontend/locale/pt_BR/LC_MESSAGES/modname.moUT	6±ÑPʶ3QuxõmUKŒG.‚3›Ù$$€$˜ZƒíÄñ¬Çk"…ÁØYÛ»x¥µ³ìމ	¡šîÚ™²§»ÚUÕ³žHœxÝPAÊ	¢K‰X‘‚öÄĉ¯ @‚ïï¿æ±k·fæ«®úë|ÿcþøä¡ï<-|ŸÁ÷â­?,ªçÙCB\>œ€ীߎû?Î|øÞ!Ö÷>ðÀ?Ä{ÿŠrsóþÀOq$^>\û¯?	Ì€Àï>ÌzÞ>üq¼÷³x~/âûñü7ÀÇ€¿úþ
    <
    üð0ðHMˆð…û³RcùWøScýEã,k¬÷ñý;À¿Wã¸ßþ¼Æ~þø9à_€Oks¬÷p<6Çú~\%þ€7€ÿ>ƒ\\ÁáÞ5HƒËâÝyöï=à§¿šg»šg½>	üç<Ç9WgÿNÖ™×u–ÿJó÷õ:óòZãyøQà›uÎß»ÀãÀ_×ÙîÀÇÿ«³½ÇÌÏa ¶ÄÓ
    æÿxƒý=÷¿Ð`~/7Xß×ÌË·\wìϽûùÛóòçÛù[Ä¢½78oÿmp}<²À|]`ýg˜‡W'©~Xÿ›lïð›À_.°+ ¥
    ÜnQAűˆ½ñ™¸&¾¤`ÿè!½Gãú4¾Kø~V°?ôþ§âzQ0Å÷ç"R»=*¦ÏÓ‚ùš›Ù£¼P.›‚9¦‡ú–â<<#wlfM±kâ¸"Þ‰ÃgsxBp®ˆ÷ç×/åšø>5V´"›ï—©`l.u¦Ì@ö•—]­séud°ò\÷ü1ît÷¼Ü5¡/Mîƒ+ºá—ÄJ’Ø2cÄ¥LÞó⢵‚ª>(´“AgÅ@-.±QqYù~×*—ŠÕʲJS§½ßÿ&s¤Ó=ã¡CODN«t4{°f]¢…ò~׺ô‚X+™«L‹+8áeèk™ë݉H{|0²¥›Ýµ¸³.UÏiÍ1ÓM±žÕÀ¤‘¦±³ûwíTÏä¤raü29ÝPy¯T=-®Ññµ·Ä˃¹ÍƒÄ’'¯^¦6?$r˜ôÅæ@+¯¥J]„ÊíŽv™—vGnk74‰‹7B„Êä’râÄ–ÎtÖEšàÍ–¾]jКÊîH®oÊcPã0=õm[
    uº(¶u޵éå¨
    F[^”Å÷•Úd3ØTÄvYÀ°ÚÕÌ#wKSI_hukQÜB§o<ÅXU– ù}&*¡£A&™ÊS¬‰ëEJEx8 ‘Ô&eF¥ŽºWrW¨æo—&¹Å•+U>â²qö¦NÂÒ¹®;/WP[(ÙÓ••¦õ€ŒâB
    M]åt׿ÚË+«3Kâ=H碸Ae7cÒv	*ÔŸªÔd¥؆T0CÚ®êI[þ¼ìã::ðÆlñNÕTU²_IéqÁÁ@TWUÈØƒû4orÀÍõ´ùUíÏW¢ʇfÇ©Ü({mð®ï &§åš±ÎÈs;w^êQû-×óõqgÁ¥«ëWW§¾ŸYjÕ1{²ÐìŒ
    }'œÆP2ùeÒW”}ézg­ùâTŽlïh×\ÍKEÖ–/vM¨‹ëYlùTOênï½·­Ü±Fê|hTj‘§f(ü‹ó¢¿Ðà˜ì(LÂÔ¢iÂk'NJÈšÄ(ذr2OÇã“­a˜\Äjžb–í½cɟʱlÉœüã™é`2Ó›{oíÛôèþD—¨aüôÕqÚ¾¬XAsHŒÜò¥Â|FÁöÁ£¸»žïý41P\R|	ÊÔVL ä9iòáÞ[˜•öA‡h¹JëTʳ{Ó[ÛûΕXOÍT%…Ÿ±³QLåÁôâËìéŠç…gÎkòĤ:›ñ;jh©sµAŽ
    (ÊŸæ	‡gĨŒtÅRV­æþõ´ØÀÄuº	¿¶í™UÉàÏ\ñÌå*pã|ˆ/;5„ä"øA•9ç0—TŠ–R¯÷
    y“lg¥Ðì7á‰ážÉHl:€MVПº‹	ϸ±2;{¢
    œ£Ê'\D±™ô†y OÄÜ»O­*ÖD°¥Tvp4SÇàŒ€¬eLÌ q‚…Lk„	:>ÀfT±˜U“·ÓË‹ÂÒÁCZG˜ÆAò×õ­hv.Whð!F„\ÉÔ	ˆà¨s+ÎR‡¦18ˆ’åãgxŒ1ðø·…¦pKX·BÂHV%nE±’sûÃÜZ_õœZu;ý¦š×‡Ã¢ÞÊ*/œ‹Ç<ÞLnTw«Ww«[wkÐäV·¦;pk§ŒkpÃáp£Î¹(ý?¨ÃæBwËÞ¿#$–>0mi0¤êžY&':bô¾B%Ê0Vz“&oãÌ)2— i­ùtœæ³6[tR¬ñ$¥Ïdy\%Ř,AH&¡„¬Ä‚¢º™õzã"qoQߥK²¨ÄjR™Ö˜–cÁ¸Š"v“–%ªÎ"îÀ®ÐÑÚ,±¿Î¥Â6R¶è\Û
    ~¶&ç×ä¹&RàgeœJ,óÜ .	–éLGµ2M;P¿NÁ£vû'6vsnd²~(’¬íÔDFfÇÏõÑ‹Ù1±,<‹=«QÍá]˜é£±rÔÇ/¶·lB™-X?}% –ï¢ÿ±–Œ
    ºµ15%UC}7´8¹ÞØ esø®S¦&dá5WZx"mÙ n±+¾‘j!K}YñN#å‹s}Ù5>¬åÑo}#Å7÷h2@ÿøëfŠL$Þ£â¿BŠC§8ï9ˆO1–ˆm§
    \lËÁCd’n\\¾†6e*D'ÌÉ-S+Üe«[¡
    í͇¥¯ä¬á7ƒ†Xd×ÚÑKž¦ã 0˜úV‹eåüΜ“UÆû+4슋¶»ÂÐ-foB·9eèY²cþ&ßæ,ë“v^µ¤š²£íAOÕ5áP¿$ϧ™sèº[̹®Ž‡vÚ¬2fȵW´IŒ5»áˆøEÖ`h4M÷§z½T9MIhã"T²R)—¯MU*lQ²V²¦¨ä×Ít*¶ÚLö]Ð~½Å’ÇÙìß=*öêaSa¤ZW¹5êÑ0’ö& g
    êæñ_ÅåH^Qøa°»eóiÏìm"›§Iö¬x²©l³úîÎÿPK
    y°‹Afrontend/static/UT	ôÇPÿ¶3QuxõPKz®‹A3ÜÈñá~frontend/static/favicon.icoUT	HñÇPʶ3Quxõc``B0È`a`Ò@RbF0	,Xœ¼Òt|{ÏWdŒM
    Š^Eó•QÌ‹É!ÔÈÊ+ ˜ãäì
    ÇpsÀê”QøèXAQõXšYøô øA¢þ÷ß ŒbÿÂ0Híí»÷ô`a€+,ñ‰
    W;Bb €œ¾`[8aCN“èf ƒÈ¨˜ÿ09ô´Œl>¶ôŽn®xDw>5¸ÂäNJÒ	²¤èk>,ü6†ýÿÿ#ðçù@<¡¾’ao3ÃÌã‡âÏPK
    y°‹A¹P„frontend/static/legal.txtUT	ôÇPʶ3QuxõTerms of Service: whatever.
    PK
    ØbBfrontend/template/UT	î“2Qÿ¶3QuxõPKX6“A‹[	
    frontend/template/account.htmlUT	ªÑPʶ3QuxõÍV]kÛ0}ﯸSÉÒB¤éhâFa°—n°íi” [r,"[F’óÑß±´?¶+¤îÒ†~äaY÷Þ£ãs¢¬[À—–§Ì¡,i'¶‰$ÐÚ­[HÎ æ”qí¦|cW’ÅV°Æ@FéÔ³*ÂE/[Žpzƒw'R:ñÂÜX•T¹	]zÁl<„½*u‹0„óËl	ýú1ªJôT¤CèÍ­‚þ¶* álªUž2/TRé!GQTÅ”FʈPFIÁà˜ÜU†½fÂzeš§)¹ †v)‰ºÛ‡ûevã÷øKÏÄ”©…ãïÈôñÖÓ€žôÎÜÕé
    Nÿ]ò%%‹g–ì4¥ùâ¹6cÎv’˜‡3\dÛC×$j1Š?Õq‘f¹ýeW¿"MFnw ›)5fz’ÛjH¥Ö3âŽãµ²1ÓØ/Œ¥3ØqÕGTã²&éw+£½ÑöÎÕ÷NÏè”>w$!”HéŠ4H¸C¶ÊXâ6‚÷I¬Å$ãõzrB>…!úÔ‚áÖâ¬!§›ßû€¤—eÞ
    Mx+']¸Ð2Ö˯ îI>ç¸k3IC+‰¦¼"±ý9—R„m#,)FH''nH@°¯s*sîJ#Á%3m”†v!'ÔKDP¥p­¢È««ÓfB˹Ãgb^“§’kÅÓÃ¥+¥¾” ÑB	ZW„RÄeÔR	“ˆ-ÿùíwéØï⿺µõýV{lÆ[¾H篒A]Ù”º˜c“ÞÝf’ª„Ú0~³lYó_+|‚¤«sž~-ɱ0e!à A\j•^¨Íoïá
    _ìëá99W®U	<²DÿÛ«LýòV+×{ÔSiÛB‰v –y†Eœígcò0äÆT|¾Ó9gïdÒ ÇS*­a›ÞžÄ#Œ£L<ÏV¤ò—ɃDØŠÈÏ‚{ùÓUâŒñ4s‡Îøèȵ{6¾(RÊr]z6¿U¶ÁµJ2š®ð_Ôyßïf àÙ8
    ÿPKe6“AÖe;óGìfrontend/template/admin.htmlUT	-ªÑPʶ3QuxõµUMÓ0½÷WÃJppK	Do+NE“xšX¸vd;¥«¨ˆ¿aÿ·Óô#hWˆC•Úó<ïÍódÒÜÜ;TÜšÅeé¶’’›Ã¢¹!™ÔùR"p4íVlݽÄdAH¦ù=i*à\¨‚9]Ý’·¯«ý;‰?ïÓ¶ÇŽ©ì2èSq±#¹kWtØíL¨‹a¹{ä-my/y¼BÓOùVÄ1æ£b™SÄÿXŸ‚Yûõá·–ÿF^¹ô×'÷øä³6®.ê‡_vžþô¥
    {o±»1t7QcE¡tzôίH»œmÒs5ó ޼Œ$~ÆXt2Š	cÃðï‹ñÏð8~‡þPKh6“A_+Ö¦‰frontend/template/base.htmlUT	4ªÑPʶ3QuxõSÑŽÛ |¿¯àPO×JµiútªŒ¥Â°‰7‡Á…µÓ(Ê¿LÎJryê‹ÍîÎÎà¹y6^ÓqÖÓ`Û§&¿˜Un'98Þ>1Öô L>¤ã¤˜îUˆ@’O´­ÞøeDHÚÓë¬×ïl)ÙËùËàS¤8“(È+Â<—|F8Œ>gÚ;—h¨—fÔP-Åw†	•­¢Vä¦þÁ?Sˆ:àHèÝÛ š¨÷áS@Ý;`%	Az"†:Óe³$ÇAí@¤g}€­ä§S$E¨OÁ~}ݪ9£ëôxýv>(/¤ß¿„p@Éžºóž"5jãjíA$‚P­ñ³ÞÔ¡ck¯JȘzÀ´#¿Ü˜Žb@á5•%„”ÂGX”„ó±óæx·ãÔÜ©Ûò5g¦­ŠQòìŸJ7	÷‚c²éN.¦Õ–5ÏUÅĺʪêÃü’‹A£brJíÕßzçý΂1..åž°ØE±ÿ3A8ŠMý–L*ÅbÊ>ò¶…¯}Lþ)ì¯Bx skAéÆO¦«“ùË÷PKn6“AûÔ7ˆñX frontend/template/dashboard.htmlUT	@ªÑPʶ3QuxõU=O„@†{~Å›M0ÚpÞµ"‰ÚXh§õe€ñà\Øuw¸“þ»—hl¦˜y?žÝ)÷u„¡ºkû¬‘Τs2¥(­«>QSlJG¡ÞwÜz¢JÚë]}ÿ”ž¼,òf[LÓþÚ<:'QyD¡  ÜyKÂæfžóê’Ü_´ï‘!MQ»jÐ&EÎ4B¾†VÖP?¢ç3|pG®$ËËPàÁZŒnÀ²&uã‚DÊI()pézŽx~{}ù­É.(¾H”;,³nO¨,Åxo>ôL‘ÊyU9?ÞáÉu~aØÝnw«1ߨE_òPKs6“A`ÙKϰ¨	frontend/template/index.htmlUT	JªÑPʶ3Quxõí–ÏrÛ6ÆïzŠ-[gÚT%Yãvd…‡:éä’™N=={@"ƒJ²U^¨·>C^¬øO’c7=öЃDÜ]|»û[Hû⎫ÜR”2˧¥«dD‡Ñþ‚R©³{*9Ëy–ÖÖí$OFD©Îw´Ç
    ‘ay.T1qÚ¬h13×gË©vNW+Zvo#|ůéÆ[¬S¦•cB!þë/¦Çç6zÅêB¨͈y§¯»µÇɃÈ]¹¢fCØ3ç„ʧ.aI³S	`^ßs¥TqkYÁ‰©œ¬(yC©‡zÕiûè+dSãù<òՙȡš&á¾¢Œ+ÇëAÝѽœw6<±âŽL}å$2˜”\¥[Ñü÷iêÔçó'¥‡óÂËóÂßzctÝf<ä*µ]¢Çõ—=œ›úžÊå™uÇÃ'›­ØV
    ëž±»š=g'Egšk$Û­H¨P¾N¹"'ngP¥ûW=|uWÄ!ú:îÝ€>@}Ý„îÌs±¥L2kßD³.@¨_{Ù¯C"á31BJKÆKÙnÖØÁÒ;K–9±åQ²f`’oÞDq”ì÷wßFïuÅ£ï‡uÌ’u,Åàyb8ªs¸
    Pâé‰Ï:ö²¹–—ƒjï8$Sé\±Š¯ãò2AîÈ+Öe¾O’Ðj³,çÝ~Þ`”Ø·Pz‹5œÝÕÊ€mp1}(ÙÔê¦f–>†	'=¦œ‘zK,£
    ËZ',R/¸u0ÆÕÑïžUSúÙÛŒ÷cL””­gæk‹»LWÈA0Ö5„ùZ„e•‹
    ûŠ”HË1yG^wKh¤e&+¡,Žàçít›&	Ö'柉¡¼¹³>Ëp5Ësœo¬joÒ–Õ—íËØ£©+˜íúö?þSýxyœ6Z£Rí™äU¦Íîšnt…Žíðïi¾hõþÇ_©¿PK{6“A]Nÿ 	frontend/template/passwd.htmlUT	YªÑPʶ3Quxõ­VÛnã6}÷W˜±•í8ëëÈ~+оmߊłGaŠH*¶×ðwìõÇ:Ô-Nlm³¤ÐœÛ™™3ƒï÷µpÀîpZøR1¸;Žw(“n¡@.І«ØùƒÂÍ 1âG:T\©óÈ›j‹yµzuïM¹‚ƒ$áé6·¦Ö"J2v7Ù2rò+RŒúÊ(ó¯.<]…³¼`ÕTO=ÈxÖ±Ÿƒæ%ÌÃËŒ¸ÔÊÊ»vHšóft{;&­KÔ~2µT£Ã8«uê¥ÑãI@J
    ìK.›Pv”Ú¸©ö«û-*‹Î
    ¶0ÆI›§Ìè<Ý2-`½†‡ÇIW€ÖE¨9puRJ?ZoÑ×VCÆ•Ãö.4àDòðR–ükiV<Ç&G!Ÿ!UTø5+¹ó,ì…¸Vý½æÏ@oTI¥TµR‘
    -hôHSÉMÌ¡°˜­ÙŒmŽÇ/cö³)‘MN§xÆ7ñŒ4®¨:™k©;ƒßé‘á­M<«Uó·x`Ö	ãmi„æ%ƳâqCÙR"›Ñ(.lø†ŠõúglcP¢/Œ ª×â‹ÅÅ~:p¿¡C=ŒÅ¢1oøgï½5‚¨©v¤ðiWЧXEcÍ‚ãûŸ7€XˆqO~„œÖ¬%
    H1Ÿ¹ª1˜e•pÓæŒ…û{2X¨¹D¤^l-pêúàIŸKîNm?κÏZÍ7"c»ÜÑ™¶'žCmJÀ{ÓT‡÷<Ò•rðÇ6}kÛÙ´§Áˆê
    Jm|öø;€¾ª%C\:ÅwÇ[ÑD~ü¸ÒOSÊÚ;I[ò°!¾;l‡ï*ñ¯
    é?ðö<çäFq*ï»P¶‹‰Â'5moÝ{I¼z#E«›Se%íùëf­]†ýòi5SÙz	û LóùîûPK€6“A‰âú\ï#frontend/template/passwd_email.htmlUT	_ªÑPʶ3Quxõ¥“Qo›0Çßû)nHÕ	š&íDI¤½u/Ó´·=M_ði`3ûͪ~÷!YÒnÚK%À6gÿîÿ?Ûù;íJÞµ†›z}‘
    ÔÊV«m´¾È
    *=t¤Û +(òyu¼I>D‡׸Χc{6ݪWÑ–°oçJg­,ïI³YiÜR‰É~YbRuJUã*½ž¦£†¡[8½ƒÀ;	G­Òšl•Ígíã}¡Ê•wÕIéjç3_Wóå2>¼“ûdN6ª¡z—=`½E¦RŽ$Œƒ²!	èi3Nô³t!Üš,&©2<&::6éQG£|E6K%
    ³iÈaòø¯!…«õßÔQóq>ã#'­–‘t-S#Šj¬¨ šxw¦òæö•Êʼn7Ô`™Æûg"ÊŸž¾_EŸ±‡V…Ð;¯£Éó³”7=xj–þeâ?Èo®ó˜`T€Ñ‚G9(1(«¡éËOèj`'!U2m#°APe)[Çв0_€d¸õµo”÷€߇}"{f>è‡VÙàó:Qïfqzsß°{ˆÔ
    ë×{‘‡ƒá´B’ OصY*gC;fqÿZâÛ¼}ÅŸ†[ìàÓ¸à¬|£	\µ1h)ò©Œùt¸BrÕ§ã•ÿ
    PK
    ƒ6“A–ýq""*frontend/template/passwd_email_subject.txtUT	eªÑPʶ3Quxõ$modname: {{_("Password reset")}}
    PKØbBeö¬4
     frontend/template/passwd_ok.htmlUT	í“2Qʶ3QuxõSMÚ0½ó+¦Q©º+…@­¼–ªí¡—žú*'1Ø])¶w¡ˆÿÞqH`¡«ª'ñxÞø½y“Ãø.pÓzÈjæùL­2˜'‡)ÔÊ6O 8ky—Bć½âtPÛvüp¬m¥ÙæÁº
    s·[_…k‚Õ¬†“ãÅ=Ó\é2JCžÝ-%?F‘ÙbÏó÷—¬àéc}„Eø¼t.z+ªÌ7V
    ¨Ý"çÝ™Ò,”QRdp‡K÷´ÇÑ“°Qkiž‰Æ¬` I©¾þ×9¼^s}þŠŒLÁ3upü™½z›ðñüƒ{¦óåäý•?âÒ±øŸ.—÷Eþ‰œ@¨
    ®ŒÒÓ]Ò×ФWÄSW+pQÕýÛžj\3Kêd_® ‡&57æ@ùd_º{rUÙȈ¯Hwü2[ Øvåµðr“ÎòJU¿P6>’ñ¬S?5õ‹ë‡×1©µ5m“øõft?ÎTÚ”XÙÉTSŽN㼩R+T5ž8¦dÀî°äB²	EG¡}¶ßìïðTk4¦÷…1NÚ8ENëé¡ië5<=Oº´._`š¤vÜ—^£mt9—Û=W€»—¢ìèß
    ³æ[ô1fb©¤Ä¯YÉu
    `n.Äûß½Q-¤4P7RFÚ•ÀÛ‘¥›˜C¡1_³ÛœÏÿŒÙoªD6¹\âßÄ3²¦”S
    öÈžFl+QuþÑÒÆ{ˆxÖHÿ[<÷¬‹Dù¾TYÅKŒgÅ󆂧¸6£Q\h÷u	öñ1(Ñ*#å)Ó†‹†¡1n‘+ÞÏëR0þ òY$î‘fr-yŠ…’48ÖÌ!>~vR«È#á2pÁ¬Y+Y¿ÜsÙ sËÊÌLý6(
    ävÅ¥ï©âóGç5¤â‘ZqŒH²€ÖÀI_Œ² ªÕðàÁIr(2.Q[ð߈”îrú{EÑîÃ鉻,óàžJeAÆ-2aJÑc²Í·[©øÒ{’ìðx‚½ Ã4æ«?ë¢ÖXb™ foƒÛï=0ë¯Û€èÏ`Ñ6‚¯gáK”44«ª@(±ÐIdèWµ4ÕN¬cÕ¶þ-ùµ@“ƒŸk®‘ý<”˜Û•›Í¬k±PAò«Ò[eû4
    ­F½ã”ÿfl|PK6“A¿@‘Áe	frontend/template/signup.htmlUT	~ªÑPʶ3Quxõ¥UÛŽÛ6}÷W¸»Xˆl¯·N¯ì·í[€ô­
    JI„)Q )_bø;òAý±©ËjmcÛ $Ëœ™Ã33g¨ÓàÁb™`78Ím!<œG§ˆ¤Š·#OP»¥ÐØ£ÄÍ RÉNôPñ$eXU­`1¯/o–#e­*VðKo‰x¼Í´ªË$ˆ•TzwéÒ]Î|Ñcš*]qm(²Ý¥à‡`/›¯ày~¹É
    ž>UXt—6Dg¢\ÁxmÕ€Ú-iÚÚ”¦d	 Œ’";\º«1{Œ¶Â[ y"j³‚eí\
    õí=;¼nsmÅ?&ç‰Ú;þŽÌ‚nE|<ÿà®é|9¹ÜòGBZÿ3ä|Ù”áŸÀ	„ºðáÊ)Î1ÞÒ&}]CzE<µ
    ¹eUÛ¿ì±Â5³¤Nöõ
    zèRqcöTOöµÝ'U¥
    Œø†´ÇÇ®²9Š,·+¯…—›t–Wªú•ªñ©#ÎZõÓ`м¸yxkQYÓ‰ߌîïljŠëK;™jªÑqœÖel…*ÇÇ”Ø\H6¡ì(µ±¯ö›õ-+Æô±0ÆI“§Hé}ºÏEœÃz
    OÏ“¶
    „«˜:*„÷­×hk]BÊ¥ÁfÍ5àLvwS–-ý[iVâ>“~ÃM[wÊK#Â^ØhxÁK´Ÿ°¹ÑðŸ¨*…/¨w"î§Ó™çþŸRúq
    »Àã+ëYÞäòÓ
    ˆj:ÿË%²%ÐHú8 351 Y¶mhŽÓ[Þ¹CÅ	Ãô_PK‘6“AýÖ]«çé#frontend/template/signup_email.htmlUT	‚ªÑPʶ3Quxõ¥“MoÛ0†ïù¬b	`7qš´ƒëØ­»
    »;
    ²ÄØÄdÉ“˜¤^Ñÿ>)N´v)`[_ÔË祬òJYÉ}‡Ðp«×£26 …©W	šd=(*vB·E á<ò*Ùò&û˜—˜X㺜íE¸-®’á¾³ŽÖ0š°}OŠ›•ÂIÌƒÈ“Й—Bã*¿™
    Ó!v+«zð܇å¤J‘©‹ù¬{z¨„üY;»5*“V[W¸ºÏ—ËôøN6!s¶-é¾xD½C&)ÒO.$L½0>óèh3„yúE¾ºšf
    RÝðèä¸ÉO­p5™"«0{•†L4y˜Û"•ÕêoÕùÏøÄ™C£Â(´Sˆ4ÖT‘&î/(oïÞP.Îz±Ë<=<“@þüücœ|C-m‹WÉäå%”6?úéNvþeà?rßíÖ2”ž¡*DÒ¡`T)£ ÝzÓ!Ši§aOÜùš°Ýß,Ý;QÑá}$ê„÷{ëT¥¡ô0¯T/Oã,y?KóÛ»ôþ¨yQ-î_§ïNjUH‚.cÛyø”åhð-ßûŒ}Å_[ôQ·êáó¸ö`Mø&¸†1u)¨PÖs
    Ëi¼'á>O‡{=úPK
    “6“A„z.((*frontend/template/signup_email_subject.txtUT	†ªÑPʶ3Quxõ$modname: {{_("Sign up confirmation")}}
    PK–6“A„µŸ  frontend/template/signup_ok.htmlUT	‹ªÑPʶ3Quxõ}SMÚ0½ó+¦Q©º+…@­¼–ª½ôÒS{_9‰Á.þRlï.Eü÷ŽC]õà$Ï¿7or˜
    Ü´²šy>A«¦ÇÉa
    µ²Íg-ïRˆø°WœNjÛîá€޵­4Û3•¤„¡|ó=ž>gß­æÙÝñH
    FI臘®K3~&pwƒ!ETý[,Ï4càÈñ£¶­aš“B,)ŠE!t2!¢KÏ7ªÎ<É%ý%˜ÙÁÞÆˆ-û¨³U¯¿gô-MëFvš‰6pͤÁ<Ôœðè6¤¦SOŠšÂ‹øÐÅ&!ü,»ƒé	—D¹þ&6Þ”\Å•++ÞùØ48”Ùm†Ö¤¶üGçÆZ¿“HG?5Öí×ðhµcf?j¹èŒøËLüPK
    û}cBmodname/UT	ú¶3Qÿ¶3QuxõPK—®‹AHÏßISmodname/__init__.pyUT	}ñÇPʶ3QuxõSVHÎOÉÌK·R(-IÓµàRæRÉÉLNÍ+NåâŠO,-ÉÈ/ŠW°UPRÉKÌMU°QIÍMÌ̱SÊ–¥gæçA¥¡<%.PKz®‹A¤ëªØmodname/__init__.pycUT	HñÇPʶ3QuxõcþÌËõc±€d(`b .)Q@’‘!Š‘!…‰!Xƒ$#$Üòóœ‹lŠòóKrò“s2ò‹KìŠA*ôý4˜€Œ. ŸXZ’‘__Â
    æ–¥gæçÅÇk€,DÅú@B?´(­Ÿ˜“š¦ï’Zœ]’_ Ÿ›Ÿ’—˜›
    §ãã3ó2Kâãõ
    *K8€zl€¥9©v C@ö²1PKÇ}cB&ç0ïºû
    modname/config.pyUT	•¶3Qʶ3Quxõ½WÛnÔ0}ÏWX)ˆDZ¢$.•ö	Êׂx9ÉlkêØÁvÚ._ÏØI§Ýì$ÒVNâsÎŒçâÔG¤”§¤5«Ç/¢£èg%
    QÄêF*C¤îôÚß¾’bÅ.>R¥AEÑJÉš”ë’KYk'=ìCñJóš•&Š¢
    VĨõ˜dÕŠrA4Î1)D6݈Úr³|:éiDðBBwc/¦U‚Xvr‡œ:Ü–ÐL½ËÞËq¦”T÷´z“½{õ:o,+/B²b­gʆ,§âçô&|NÒ—) UòmàÿH£n±(Qa@Eo–e³ ©ÜB—Þ^?©kP8ƒYÒ^ä[\AÑ^Ä?P­)Å¡’ŰÄ5Æ»º o(×·—è%(½ŸŒGoP*¥¼bcR0®N®ÓIþ3ñC«UÞMoñŧnþ
    ½
    5½Ç}XkzXœv
    ¤¡æ’ÐBKÞ FsÉ4±¹x¤qR0˜2…'ÕÚ)()
    º)uf¹rí˜Ï?%þ™6¥IîRœç):—eqÚ×çÌ/	§uQQrµ ×§d¢d­-¦1Y8\êÄeI9 êW„ŠŠhƒë+½ëˆ æ#·V\È{G’›Y•
    b
    Ö)ï ‡ <ø¸ƒ>…ñ‹3\Ÿn»Ã¸—l5[¼lÕ°­
    •¯‚þ¢Z7„~eŽ‘
    çÜûî1ã~–TÔЂjXúB÷¶†©xÌ‚
    
    îç´Ãâ³#L]ÝØ6­`·Z–W`–wêj4?bðÉmÉ›[ðRê-*vÖŽ'OžgÇøsÏÈØÈLe˜˜(YŽÏž>9#Q¬Ú*a8Ϻ ¹f¿a‡ïOŽû.LOði¡LùJyÄç.ýø[c_R»,HÑû¦l•Ý`øûT»|“xëN3AÔ²²›Jf Ãä¢Z!0ýÙØ)õKrÏšsØCjΫ9W'C·xƒ®>¶•„OB@r¥0Cjñ`cs¯$={@Ìš¥ZßHUÍ+ˆ9¿9Ì*Œ{Ä᥄!,ÅWìǹ+íÓf\ÂLow9¾Ão0åñž­ñnýùÓÛÿÝPS¶ok8ì!­áÿÐÞàÎÖØOÞÖ"†ë¹Šð|ÄÜažü×6ºï;l¿2:sý¯eÔŸ , ?>l;;l:ÈÌ7ÂÃÌ™ÜQ'ˆ@£ÜŽèVïì¢gä¡>Å¿˜<$^
    I}í…'¸n™INÒèPKz®‹A–ôT'ö¸
    modname/config.pycUT	HñÇPʶ3Quxõ¥VKsãDî‘üˆ'qž„—Tù²É.—-  È'ï–«RT¹dÍ8‘-KF3ÞÄTöä-6{ã÷ðø3¹À	º[’	§Ý(¾™îùº§»gì¿ÊÿöÇ3Ò?ßÏñÕßc#ñ_@p>î8Yß‚s‹û6ôrpžÒ†—8¯2/Q¸2Ï9;«Pµÿ/þ5껦ŒÍÓVGyæÔ÷Œ—ÃOzóø~IT`3\†€7Œ,8û!\‡œèè–!®ƒâƆ³‘Ï«mÔÉS!-QØö/ž¹±V±Y ±èißøQø$Ž£¸N‹jÚƒÐ3EZ¹à°!¦ÏdXª¶;LÅ
    S;úÕê#7Pí£S¥»&êõ"º=5þõ˜ÁaÈ*M<¼Pf‹Ì‘a‹eQѶñ]Mµƒmbäà5À-ö-rÄ"„±É£·¼ˆŽÀz!Èš—§}ÀÀG9òögÆòScy8¦"ÈÅ„îH§$Š86tÊvæ³NÐÍHPΑ&ì¼ð
    à•€6FL	~xaÑVÊ2Èùlµy8N¡JUÆÐB-LsZ¹”r2‹ÐƒøL˜%UH­Y¤>qAÂËp”‡VîÉÿÃò«Ùœ"ÏY¥0Ç9rm_¹ÎøF†/3¾òÆ7gñMï2¾5‹o|ðä‰åö–;÷Xþn˵ŒÊõÑJ\8UºC}Bv§ïÐç÷qoÜc°:Vœ&º3!Jq5M/³É€ý;Ôîаg¨M˜¬WcÄÁ¬ª§
    «ýŸaµÔ°Ô©ï3§*@P	0”a±{eæñ7ÉÏfÛg(fôs¬yNúÖàÂPž^_*Wbªs	ñ¢¨ë«&VŠX®2×:n7Xkª=‡‡ž•–ÔBšÕú²ŒÆD¾Ådµ™n.­nD×0ZÞx¤¹>õ]sÉNä‡U$6kIRëHªËíó Æë³HEæMk¸½–tO’
    ÒÃh;ÆÒ¨BÉN"6™%}kãßK¾ÉeFõúkÒìæŸß(®­*t[’¬UºÆm¹ZaˆdsÐÑÆøá…æ=‰•ô5ŸƒÐ¿Ö‘×ÅQ²ò=¥KØyôÑãÇør­éËÆ)]6)Gé@tÓ8›FrÍø°tÇIÒO³¯¿Á4ëØtÓ‘
    Hî 䱆^Öoûd
    îùþ‡ú|}Z˜oÎ1¹–böɵ§’ËþÃ$éÕµŸ¤·óq–ãê­n ܉k"5åô²(–ð©—Æ‘t2ŽP’ó†^€‘q80~ÀÁâ<¦¦˜Ùá|‘{+Š\pê„Ë&­]Á¯XU»R,}úPKû}cBÔÀ¼ïmodname/storage.pyUT	ú¶3Qú¶3QuxõÅWmoÛ6þ®_qPZT.4¨º$¦M‚¶rÉ,ž
    ÷jE‰³»!@ýç«>Á”Ó’|MqÈX~«ÝÑP#i«X­C„£`©ì›ÃK¦Öš>^Þ¶öÛ–µCõ‡¥ãæŠu¾±_)t^ÅöÃ@¾?F•¾²æ;Çš+Òï]ÀsÃsÀ‘<mZéuRÍÆ0nKÆ«n1Km!ô8	UêÀ	\£º£»c¼dY‰Sóߨé…í¨ËÑö/k¤â%+YÙŸÌöûä
    àûxBsgl‘˪k“¿YÙ`¢f®ÆÝž­š¡Û‡þæ÷\$2û—’â‹ÇõÏ9üE¶ùë¶³¶6Ü;®d{XWDC™jÒ['yI•Dm·ÝJ|±£$dh€Sð¸èÅÆ^‘ä0oÇ'vÜH»>íŽÇ<åÝvåš~$Vé?;æ|NÔçn7ð!ÑÔHIì0–7ÔX”;6wE¨‘‡Å'(„¤ôqO¤?§vfÅà÷Zæ·hâÙc™=/×µ%ûÚ}$܇é”ìÜš`ì·òÁÀùÓŠŒO¼ZÛ¡Ê¿•Ò§‡ê	ÓFê§Ⱦïß3¶£šîÇÉc5½—v1ðqÂpú´÷ú$ð±çñcÙeã}î\›˜|\.ì²?„qLRì*²CaƒbÙÜ!{f™àQÑTo‹ƒ>ÙS©­»òú¦ââüìXYv8’®ÐŽDRè9ß¹åþG^’ëî`"0kÖ[en×	\Ñ©#`$°;É0¼BÙb°—bø1"¬°âJøb™ãH¡®¥(4´Äl¬,KÙZ|t<%Õ$‚Ë--öÞéÅÆ¿äž$?žžþ2‡ÎB*;"ͦaMÃX˺ã©x2Á[ËÆšˆ,0=j+âôÀqFﱜßý!{=ºÞ4}Ì ­˜ 1Q@«ìó;ƒ‘Ka9Ä@|¸æÐ"Ôöx„ð
    ú–¥ 7öæMˆdÏs¸C’ÃrD™¨Ë!’Ïv.zvù¶ÿ !´CÃ
    ´Ð
    ¨Þ;—ÙELwöï} ìÈÀþ"Ýé€	¸ü3ž”µ]þofjÿ¬½£d«äPcÒç|;ä°ÃÆöø
    Îví%0úPKz®‹A¾A<'D
    modname/storage.pycUT	HñÇPʶ3Quxõ­VKsEî]ɲ%K–üLœÄ•MRr‘’Iª¨BŠ<Àq%k'qP­vFöJ«YegT¶RÎ…¤ '.øü.üøü¡»÷a;•â ÕÎö<ú1=Ý_Oá¯Zåúï¿Ý÷!ýMá{_ý+6“m€° ´¡]€¯Õ*eˆ¯€ÕžK–h‘²àqºpÚÓÓ1¥tÚe¦m+0œ…ö,XÔ/@X…a
    Úµ¤kç ]QY‡~úóÐC;¦àÀs€oÚ JÔi/‚˜¹ÄÓ3ùô2ÉØn–qÁ?øÛjZHÚž='Ý6a´ËÃc-cû¦?#?Ì`“8ÁÝnÒLÓ&Nît”7”Ž©pg‰qˆÝdUÞè6Iú†ÊÞÆm©&màzâÏ¿ÚD±·+[£‰K*./%Ë'kˆ,âK6ë÷È€¾Ɔ~^â¶-øØ|Ï]ü?³àü@Ìú<ŠÙÙ´#¤ªPÓ+ß‘r†Þ@jÇCGxÆëzZ:>ö´£½žtzq4tüH)É«+ýdG±n9›Æ‰¥Ç
    ù•óÅÎÎ}çÃ÷¯:û{R920{2ÆyjQßp¢Ÿ„ŽKÇÈሃp’ˆÃ©)Z•dìffL‡ÌH×Éž³+MS˰·~-ߊöOœI CáÐT‹Uµâ±z0–ñ¤yQ6­k®_\ÏùFq x”GØÓSéû)9íʃµÔÙèæC Úð£
    žQ:(&‡ÐO’â[°žÁ ê—è0‹°‚GaÑ¢ièÏPôcÿ;̘»ÈZ†~…ù~:ÁWø/¾9xLÍÂa†(e¶›[z[vwëV~^w褮9ïèàoÜŸ>Kïl*#ãžçË|E³Š“.Å­©Ñ2oÒ•·)aP¦™Æ⇑’œ2¬ÇÔ‘zE—›g2òw_v
    %#…/aa÷&Û¾]3‡ôI[Ì,˧°zä…cÙ$}†¶H‡Ë„ïjSBb°Ï$錹•IvÓÜPš½H¼i"²éBWˆ}„
    ÖšµlÍ㻀íŠÍnOFŽT(yS§ëùÍ{æ$‹¢P³Óöco¤9!Ý%j–s´°²±7±”Îívš-Û˜,ï’òTJÖ…Ÿf6/8Tuvuë9ÔaÔhc[äv
    â1È^0® ¶»	¤øvÊO›¸E2^ZŒÞ˜"ƒÄw(o°Jà©ô9EYö*f
    ÊAwtlxr‡N
    #Ãa•C^qIH…üÁ\ä*S=Àùøg–XáHçª9UË©9x^`-xògÆRϧ95ÿzæbʬª`¨ø$釅'5íÀ2‹Ð_‚þ2!,?‡ZE#ÊÅTíèœeÏe6.d6","pXb²QT¨]­ÃÔÃ1Ùcz+N„½n(Ag%§xÎÇ•n¬‚ùiô—±ŒíøzJPQ™\vºcóz¹N€¥&	Þ¢}¯ålEX>ÆJ¡økddÊ5ºÕ,ï÷"m˜À²aG1-Üœë#Oë}Á¤?êä0@}ÉX¦•†Ë8¨(ÐÞ
    #Òÿ³›æi/y?¸¥Ì dˆ
    ÚdÈHNƒ
    ÊŠž{†Ü˨@RØù©¨V±º¤f›Ð{:yˆ‡sÆ÷uܵ̃¢V0"0
    žJ³r½ÂqžT:™JÆß#ÌN7Ì´9]oð¹”ÁzèdxíLv:Q,Ø”x,ÓËXw¼{œ"!C÷¤·5‡äÎEöEI¹û­îX,£l<úœˆ	«VÉ>o/áwÑ.[|V
    É)»Z¨Úu«–?%~/Ù[MÚŸKæ¹ÅÌÙ[X$Ýiˆå¶rMóCôCR•Üpü¢øÆ·Å3pì®Z²ø±›.mË%Á|Diín%A”#Ù´IW1“TîË™ãïz!† §_í/‚{ІÔìã
    /lUo¼$•pîØºz/RÉ ~?­4vÜ«dâÔ\£†ãj–&Q·1Å;|{Gq^O®é75É- »Vݪà©ãc7¬žô¹bùì¿PK¯‹ALÂT2	('modname/txdbapi.pyUT	kòÇPʶ3QuxõÅYmÛ¸þ®_A(]œ´U•&E‹bQ£-6-p@s×Ë]ûÅ5Z¢wu–%-){c÷ßofH½z±6I¯þbKÎû<3¤_±¤L³âáŽêýoþè½bu]ݽ~-Šø9;d•H3—òá5>½þkRgg±•")eº­x]Y\Ût/¯E»xmOU
    O[^¤ÛTä¢Þ+ïWy–ˆB	ÏËŽU)k¦žò¬¿k/ªùY_*¡©€- Ò¾dbÁÛˆý!´#@Ì?²})YuyÿÞZÓp°è
    ÀÏÎ~¢4øïOƒ×È4K?bnæ*&ŠÓWvq*T"³
    -áJœ×°wýÛÍ$€ì5°ÛM2¤žµ¢]¶ö{8æ#2¨¥¹rRvj_ƒ±`·›‡ýNŒgŸÈa{é®6ÝfÌ‚ÁUå]SÍ¡ßeI}O¿]Eu“‹—gܤò•º$eõðv¨}³2õЧ–¼³ª}oºäÆÏɨVï/fX?ü’ã<ðožŸÄߤ„Žæ¿ã5ßqxy£àвfÔ©B“`BðÙM瘰
    š}ßî~Ä~\ÒW89$K˜kÜê"¸ØÒ¸„¾5hnŃнAË^…™‰Û…‰yˆØ‹1w(0†œ=ª îíøÂÀ&AQÜ׸þ’¶,:@áR®l}›¹vG<Ác¦6‘)*ß8„
    Ë51OÓàz3\ȱ{©HƒFÈþ3ŠDúLe…ªy]òé±2þ¾–0ÿ€¿G0¬St}Ø43“%Þƒ›‚žÙã¦q;÷ãñàúÒÆÕX÷Ï¢?j
    Ìy³	,MÃq—΄ÚÖ:™‡9;•y×S÷Áe0œ,“Î)‡xÛ‰†ü”×îoñA#bÜÕÑv©d	Ó\}i™?rÕd¯;@ž;„6+Íft>Sv£{®ÇÕrVü,ŒÞ€‰XýƒŸí4ñ³ÔŸ«°fðk±+kƒ8Söp†<%¬9ò*Èùq—rv¸cè	;I"›qŽðZûÏB
    jz¨º¢YµÇÕßw^2‘§V¢é#eí`Py`â”1ˆ«~L
    v.Y¹Ò	ÂÌ臌^Íìœ
    I.¸ttÔ‰¢ó‰úNµ¹ŠÇ634²üØétÑZâ§ôÏEº,Ii}àÖÉ‚<ZbX-ðõÐÕ”UYá6¿ŒYÓÌt-ªs‡•Ž‹Aß³î|qÿá_ïìéâUºG#Ñ#x¬|†ë‘Y\º'ƒ¶4#ho÷£¨Ë´§#ÈÊ(™ä#g0‘¸ÇFMŠ·à;xŠÛm8#Ad[LÔâTiNƒ#Þa?2èÎI 2
    Wéç¯Ùú gÌÍHwjâEÇg3“ðW¯¯¾°©mtÓ2)# Ö©3D´)4é„s®d¿)>ò×È!ÙX®mp|lMÁD,.Hj4ǧd<ÁН¥á™»Äq8¸Q!;cR+†Sp £ÝäÊKð
    ?~äÇ?–Y è0„ ú(Áï4<«.sé9µŸ1âç.âˆÍRëç·gU‰"¥Ë» Ïi³v6°ñ¶?{¸õ´r­éí ÚEÖ!q š¢Fî`È*…VÕÆ³òNo½vV¶üÿ´/ưO«[÷aOИҹ.@x a'³B™Ëú=waÓ|ˆ°½D›¼_údOŽËñCñd	åU7êÃÙ'KƒqðnêEÓ `œtíÜþ2ÉŸ(väÄ}]nr’XÁW7
    äÂPüôU#›Ý,¨P§¨‡úÓÒÝ×–¨º¬ëßý¤TÄœdÄOsÒŒp¨1ÅLËuÎâýî2×”ÌÀ6®4't€IÍ_§ðÿ0$ 0n€3`×å€rãriÑa
    €‚ãÂ44$B=g¯
    d‰gô*bÆz÷G,ÏT­§¦õ›;göF°…aȈêÕ7ÐÐu<ÙÑ
    Nü=ÂÀÍWÔcéë¼À³<~éL ï.myƒ^I1ˆ4+xBí¿é÷Ùjµú|ÒŒ¢qh²Œè‚Ïîã’æ4ºõ—tÝ€ý[ßã&á|f°‘^8’5O	¶‘°‘en&:LtFѱˆ€ìþ™>+I‹ûe‹”Ú©‘ö¬ÿ¾mÆ@úc¼w/„¬êÈ'¢Æ—:‹ö°8{Y±À¢/bˆ¶À:ß©SŽfôކéûÏáb
    ûeÏ á£™Í+æúb
    g¿DV$ô⮤Õ$Ñg5§¦[^͆Åz!v&ÞÞEˆª§ïj`M_ÕŒßÔ¼ÇK¦ ocX¼´¡&婘Hœ¯W—Vò–Žô»õðÒpû¿lŠ|ŽIËÏ-üŒ”¼¢Ó
    ‰j;ØhéãkôjÅ™ùknÿ>ƒ:•PCVÎ_¨/Í_”¶ÝgR]»¸jÇÍ›N;R®8Dš?˜ô1mÚ/…x¾æ–™³¥÷3PKz®‹AïlÅàmA1modname/txdbapi.pycUT	HñÇPʶ3QuxõÅZËsÇyïž}`oä’°Drõ ´¢ÒbÅ–`94aY¶$HH„˜Þ,và‹ÙÅÌ,	T€*Çp9HlËv*|È!‡T%GÿNù’Ê%•{rË!·Üc¿_wÏîTÅÁ¢*¢vÐ=óuO÷÷ø}žÜO÷þã×4•ý/'¿?–_òwr	ä­ÚJ­em­Ö´k{jÍcÛSíœÚÉ«µ¼ÒèçT» vŠj­(ý¼
    ¦TPP?PêH©o¯© ˆÎZIc*,«
    ™°”=WAÙÝÏîN¨`B…¼;™ÝÄ2VjÓ²ÔÖoä¿åš–fZ”K#Xot[¦_ÀVÂ0N'¥õ¨ÝŠÂ•ßm¥áж¿Šm?K¨°ÉÐòd;?ð°1y]ÀnÑ(b›hÈ~òn?ÜåZÙíp;DcBù+µ²LÝÌË?O~÷ñ·ärà©¥ê²T­¶¤­Õ‘æœ:È©ø¸Jw+§êy)ÛÞ*€ +b™+Üìrm[zi§ÙÙÙi¥)î%»mÙñï³ÝìDQØLÓ¼m§Òˆ;OêfÚ‰÷ÉÂf/N:±¡‘f
    
    ö’°½AŠ`=jì„>˜Ç̈f»‘$5p•—ä¶\î|œ„qr§Ñ7î,…ÉvÚéÞÙéžýM÷(³ÛÝý´$CêõVÔJëuH7Á~”.ë9]Ô3^Ó³\Äï}<¿£Wä
    þ	E`"ª#ÜÍ©O´Ò–"¾­Ô°ìåSÝHRðî+Ü¢9‘Ká^Øì¥!ÛqØm7š!W¶¦ÍGvÛ;Ûí…Â9ÜkÄ›	´ýÍÑy÷¢1{säÉ‹çõi.|íwæB’Ü.©øÔVLÑ‘2ª4È Z‘5þ.³¸äœF|÷B¿èlÏ(ÝwÆìÍFøó¸{Ñ1äÌ\™4\y¿Æ´Õ‰>‡yŠ–33ÿ_¼¡Xµ‘Ýî÷ÙäCtd‘C2æøWp9'®ÌžàÊ{²†ëÿgðîm™“±ÂÁ‰v{'vÙŠRyIo9¯ÕO›Õ£?ó‹˜ÉãÚ›Xˆû9/JAo˜ÛÚ-}¤r®—G^’±èN»½ÞhnŸ×Үɛ£/
    8Ölw’ð¼Öíû½þº–k“Žõ:hëuúªz]ÆöÚÒ¥¡T–;QèOa—pYÀå\®:¹r×|E‘g^)ˆ·%0"ñ2¥r¾\9ü#?‹ƒVó‚!ÛžŠßQ)+!˶÷UÒ‹MId°M©Ô¼Ø¾lfç©OD˜ªÈ©~ˆ A„#!Ô¡Pä?[cÙ ’Ú*ãÅv¨6CKúÏnè¸:,¸öÚ‚b“jk
    
    ñ¨O™%Aà%·*{|_¨ƒ¡/ûØsìiÝÑ*Øß
    X±ˆG²YOíÝŒŽiµôðšúŽ.š½æÔ†ÑÔÂânE­Jw…ú·Ü¢ôÐL7aÔÛŠ…D› Lšq«L¨á½>4(ÍÑžÐy쵂=þmvÚ£aQJ)“ÿ!&(Qy‹úY]Ñ>5ÿÞ¾D‘Á:COªt7Ùovº›w“礳ÔHë$¬ÞHª­¤uÒê~˜V“^·+QUÜæ^û|­Íº-%û	ýÛcY°lXâ …À§~ç	u5é/ᶉ¼ãimvµÔj¦÷Ù÷±Bì}üÉÌt:mÿón.·úÛá^7Lc›sxoØhsºl¸&ñ5í^øµ8îĵ¢ Y¹Œø0sþ+¸¦øÖÑìõ®yc«Τąå┇“Ò›Õóü+÷õ|Žwy}K6p:öÿžr±?¢~ÅìÆDý9õç]Ô_pQ6À_#]ì/Ú/á¿d,bváóšÄÚ´½iS¹CÃÃT³ÈGç¬yAèDï•óÑ&K°¿»3‰ÂÞ€šbN2¥É–>;`}
    80Ê#âÑ»¯ªÕ“IDÛF
    ÃÔµ»Ñf˜›„‰„ç@t·$«ØIh¥õºŒi¤i\¯+ÍbcÎêßP\¼Äż>MÖªä`R~×5™—dÞßæsDŒÊ—,\gx”“ønÏ…ABTTñÛ.ÏÊÛŒ ¥`P¢Ÿ
    dƒ¨ÂÙI¹•Qà•š w䦖{p&ç³ý~\I$wêµq'”Öÿ
    X
    °õßp`ÐÿK*‹»ƒ°i쾕´¢$mD’±ÐH÷»¡ÑJ·¢ÍÐçMz—¯Žl’Xà·”u¡JL°â‰ñysz^¿h’6ÏʆrYWôŸ›Ã!–ç‡Ä5û2Q€ã+ý.ÙŸ¢¬ŽŒ?´½ò5ŸñÕÇk[`®QÏ%eƒÙz}Ó).Ùé¿åXmb£ŒI£ñç›`\8`Ê´5ã
    €ÒÆâF5½¡Xq}îâë›0,º^o«~8éd—•-¦Wyt"]øæ93®/#³²ÎÀÊNño^
    'z†…C¡î¢3™ñéAƒp£Ñk§£	û=«Óù£¥Á%a×ážaê猓‰ô^¶:cœÐJþ¸_Ò4Îcrv÷¯lkoƒ«“òG¢Yáâ_(Vå<õCe)ŽˆvŠmšñÀÀ
    æÙâ5i ‚Òÿ‰:zSÆ¿´Ô&ßÎ\WF.^Ö8°
    Îsrö#å=D›EHÈŒ,Çàò±2øýª›lÜÅëMxŒIŽ9×âК8³œ2T2ABq l.µ23J‹²‡ÅL†÷NKó¬5¡Åvcg=h¼yˆ98qðåÆS?yÆ¡ÿ:^3¡‡ÿe§þ;nß{ôºâÚCÿM•¥a#öY@eu0L{qÄ@äâ_Â8%gún~£‹ÇŸ¶÷Q‰7b[Ë6–}³úØxîãï³ÔÜq3ÏéËú¢„ç—ô}śҳ^EO÷-Ž»Áˆ|éô}k}(Q]†J?è×ƒ,Hòè]Œ`Ä_’„áYŸ1Z"¡PÒ Ûé’ÃþêùY¹ÿP†üX¹¬Eôœøà3ÀOvãÕ~LÖiëõ¿é#ãr
    äWqAâdr~;p$þ;¸¼‹Ëó¸WM¶wº¢‚ûþ¸ME¡b¢¾¿aê+ÆÞj´“ÐÿèÀÿO‡örfNQ†àå	jE]+OÉ/WÎMæfòE=37Wb"çûþÇK§ÓŽŸ*—vl2çØdÂÑb¶l.‚D¡È®vÝ1tCb•ÉNŽ´K;
    6MAwb€,?@6I²‚#›r‡ÓÈB˜G:•ÉéùLe¬iÎ6gÜ)ûl‡,ì=Â<6´¢€Í3ôöˆuñ2µÉ¼°þ5ìÊòÖì™ÇC­aÇoØeo®—>7eCq„1WÐý®§v+ê²øŸ¨lG˜RNÜšY—§áòä&UNhYqÚE  ÎüƒHôdDò]§Þ'ÝÄÅXä#çÿ{eÝZƒ˜Fç‰ÉáÂhß*šâî¦{£Áî­Ì@²¥œµ¬kBêð)Q¯‹„,™¤´Êú®WöŠzÁ Ô¸•ÊÉ·tÆ4ªì¡)Ü3û=ô\tœ†˜Âš	rWDý!=Ù¸ÔÞ×9:oRã/©Ã"‹kLää(‘€ÐEè­H(þK´ÅD¢Z Ú]U«Bƒ—PçÙ¿à
    2×ÒÃ+êP²Á1ÄPŠx]ú,#¢²Ø•òY"‚Mg”×fÖ­	†9–€æu§ncP·Ã’}÷Ö¤:(9œD1¢¨eT°`¤»¨N©­iìæØeT™Ë‚ù,'Ï(çÉ«­(í VU»‘¼R}?ŸT™ˆÞâ‰n»C:ºi.úæAÃ_©áìåÄ`NP)m%¾Ûw³ô¨Ëö±ÈÆ&‚9Šœ¡¸5@‘'Å÷EXð¨¸Ý…6pc…(¯ÛL«íF’ÖÍŽQÝkµWª¤Ú
    X–J®>r€Œšù\Ÿ¬Ù‹cáVíå‰Iö´û²%5ÇB<-ûº²QP°ÎÚÿG¸<ÂåÂi¦q#JÎS´3Ë3Ã$ærÛàìædåŸ0ÊzHÓç²_Y_ЦËuo+l‡û	×ËÞVG”…e†4Œª˜C÷n7Œî–¾ÑßÁ…æAKø¬Þ÷ƒžñaàé*[þØ%¿'æO`I®rR^ü?öh`!eB% s]z²A¸½7\K_0ÛÆ¶ýšg%ÜdY ¶ý¢ÏöW±‹2ñ¬ÄM°ôÈ7oäTÅ ÔJŸªùü0¹±.Y%ÿ¶!ÀEFœ\øûÔœè•ûNýÝÏë‚[7‰‘7MwzÝõýdµ«ëûb-|Ö‰ƒ0¶ÏضϨØIÓŠÒ(ÐxvZ)3K¶@Ç
    õƆØ_RΚUûMM•ÆsÒ®ܬXîFÜÙ4lØöŧRÜHN‡Ö6=¦ß?:Qyùbæ^:wÊz^»1’¾§#žTâø(Uø/dhsÉaÃîÏôƒä5ßLÍ#mÄ…æï‹ü?CÿO”­*Äa’•,GqˆœÌpþ¿Ðzn±¨¯	ô\áïY=ËŸëª^ÐR—¤UÑSª+¨Ü—&t”H(í¬ñÝC™×Ì©ët]¹~
    7È[_j
    >%—¹CRè“ÀjVúU»ú—\V®sJï’éSÏLuø„w8‘+er½lSÑî8‹ð ãÿX¸a›š¯öpTw^µE½žà„äºÎJ7&J¬*W¹Ÿ™6òÅ|ˆ¯wÂôQ'0z¿é=ÎÑýùÇÊ•g~¤Î¥<#J >ÅX|bUdHWôfò3“9}饙Ër}e¦œ‚®@óŽ)OWhpf“}òD0«Ê vRàÇ¢z NãõK2¨Ód£r£J$s%™µ2ê.,Uºš%o¼ÿxÈnüQíæ’Ñ«ê3­ÏSsîÉŒlAB€…ÌÇŒ=ͪjhT›^”Ön2	bÛÙRffÕߨ” ù´™`<û¸Yøc$2ަ?’»ª¡ìÓÔZ¤wU,óBß*³ï«`b†£G'ë ½žçÌfHŸÜnßÍLЬÄͽãð!KÁò´ÅxC<mEPÒV¼vbI®*@~#S`Q§ÅÁÅ=@Ôj¾zöø¥3Ëð(¸'ëë&~øs§x†‡´Á”ÐrÃ*1²"0ÕÅë­8I_×î77.Is&€Ü @›šËéˆä<€×Fá“{}þ›XVfPùLj¸PkEgÿg¸ü—¿V炸"Põ‚³˜¢„¼ÍËߢ™˜¥}
    î.oƒS¨B¤O$)ƒÛ!¾ýìÆ­$4ß+Í<ã§Q˜š­…øˆ§ø&¸às
    £oű™ßưXÂмY=››±|½òvåG¿PKšbBÎþ„e0modname/utils.pyUT	p–2Qʶ3Quxõ½VaoÛ6ý®_A(1&g¶C‘0mÝТÛ
    ,í>ÌðF:Ëœ(R%©ÅÞ°ÿ¾#EÛ’¬v[LŸlêñøîñÝ.H*3&ò;R›åô6¸.9KAhVVRò¶ñððÓöoºI¹ƒNiýÕ'xÜ.­Œ©8ÛýU;l]³,–J–Ä<1m ‹™0 â!,AyÌe)3AKˆµ‘Šæ°Å¼¤†>R
    ?³5A¤œjMÞAYqjàG<ÓQÆR3¾>a¾^"
    ÒHRÒˆ®Y+²ñ!H&A‹/YRÆIVƒÝBaâ/ÊYF
    Ø`L·
    ù’$ÉÁPcT’DørB,k¸‹«6û?öQ`j%ˆÏ-v±{ë*CÞÀ楤Üöê´N×§OR­Û$ö‡‘Yór'Ýw(æ+*2*jÝgü+|¨AÿÊÇJ ´ªÌMœÊ²b¢ð9þýíô÷ëé7q2š.¾¼ï/Ä~añÏW“Û//Ãñ>§j×§à~·È·‹`\R“®¢¸…Z$i­“ÔóññÖS “ŒnôìfB®®Š'ªrÝÓ'¶Û5`HR)†©Ù(á¤çþøO-Ek"jÒ¹«¡§Ã¢ý§•@>”@‹¦]KìéxŽrþ,å’®,Ÿôx‚-÷Q
    6”k.×ÝÆå”UGI;¢ÖæØËÛç2¥h©^NE~$cû:ìdhWŽ&לcƒEë©\´ÆvSº&FÀÖ ©PD½#ú¤˜Ä½ñæÒX~µN¬>ƒÎjæ¡„¶úZ@%hç9œo¤1Ú¤’Ø™õ¼µsñ|{q*áedx¡cšŒ²xeJ’Q—îÒuË™çÜoC¯ßt¡è”ó&üAÀm‹yÀ´˜®eGî…Ò
    Àßhb*úå°ƒQ1T
    –lmAÛAq§ïÂæï+%±á›M«1´÷bï;ÌÈŽ•o$;¦s1]ŽôH[õ»Ú™LšíÞˆ÷nâ|äLÀ÷(ð#M}NªÖé]I
    i¼ÛzL)Ó@~³“ÁÝzvÁT>Ô6>Áò!X¥Äù‰JΰoK¹­¤÷ñû÷¯_Fzæ¿'b…ó$vKÑÍ‹ñ8^Áz_ö>ÏÕÊ>ë»­7¯JlæQ1!}o Ù,´- ¹"·/¾¾¾nñ̈xΞä'Í…-¤qCc&wEÑG˜ÖøÑ¥ûÆ/¶¦?(‘¶kŠ®YùDººãŠñø<šöÞC.cÄ“ÝûQrœî¾añÞ)üV;K¤|/Ò°Bçè“ÑÇ}NêOVHᦕòF¥S4Róë…›ì "‡'³¹!À1)u¶xøî+„•?ïìJÈðë[ɃQõy,šÅiÞüPKz®‹Aí(+XWEmodname/utils.pycUT	HñÇPʶ3Quxõ½XënÜDžñîz³›[›&U«"äJ„n/I
    jE[ZTÚRŠzNÚK‹åؓԻ^{ñ̶YH$Dx^€¿¼	ÂðpαÇë\T!m`7;Û3ç|ç;»ò×tó?ÿÂgù§¿›ðS¿ÁÀgcíbÎYÛ2óJi^eíª™×X»Fs‹E6ëÕY»Î8WX4Áz
    ÖnÀq•É&[ç,¨±ŸÛaìëö$l&-Ö™bi:W/ÎͰ`‚ÉI…t7\m5AÙðoøŽç0¹ŒFŽ»‹§ÇÅcc—Mì1¾Àíƒ
    !r´ŒØœ1Â*lÎ
    îGÒKsÃHJê£ðÜhÕ’Ê×`u¡îÙ=êæ®[<ØuVAìÈ‹7ÈQↅQ⃮º’A7òŠ8=¦m³¹3ȳÙ.7F¾˜_PQ4 94îæÁ”ôúa$V'qÀD*Þa‡›±k8w¢wm>k5*j£Ö¨»W¥RaS;±«{¢ýÛ×;@ã «yfÆt]cÒf:Æ&gräç\X3—5ŒÄ6’¦‘ÔdÒH&ŒdÊHF2	Ñ$U¦½BAnø5u­àSQ£—
    >mQbQê,Ú„B×-v¼Ä)J1‹jQee9¥¶Í£„*­ž/ÉS„Êí§r=ÜÜStÒ•c…ÿÑ}û@“õiÁ9k8G	Ë7u]¼àWFŠZ`ÚÖOœÅ,*ð)DH§Ê:µ,cBoÊ;6Û&Ï"Fu¶U!Œ¶³ã	Öi°­*.ºc±g<—5IF-ñ|!Ÿ¤;¦pà†/Ô%hi;3(*RRp?_9•Øi¦ò»Ö*Ê“¤N/žL¿•’]j©BªÆmá9è±*õBϰûµ‘ƒAÐäéÓÏî§>ïËxuõ	Sè]’‡RÇK¹)ÎGö4{Ùƒò”UD*§© 0…	ôÕˆøAÓÞÔ£eÙ5'p@ƒ5z­;7föÁö%sDýÓü˜5ÏOIê|W”`è«‹E”äÁq!g
    ‰YÖ}†ÞŠéYÊôFdÎc²ø‡K&ûÊM@J•²î•1³nÙÎlñç£h˜hºtâþFýù~îg	aÛB§÷òT€ÆÊH
    ôÕ`JÙî,A´JšÞÈRâ¶X.B|Äå]˜鞤@’ÑsÉÃ@lr·žk,¯-²ù´jÚ^ÑùW ­$¶¢íÂÕÏ)TØÉ¸bß@J-˜:WRuã
    :haÖ›…„Ìðæñ	Ë”0Fó"HÂU1¡Žm*ÁLa˜ذu´ìãÿSô!$/ña6Šþ£ðËu@ø™†ŽŒ»´×¸‹9‡²º‹fñ¬ø€Ëöhïö*:Y2ø:txFΖŒ¤Òi2ÜbæeYy”·¨s+z»,çS…À«úiÒ‡ç¬áÈ3´hGa,ojkžßUâ*žýìö]Å]vݾ ‹’
    ZÛÖ¬
    4œ™³çšðµ[X1>P‘_ó¼åìá.ë 'Kò×rMàÏXô,¢_C–Árk™Æy¤‘¥¹2ËJ')çÝ0aåÀßb$Nd]iýA–ØAF.¥^	¨À½øžAèçØC…
    IFrýâÅðë§yû¥©âP”_j^Âà_Úã@«g³…Šm] Â „ƒBÞ‚çA´ù]k•i•~ëÄYLkµÃíXŠn•Í9HÏí‡ÆüSšòºT^glâ%þŽ*z‹®WžáéÛ!í
    Ý€1Œa³þBóü–¦{9'ˆöl²&GCOzPÆtÐdÄTá¤qîvh½uhŠtúZ/%Úa=o3¼'AgŒ,«Zj5hë÷‘	ÿu?„›:Ï3ÖötÃ`7hØh7˜Ó4*W†0šMA#Ù§²^Åå“0Ÿc½’«)~ˆúÜi«ªåh^CøÔ©ã<1Q~OB8ÙzE,eGˆ“ÞÒýÕwÓn¸/©ËC÷w¬+q5Vçe(OEäòòòzd™
    jZˆà-nø+Ä;š³¬qÙÑÚ™Hi£b¨¦ë®Ü•™üfm¬÷ƒh̼g‘«˜“F+ÌŽwYré7];;²\3q5Kv5Eöb–ìňUÐñ‰Y»;ýzhئ-‹åŽ^…-I„uî±-V¼0êkÐ.9c{ž£ÝyAUº{|ÓkWmÅÈÉïƒÁoÈ‹ûº¯%z|):wAí·V>ÃòçÑø°þègè.ÈžÏÏ®z~æÔ4GþAÎzYÁ°-ÆÎî­/=¿‡¼g3vSîc”#IwØE‚ì³f˘Y#é;¤Pd¼®ôE&3Ì¿5ŒúÍ3ì¶Ù
    Û–ÿ®Þ»/;s[Ðy¥_ðæï÷¿*Hä·®òjXÛX}ÔþÂT}Þ´
    Á0iæ°¥fª#˜+Ƕ1?WvǵùÙ÷%{ÆvÀË`ÆÇí¹3ÞíÕî8·î†Ó3Ì4mÔã0c6¶9Øœk}vö.Hºƒt¯ÙÄî¸q2&	½£La*")/±sÅTŒ×uW¨°itÛ@’1˜T;Økl\ÉÒîÍŠh(>Œöl}/×cY >	§Zù©Ž½‘ûi.c¹§lâ"ëØñXdaƒ™Åz,¾=*áÕ×ѰuCöƒ þ™³2lˆÓg)„óŠsP
    õD~Û€`1ÖC¦ä¸÷tƒ-ÍA›BèJ¾Œz2hQ ¡&*ކ@{^·v¯¹ë¹_7¥NW³“Çu®˜SÚ”†ZÂýª‘Õ°ðŠž{ùõï//#ÍØºÍ±[cwWqÊ‹‚éÀ¡PQ-ºšP&¹E6]2~R¢÷ùïÃkR à žÏÜʱ¾”k¸tb‘â	Í2!×oKo'.ø‚fݺ¦tŽÀÐ×4I÷ª#Bß2ü›Èúög¬ºkÇßë,­Û'Í~va¬*˜w ýàVX¸Ðƒÿ•~
    ¢ööçg£°š=ËÆàaýdm­«ÚÝ,¼& à
    Vù©uµnáà¸ViÉïBÃõF„Áò¿Ï–ųeF–ß_/_]/oÈß_½¶¨Zg7qìç™Ñó#‰šº3g¸•ö@§:¤P)òWó½S[ÿ¬LAðp
    Iƒ÷ƒR?£È³táÞ‘D?9߯˜%Ã`ƒlF'šqbíÑdn\Êkb£áû–§éê7Ê?”_V£ÌʧÙ(§®MŒj(n
    Øç!ig¢{÷Ö-ݺýãýZ
    pžsó‘E­Ÿ}Lî|°Îã3Ó Âzt²5kÎöÛ3Me|w”`uÈïûÚP!vì„ÈœÁc<*û µÍ©Î]ãÇb¦Ô—c£›uKXå/ùˆîè¼ï9Óÿ/F›M¢ôŽp¶_?‰6%#S_šîB‡£¶ˆøhg1-l½pÖx&š*öFHPû̃I§ó
    ¤ú>‚m½¶4P³£#cÞ{¹H“Ó=(aO©C'™uGF'eÑüŽ%±˜ŸñNý器¯Ñ£ž¢ùÀý	¿{™ûüHÐõÊüÃÁåîœ^äæ³öî|õ­…®CÐ9l€Êù>U:ÞOµïõÃ?Ï~œáéŸÆk¿œ®ÓFÙÓì:ßmýx¦W7OÏü(ìy:¥ÙÖ뚺³Ò_×úwëÜÏ¢|
    vækÎCdnçÍ&m“s€å½»› ›Èdž×ó×íbèS¯4uݶ¨á"üÏ_¡Ê/ªl>é~n¨ÒeãW¨rªä&I6ˆåPyñ`¨²IŸ
    ª<—;g ÊgU6Þ6UþPKz®‹ASÁöðU7*modname/views.pycUT	HñÇPʶ3QuxõÕZ[sÅîž]íju¿_,ÙÛS’€1ÂŒŒ
    "d3Úé•f5;»ž™ErŪ"åTå9/y¡òž*þ •×ä)•—¼ä!UÉKªò’óîž½H„Rørvº§OßÎí;g7÷ïÑ¡Úßÿp«*ÌŸý¿Fÿ“Ϥ>ý“"b#{–bñϹ®ç|×ó@×sAlìsQlíó Ø´Ï%±QâgG„C¢1,6†…D;'ÂÑ£ºá˜hŒ‹qÝá„hLŠIÝ.p{JlLév‘ÛÓbcšÚƒBM
    5%j´·’ø¥…øpcFøCxQ“ÂÎzg…?b{G³Þ9áñ$ã¢>ÏóŒgï„?Ñûn2{·(ü©ÞwÓÙ»3Ÿ±+Íf½KŸëå˜ÏÞ-ãÎÖË$¤à?ôg­L’é_ÕT¬›cØÜÒ"š6coKéŽa"¯z‰zÓ‹üвŒYWI4£w‚½ Ò½cDn«F+ôRõF B?áoE¾Ú3ì™ò€áU¬u†ˆ"µ‘+mú ù!'Üõ²C¯«Ò0 ñ˜ž$ò@ˆºñ"?H0=”BF£Üv !hÓ¤ëeœp-"ºê{ÉöfÓ‹}nØÚåí´òÞnµÇ*J+íDÅé uÄÊbUMÓ7ˆÅÜB[Qa­Œ1I¾‡5Þ'ÎdÕUmõºJvÒfkµÑô#¯¡²ÏOµ›\nÝçËßRé%0øbJr©ÚÓØ?‹ÙR‰³éCÖs$hg_r3+ Å€›úäà_ã¶šIšbÊ •”L›OÎǪ±¸x¸;`Ïv;n+VˆXÝk+b/ñs£™ªJÐrÑbù¸¸Ê[>ÉU¸¸ü§Á™ç{XÊ—ñÀ"¨T0´RI‡¸A¬íš.”“ù:ë~ÃÅq	íâY*°âßô¢­#5wò æ’ÂâuÕ1â+XñÝ…ÂJHT•Gæ¹Sq™—ƒX!5‡ôWY‚ˆzAÔ‹VÞƒFÀ4îa—vwdÒvY`.¬›Ô‘ÚKY8«å’µðêýjØŒ‹>lVéŠÒY­ˆ•¤Ýj5ãTùý"I'YÍé"ÓP•j³¹(bq'ì„ÛÊ#³HX lE˜Ê‹·Ú
    2¥.%)™]Ò,¾:‰¼°Ðsàd™ÌÊigV²»Ó 3 硸˜ïƒ“*Sœùºõ"ýÊáXå8›)‡b3…u²–Ø62E9àÙ`¤
    7´w#e¹AF[ÚÒÅ7¡pnÅL’/&ðÙŸÓÞ
    {É6.ß…ãu—AØ¥•ÑI¥òrfÂβ,z%‚EÙçíªÍt”>½vºM*T)fø§#°Gˆá¥.á:^©V›í(íWÞŠëÎAq)¶>Ud»³¢Óÿƒ>Löê‰`x`ãÂ[Bt¼Ÿ¹ôL|±!ïì³WˆoZ¡èMà…ú	²,Ò(¸úhž‡ "Oä2;RbØÝ&·àÞÝ*ªáá‹~ǶëéËbÍqǬÙÖÚaÈn˜/”ù³.wâb—€Sò|‘°Ã®Q˜­Ôj¹E«o:ÊLá:BåÅ•î¨Ë¾¥ËiäÍl'ÕÌ7À9Ær‘óNA‚IôtÀ
    ç_òë
    '¹e„co~€4Ý|ª=·Ð¨D{õˆ.Gì;‚xºýâ¿Ö‡1Œð[	ïäEü{È|‹ÖÍA¼W1ã(*ÁRê‚"ô,zg£MÖÇÄNÎLNÍʘ W-
    Û-åmψí¡íѺ_Hì„Ew‡.èJWÏïÍ“„p±·/$±?àýV >oJ`wÂl >‰ë¹¦øPÔ3-â¿H<ÌàÕIÁ
    sOŠ«æÔ}Ó³•@×\ÆÄ>c%w¡Á-aÂ\}â…¯í~MÅñ6¥nyI²ëW®t7ž
    ÀÀ=²eÙ)õkŽ®Q³á¥Õm~n·|¸Êòœ56`Ø}
    „­¦¡£34aL¬ÒvÝñBÂs0¦5ŠýîùÌ
    A’4Z:P‘¶Ð'„
    îKÂ`SãÝmï
    »ƒmµç[@…Ãö¶Ìއ5K¥ºMž0›÷‰rÇa¾kÖîÓö:]\½ûÈ•ú~\ýmp^³f.rHަž’ãlòSÔš–rœÚSÔ*ÈÇMψþt–œ’–£Nyø`ðry<ótXðº—z›”"­{5¥/}œå‘zÍÃM¯º“œ"V½@¯ƒ³V¥#,Œ€rD]¶¢÷[ý‘’}Õ®Ö‡ZUW ëa”‚÷g`%ã`/QÈÂâå¾$l®“„‘ËL»d†ÆÈÆ”àNÚo»Õ1ú²€!Ä…Nô8=Èò‘e_#6û¼36 œÉÙÓúÒúFi{œ.—î(ˆápnÆŽ.Ž®~ãèòÚÑå8$2G—çÒ‰b>ÇœÇãb0Ày HþñØ5ž)b’9ê¾_AYäXÓ›¨!š_+…	që+çŠØ´ÈO“K¬³³§0¥µRRx"Ÿ¯‹-„ (Qïuý’b˜Ù4b—>Ç4}š»€ˆð€ƒr-´gѦÐeÛs&bÌ‹ú‚¨/¢TCqßB!ÆŸÁÌ[\~ÁÜgD}IÔ—Q€Aó,J/Hç„¿€ JsÖÏc>Ñ8#ü%,ï/‹tEÔá·gùí£¢þ±çŠÂ~QÜ_fwÖzPÄœwöÅÝè#‘O/ˆ!ÿÓ‘û%‹ûêq HL¦—D½,üsØôƒ.ޝïüW\ýSâ°>!üfÌXÍèGl¾j-úQ¶èK6–„j˹ÖÐŒÜÕ£ìœs„öó„>ayj/HÒäˆðpž)Dæ ‚ÿeíˆ>>‹Ô -elšó|÷r‚7
    î5N†t1‡œd³ÁÕífPÕ	1B¡É£)/NbÑMÑ.Hv‡|¶ËÏpV½wçÃ…˜«[ŠRôVüÒç"ËYxºàÓ÷¤¿ ¡xlJ¹ü*m8‘ØöbJÌFÛiíé2¬H›Ï÷iÓÚãfƒ›º°×Þ¬«jš,dâÐW\1/.§{i·Ӣ‰G|²ox§ld">öTâBr	ÂS†)%m®$*òW4÷
    s£óBò<ý7´ÈO–²ÌêM;´›4wtF˨‡1Ìs6 èÐI¸H؈h,Å“²n¢˜—¸¯‹ƒàé†ÈJzJîw[*Z_¿ÉPž`ó~j0M½DzNà~j´Ã4`Er1zÝ
    b­WÕf£¤:f©bOXç}Ï,=ð‘ö%pl× ±›£Â#+]
    «VèU•û»HJªÃ[«â‰6.äi‹|Þ.îõ½ªj¥A3ÒÚ®H÷•Ë!&̇j$[å!t5&Ðø
    °‹-b‡iê6mGÌôDå _ÓŽp›3ëX¼V0Ÿãú37o>—és.·H1}:7Dø‡s#”šÿúC‹0¡““+„'ðÎÕÉlo)IkÌË™’]§…è^!†O…!¨>va¹·¢ï–¢>,çb¯ÿ3ìö'q»ÁØâðî	§»9½Ø-g{®«ë°¦Óq‚Nb¤	–:X:ˆ”€óØÊÞaÈëwÇgwDò×csÑpÏ`î´H@CŸø3ZHô5y¬Eî}&34÷W0ȋʈ =­a~ ŸqNÅ©B¤“ü0j ˜…(3D‰xt2è˜Ùg;XÑÌëtæ­åtá%oxŽw‚ALjg®#fÞм–Z¡G„…2î™Wƒ£PÙfº=3Á¬ˆ>³B&¼Ò•$£¦À53„âN*ÞÁthÃwî½NèIë’#µË5¶Îjî0ày@®‚ !íÔ키âQ< ”uR7&rÖ•–u•‹½nÿvæö9‹oÙí´vý“ºù?ƒs-sóãýî=kÈ©O³ûÖî~œœú‚,9Ãr†ýœôô›ßÀŒtuq]d¸«cWmjh}#³oȘ1sº$©ò/QªâH¥ú[ ±®7­ûév3r³ßØ™Cè/‘F;—Ûi&.ÿ%Ÿi7£)ü…ÜÀãí—Nüû;þJˆ«Œ<ñµœônÙc^Õ¿·ziÚÎ0"ûþB9Œ‚LäÇçJsÅå–o-_›+þPKh°‹AÖx²Ôòmodname/web.pyUT	ôóÇPʶ3Quxõ…SMÛ ½ó+Ùƒ³M½×*R+í¡‘ú¥®zZEhc‰ܤ­úß‹íµÓ$‹í7ï1ñÌ‚
    +•©×´‰ÕÛwdAî´h¢ÎúHÅO¡­ÁR[ÏÑ#=лƒ•H{Æ…Çp9%¬©T=Ï•!ZõÈy‚;øI”!„
    !ÐGç’?ˆÊšbb¡œàË5¡)$V”seT伨«U_–WJcOj£Eéû>Y:ðyþ(¦‚‘¿#5ú4/#ØFáÙ[½~w
    (7FâéCÖ,Wÿ4˜ú¡(ß,“2>&ä:_BØï,x9òŸäº„°‰¹,zÌÈuIPµi\¾N–<'ä»»­Pæ\±1·¶™Új_š¶\‚£œùÚ!7Œ5èþ·L'æJåQµæÂKñ›5^³5M¾c4‘O.ã)²?“º[2¾.è&Ÿ­~!ÍëƤª(Ëw÷Œ*ÓÍàzfd¾|é’×#ž"LÐÝ̇nT_fnÿ¥‹Á†dËéUž1ÒÆQÙ¯eëÕ¤öµÅGÖlgË€±q±ÉAƒÏZÞös›Öhœ34Ñbà°£}Nxe\^þòlå‡e]Ñûûìä/PKz®‹A^nb£òÎmodname/web.pycUT	HñÇPʶ3Quxõ¥SÍrÓ@ÖÚŽóÓ¤I§åPàÈÁÀLsãÔaøéÎ@é8Ó¹x¶ö&±ëØïš¶3í©\yÞGá’§¥GHbå“VÒê“dû× §~|?aõ±ñy…^ ˆð' ˜Š[
    ¶ µaéÀÔAºÍz¦­Zw uaÙ†iõ(I’.Ì0Ü…/7Ÿ¦]J:ñÚxYü?Gž@hZ(>Çê\ת‹"̳Y<¯õŠiä©ÔêC|gf-¯‹"Ciâ<[Ó!÷7Dg…jˆàíSü	g->”õ-ùþ¤;Wn\[T¥± ±afuÂc›q‹±ÃØeÜbÜfì2î0n3î2î0î1îrHú Æ{mÀ‹™
    ó.\ÛõáÊ‚3Êoì·	É,hßpcȰæ$[dÆšÞJ›°„	
    auÅ6$;p…V›¾Z<°‰7BÚG†Z2Ö=’©ÌæcoïùÓZ¤^œæ²Œt‡T†y•Ýæ€xžUÅ-Ž³Ú‰p^­œ
    ©õy¤]έæ2Õö18³°¶Ú¿ªLy iÊT…4C	–y”É¥2]>›ÇY@žT›¬L®t(å=@Õç-é£(d©UPïŽõ0‹ÔÅ;™E©*ù¦÷È´ÑG¼\+ªq“ö«&ܘ¨ö	Ò;)î[³»adùXývÌh,D-¼Ó3y1^Íiý„öŠKÿ!º?¦¸'üf­¾¸ÿÝvúb$zöÀꉞðîL‰‚€‡˜¸ÂÅ8åmAÿTÝñˆâ¨k®ðèžÛj${uËým²oܱc¼O£ãµª7nx»¤{Ú䥜+†¿CÂþïb¹û5û—T¿˜Ø8k„mÙPK¹}cBŠ+‚Mmodname.confUT	~¶3Qʶ3Quxõ}‘ÝnÂ0…ïýHÛui‡O‚På6.D$u»Œîé—ð£m ¦\ùó±ÏI²ŠŠ0ÔÛÉj¢q 8î
    EIu‡NØÕ-óÞÒ<×µPI}þÖ]ä^©7pÜ¢£: îòø…OÏDQm{×=cPòÁ¡ÞO_ÉI>œUÚõØ82I5’€AÅ…R¹ôä9ŽË¤d¬ÜJŸ&CoÂíþt•©ú0=	‹Ì`Ç’qõ²(Êt*3˜Ïï`›×”	²û•ýª29ù1åºuz´j6+ç0¤ïèÑç
    3ùä˜GŒ¿ïcïÇÃëf—`ûmmÓ;ź¼}^æDäÑ>J$^C±Í‚¢eõú¶ur‘þŸïPKz®‹AY³vúÏimodname.sqlUT	HñÇPʶ3QuxõmANÄ0E÷=ÅߥE±$¤n¸Iå6îŒEW‰[èíɈ¡KÛïÿ'Ùg]àÉh¤Âü)Å
    üãÞ7Sf2¾·õ9S2PX²løÌš¾ÏO0…›UÝ›:Q¸h1ñœLfaq‡)»¾Yï¥?²ª
    ŒzÈmƒšƒ$«ŠŒ¤†´V-­¦ƒ¤šˆµô±RW~àH°Qž.”Û—çîHÈB¥|øƒ9ýÇÌuE>°×+ö‘2Ðd²1FÕÀ”þ´Ô¯DÊ;ÞyoÅwM×7_PK€0B5Uö¨ª:	README.mdUT	ÿ÷Pʶ3QuxõVm‹ÛFþ®_1pIŠ%Ó~*G/·¶9šžS(¤á¼Ò®­%­º»:[¥?¾Ïì‹íËÔœ-ÍÎ<óÌÌ3{AÍÜtfPe-œ’4ZóI5¾(Ÿ÷­v„?ß*rf²¢ÆHEfC—Éð~½
    Æ—ü~ºT½ÐÝó¢(..èEm&ø
    ~6ºSÔ
    GµR5V	xbò¦^7¢ëfªç‡6Æ>R7á/ô!mL×™½¶WEQÒzí¼°¾ríz}EN÷#â¹Vu¹ÆêÑ“7LÂa©TgÆ^
    žœ²Ê—½‘!"mØÿ×ÛÉ£b]ìUýµ£K>vz(ôjÁHupèäà‚GbÔС\L*`wáŒTµéAû”S´7@cirj3åd](ÀÝMÆŠâg€>Ï™Ï!žôqý^;/©Ž5(KÐ7~ë%ê“ÉB¶Õ‹qìPÆ@Ó‡xòctŒÈŸ&çÉNUË\¥"bB‰åÔð©Ç0r˜¿ÂÃü)ËÎl™³ë僰KüXæ&©ðãëQË“5⟬ñæsëo'tÌ£iU³Ë/c=J®G%£Ÿ½é§E6V%›8¯@†éõ?Éu˜§£dá”çwgù-RrœfˆŸ"ac³ÎÉtC¢P$èèŒEq8ø±g$È~a`Õß“¶™Žwê&Ž(¾@,³#áÏ}¡¸–«ÈδouÓ²¿=&TÖ1·˜ß«@ð!6‚À†;M‹
    "«Ô\Y]X"¢t¢­”Õ,¢;î¡P!z`Cs
    EÉáÌYè±7¬õEñ®SLxõ_)3"dP€šÏɬ(µQutÉõ\%ßâÛ¸ˆŠeÀ,ÎÒ(œÛ+]†rÃ@‡À/'Ÿf+wþ1q¨Ï …•À¹uð@ØiL‘CÂRá8Ê’uÞrâ3æº\»[®Cñb£=¨Ô÷'‡iÁâu*A7›`.ÍðÄ/Â×=-jôA¶Ff9Lšlg6~ŠVQ(“/Æö­p ìu\ÌÕõ4ø‰ÜŒ†é±c¤	˜dVŒ¾„‡ì–’·ëV4ôÛŠþ\Ð͈ڴÝbS€DþáWÓ«—Ví?>m½¯–ËþÐtÕ|M5jÞ/[Ô0xþÂÓö½œ¹”Ï/x!$4lþ”ã$½{p«Å¢Àd$Ö‹a†:þúþöm áÝì[“/.¨]¼“ÎXÁ€ÒqMç±Y~Wµ¾ïŽÛ¿Ç™þ¥1zΚ·öFâÛCL%o-1l'”è:¡)KDéKu=ùMù#žìÔÌ}}õýâ*å1^¨á¬=Ž6šëGÖEε1Í„àXõåZ!¡H
    ôeNÙ>ýýË;–—‰ï~¸›©ƒ÷&V›gÁæAŸ‡úQ9ž˜Šþ—ÁÝòí«ûÛ7«Õ‹_Þ¬–ÑÞm7½?wC¥ù‡z(ÀıÀ¹ñl(cÅk\ƒh5ŠA»6V{iÚNÐÖë ÂiREnÈÄžÌ2ò
    Ь­.¨>m†ÉZVÜ&¾rá¡Z·@–»=hLí	NådÓÄáÊËk]x¦ÂDìq3³ÒqZÅÕॎ‘VQ-z1SlVUœn…Ó}ÄUûtÙÍ(àŠ/	¦gÐU¼‚XkÇ÷æI#ã|6Ém¬>,“ų³[mUüPK
    Z‡*Bscripts/UT	›9ïPÿ¶3QuxõPKV°‹A¶Ç梄­scripts/cookie_secret.pyUT	ÓóÇPʶ3QuxõeÁ
    ƒ0Dïû[ÓƒRª‘"ø-K¢k»P×`Á¿o¤ôÔ9Ì1—&…­q¢
    ëŽþˆ¯UÁà¸N¢ÏSœï0p}ËÈ@¿n
    ܵ¿”’Lyš‘HíÂD8X-V”¨è³ü&Ë/X»®eÍ/\žl}Z[Vµ;"¼áYUðPKW‡*B€®ÂøÆscripts/debian-init.dUT	–9ïPʶ3Quxõ•Tao›0ýî_q#h[+M¶nS*6e
    IÑÚ€H:iš¦Ê'±D1Ó¤jûßw˜$ÔVª#%¾»çwwïìtÞY×<±ò%!N~:cwîÄá×È#ð3qÇ#–÷a·ô%ô†a4`·ÏXdL%ͤÂè:ãýH«ÓÛÐÍiËÆ!èÁ'øÇ{Áí18‚.|ÁÐt)2iYf<•\$}P9PÈYvÇC"¹dÞ‡±H¬Øµ
    ±LQ7NVk$Ä5ÍTïÎdØèœøƒÙ™myfÅ"¤±•£Ný†­Ìʹ‹ÔÜáÀ¹ð&öÖ¶äŠç2"dê¿ÝSçjè¶•R¹´¤°v¢n£“Á…cï¼Äÿ3;ó&ª(]o0ôu½¶NQ"hxˆï3û.rîNgÎÄÖº½¯æ~º9õ&#w¼ÏhÕVYƒŠdN|w8rÏÛº£™•I”òˆœ{ã‹Eƒ.2ð}ÌöÐô?™8$s¦1i9B.1n¯V+#¢’’qà]úµYÉzåù³©­ÞªÆw
    
    †åÌyÌ0Õ¦pôaúoSèî’ ¾T
    Á¨U‰Ä9±íJ1ôÐ4E‹#ÄM%›FŸÃ_xÆU]ð祿€	`áR€æôá”&$°5ÉvhS+qk.¡Kæ¼æ‹`o$/‘ñ…Rd÷	–W	°öþþéV޼|2W›÷òñ¶¬FVOŠ'hÏ4¡dRÇ_'ŽƒÝà{3¦·`”úì´nÈa»½Y™GðU³ºŽú³[|áuÛ.iœ1ÝÞ·ÒT(c²ÈJTÙK‹¤w|üM(Š8*ÅQ¥?ËR#¼y*%é«
    ‰4}“B¿P¡ºG{:½EŠDÈWeh5Òœ¦ë]
    x²-é@Ü¿òœœ(„H·€ºù:ž±=ŽäÒÃF—9]°>XL†O¸4£Ö<(šÇ’ýq“ïIƒïï{Ku…7ä,§!!ÊwDþPKZ‡*BñøicÄ	scripts/debian-multicore-init.dUT	›9ïPʶ3QuxõÍUmoÛ6þÎ_q•…ví ÉΚ
    u ž­¤ÂRÙC$ŒDÇBR©8Yšÿ¾#eKrÞ€}›
    Ø<Þñ¹ãsÏI½7ÞEƽ*W„ôz=ø#8
    #£p?‡SÒƒY)n²”É!4ûJ¤œ^1ôÆìºÊJ–:sEKebl›æù®Kõé­k–´ÊUçìÁ/ðöwœÛcЇüŠ®ùJ”Ê™0™”Y¡2Á‡` $P¬¼É‚ƒZ1Hî’\pkva\¬4ГõçPˆZš»Ѥss2-¾ø^%K/	Í=‰L
    ;¶1ëÍÆÓÆà‚LFÁ×iäomO­3©RBæAüW8Î&aì{U+O	¯!uëF_¿Ù%³¿_¦‘)ʶ;CÛn]„ÝHtvHÍ£hÌýWñâl6þ§Oý9ç‹ ò­ÁÞon¿‹Œ§Ñax´›Ãk-]•›¾$£Ù£î»ŽévGE‘g	Õ,r‚~½^;)U”ÅÓ“YkbKÆ+–|Q)Ó5©»y¶må²â‰F¥(A ¿„¤’J\eÿt¦•ad•¬€JlµR¿„ñìèr™ñLݹ„dKøoÀ¹E	Ö]Ó‘–¬XAOã!Œ)§€Ý²¤R¬‰v-w›)eÖ⥰ÃÓK œƒD‰òRÁd¥0Ü=ý(Ç?½‡û-ªÃÁ‚µòõ]5ÇuA#iÎ8dÎ%»Æù±íFçèNÑò7R°ío­0Nëýpr¾wCK¯¬øc è0Ö-²Ô„OÚð\\¾Ž^“z6-æ¾åTX›–	8ú.F"à8½Ìr†ÅmjÁ=<¿ÙÛ$lÆãµîë<:éeíZá¸C‹M-8	.j™[¦œÎT úÿçÁ)1.Ó5W bÙjΙ_ƒ£¡š²:¢rœfmng0t½¿›•‘mc¹¦%­ZL˜QÍKFÓ;@Î96×Ú8K¦ª²ŽÔÚx¶·¿ÿ,\"ª<Õ‚3y
    ®LíŒM›oØ>èüŽCö(™Ì+P·µõ$Ø)4å(E|*´ÌÃÛÏà¥ìÆã¾6ÕhirÖL6,ò ÇA¯Nƒ(ŠÿÉ4<¯§äô'Ê)†AGTÿ]7\¨×5³áõ)±	•,ÛXÈÙÔø¾-wKºÙ980¢Ø´Miý%ëbú]Ò0òü±ÆdñZò5l}‰I/Ù<¦O‹ÖMµî
    ÌñǦ®>¿Ý«QêÇíœIšböúä_PKV°‹A4¶ã~scripts/localefix.pyUT	ÓóÇPʶ3QuxõUÛŽƒ †ïy
    Лӭ¦w	OÒv‰Å¡K¢`l×·_ «fçjò¾É»zòXß­Á>é8‡ogIA•ëŒ}4t
    úx&ùèë3ŒEX6?û¨j*¥m’
    A™”Ck¬”¬!4NÀù½¤Ñ¦‡¥"u«ÏËé¶Ú­÷±kjqG¶µ»Øs#X¾$ÊìÁ‚14ÿs‰ïC|…d½7$¦Ã•rÃ	÷‚ñË»J¶³´Ãœ¥ÆFÎÆÌšX0•Ÿî™¸^Oì3‹åšü;ì¦P½Ðà›­»JõÎ/É/PKO°‹A,SJ(›Ãstart.shUT	ÆóÇPʶ3QuxõE=‚0E÷þŠg`2iatq`sRúñˆ/)¯M[ƒü{Ïoνա1ÄÑù)*Ȉm¢XrãÐfILE9˜B‚˜‚{ÙBÁaôa‘Kß1¤ý}¸Ü®}7\Σ£ÄzF¨ëve¡\H»ZAF8m€ôЪ=ððC&¨çྺZШ.FOVï·Òþ;xÚöâPK8Š2BÜ/¦˜Ù frontend/template/error_all.htmlUT	ÊùPʶ3Quxõ]RMOã0½÷WÌZ*‰ôkW Ò¸ízC=8ñ$ö®[¶-QÿûN>š‡Xö˼yÏoÜÌkáeÜãB†J3˜ŸgÍ2mò ‘t-”øpÒ˜Î2#NÐÐÀr!T]FÁØ6+{Ü~3‚©bø5ü9ÏhYä¦\ÕÔ¶oRqWª:†ðC0Û;FïJÃÃjd!§ ¿wøI•°š”þ*²àLý­în¬Aĵ*	×X„Qi"ËõÀ/H<òêc¸ß\.«ÉM$Q•2İîéÉrH‹‚¤|Ûü¦L-/±KT¨7È5÷þ‘b¬M8‘ëô·1ÆúÉ’ö-d/¥šFÂÒ¦a/»Ý˜û˜>\
    µð¯,7Ùþ.ç
    ½'I¶¿9Ÿ“¥MÛhÈŒ*€á1GÉ‚ªp2,iÑèC Iú…ÀìP‚ò°s‡Î}{õĦÏ-wm{Ä!yCŸs‹×>¸ÑÕ$µ¿é¸î5M	%Ò‘·Ï©Æt}$6½Ê=máÉT–×'znëM§›,‰’Î>eýPK
    z®‹A”‰•$
    ¤.gitignoreUTHñÇPuxõPK
    z®‹A	íA]frontend/UTHñÇPuxõPK
    z®‹AíA frontend/locale/UTHñÇPuxõPK
    z®‹AíAêfrontend/locale/es_ES/UTHñÇPuxõPK
    ¢:“A"íA:frontend/locale/es_ES/LC_MESSAGES/UT/±ÑPuxõPK¨:“Aý{ÇÆ(d,¤–frontend/locale/es_ES/LC_MESSAGES/modname.moUT;±ÑPuxõPK¡:“A÷9D_ÅR,¤$	frontend/locale/es_ES/LC_MESSAGES/modname.poUT.±ÑPuxõPK
    z®‹AíAOfrontend/locale/pt_BR/UTHñÇPuxõPK
    ¢:“A"íAŸfrontend/locale/pt_BR/LC_MESSAGES/UT0±ÑPuxõPK¥:“ALÍÚ
    ,¤ûfrontend/locale/pt_BR/LC_MESSAGES/modname.moUT6±ÑPuxõPKr:“AV÷˜”˜È,¤bfrontend/locale/pt_BR/LC_MESSAGES/modname.poUT×°ÑPuxõPK
    y°‹AíA`frontend/static/UTôÇPuxõPKz®‹A3ÜÈñá~¤ªfrontend/static/favicon.icoUTHñÇPuxõPK
    y°‹A¹P„¤à frontend/static/legal.txtUTôÇPuxõPK
    ØbBíAO!frontend/template/UTî“2QuxõPKX6“A‹[	
    ¤›!frontend/template/account.htmlUTªÑPuxõPKe6“AÖe;óGì¤	%frontend/template/admin.htmlUT-ªÑPuxõPKh6“A_+Ö¦‰¤¦'frontend/template/base.htmlUT4ªÑPuxõPKn6“AûÔ7ˆñX ¤¡)frontend/template/dashboard.htmlUT@ªÑPuxõPKs6“A`ÙKϰ¨	¤ì*frontend/template/index.htmlUTJªÑPuxõPK{6“A]Nÿ 	¤ò.frontend/template/passwd.htmlUTYªÑPuxõPK€6“A‰âú\ï#¤È2frontend/template/passwd_email.htmlUT_ªÑPuxõPK
    ƒ6“A–ýq""*¤5frontend/template/passwd_email_subject.txtUTeªÑPuxõPKØbBeö¬4
     ¤š5frontend/template/passwd_ok.htmlUTí“2QuxõPK‹6“AŽüvnŒ\¤þ7frontend/template/signin.htmlUTuªÑPuxõPK6“A¿@‘Áe	¤á;frontend/template/signup.htmlUT~ªÑPuxõPK‘6“AýÖ]«çé#¤ù?frontend/template/signup_email.htmlUT‚ªÑPuxõPK
    “6“A„z.((*¤=Bfrontend/template/signup_email_subject.txtUT†ªÑPuxõPK–6“A„µŸ  ¤ÉBfrontend/template/signup_ok.htmlUT‹ªÑPuxõPK
    û}cBíA3Emodname/UTú¶3QuxõPK—®‹AHÏßIS¤uEmodname/__init__.pyUT}ñÇPuxõPKz®‹A¤ëªØ¤Fmodname/__init__.pycUTHñÇPuxõPKÇ}cB&ç0ïºû
    ¤Gmodname/config.pyUT•¶3QuxõPKz®‹A–ôT'ö¸
    ¤Kmodname/config.pycUTHñÇPuxõPKû}cBÔÀ¼ï¤JPmodname/storage.pyUTú¶3QuxõPKz®‹A¾A<'D
    ¤RUmodname/storage.pycUTHñÇPuxõPK¯‹ALÂT2	('¤ã[modname/txdbapi.pyUTkòÇPuxõPKz®‹AïlÅàmA1¤Iemodname/txdbapi.pycUTHñÇPuxõPKšbBÎþ„e0¤wmodname/utils.pyUTp–2QuxõPKz®‹Aí(+XWE¤j{modname/utils.pycUTHñÇPuxõPKšbB3	š-¤ƒmodname/views.pyUTp–2QuxõPKz®‹ASÁöðU7*¤n‹modname/views.pycUTHñÇPuxõPKh°‹AÖx²Ôò¤šmodname/web.pyUTôóÇPuxõPKz®‹A^nb£òΤ*œmodname/web.pycUTHñÇPuxõPK¹}cBŠ+‚M¤eŸmodname.confUT~¶3QuxõPKz®‹AY³vúÏi¤Æ modname.sqlUTHñÇPuxõPK€0B5Uö¨ª:	¤Ú¡README.mdUTÿ÷PuxõPK
    Z‡*BíAǧscripts/UT›9ïPuxõPKV°‹A¶Ç梄­¤	¨scripts/cookie_secret.pyUTÓóÇPuxõPKW‡*B€®ÂøÆ¤ß¨scripts/debian-init.dUT–9ïPuxõPKZ‡*BñøicÄ	¤&¬scripts/debian-multicore-init.dUT›9ïPuxõPKV°‹A4¶ã~¤C°scripts/localefix.pyUTÓóÇPuxõPKO°‹A,SJ(›Ãít±start.shUTÆóÇPuxõPK8Š2BÜ/¦˜Ù ¤Q²frontend/template/error_all.htmlUTÊùPuxõPK66ÊC´python-cyclone-1.1/cyclone/__init__.py0000644000175000017500000000127712124336260017125 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    __author__ = "Alexandre Fiori"
    __version__ = version = "1.1"
    python-cyclone-1.1/cyclone/escape.py0000644000175000017500000002611712124336260016626 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """Escaping/unescaping methods for HTML, JSON, URLs, and others.
    
    Also includes a few other miscellaneous string manipulation functions that
    have crept in over time.
    """
    
    from __future__ import absolute_import, division, with_statement
    
    import htmlentitydefs
    import re
    import urllib
    
    from cyclone.util import basestring_type
    from cyclone.util import bytes_type
    from cyclone.util import unicode_type
    
    try:
        from urlparse import parse_qs  # Python 2.6+
    except ImportError:
        from cgi import parse_qs
    
    # json module is in the standard library as of python 2.6; fall back to
    # simplejson if present for older versions.
    try:
        import json
        assert hasattr(json, "loads") and hasattr(json, "dumps")
        _json_decode = json.loads
        _json_encode = json.dumps
    except Exception:
        try:
            import simplejson
            _json_decode = lambda s: simplejson.loads(_unicode(s))
            _json_encode = lambda v: simplejson.dumps(v)
        except ImportError:
            try:
                # For Google AppEngine
                from django.utils import simplejson
                _json_decode = lambda s: simplejson.loads(_unicode(s))
                _json_encode = lambda v: simplejson.dumps(v)
            except ImportError:
                def _json_decode(s):
                    raise NotImplementedError(
                        "A JSON parser is required, e.g., simplejson at "
                        "http://pypi.python.org/pypi/simplejson/")
                _json_encode = _json_decode
    
    
    _XHTML_ESCAPE_RE = re.compile('[&<>"]')
    _XHTML_ESCAPE_DICT = {'&': '&', '<': '<', '>': '>', '"': '"'}
    
    
    def xhtml_escape(value):
        """Escapes a string so it is valid within XML or XHTML."""
        return _XHTML_ESCAPE_RE.sub(lambda match:
                        _XHTML_ESCAPE_DICT[match.group(0)], to_basestring(value))
    
    
    def xhtml_unescape(value):
        """Un-escapes an XML-escaped string."""
        return re.sub(r"&(#?)(\w+?);", _convert_entity, _unicode(value))
    
    
    def json_encode(value):
        """JSON-encodes the given Python object."""
        # JSON permits but does not require forward slashes to be escaped.
        # This is useful when json data is emitted in a  tags from prematurely terminating
        # the javscript.  Some json libraries do this escaping by default,
        # although python's standard library does not, so we do it here.
        # http://stackoverflow.com/questions/1580647/\
        #       json-why-are-forward-slashes-escaped
        return _json_encode(recursive_unicode(value)).replace("?@\[\]^`{|}~\s]))"""
                                    r"""|(?:\((?:[^\s&()]|&|")*\)))+)"""))
    
    
    def linkify(text, shorten=False, extra_params="",
                require_protocol=False, permitted_protocols=["http", "https"]):
        """Converts plain text into HTML with links.
    
        For example: ``linkify("Hello http://cyclone.io!")`` would return
        ``Hello http://cyclone.io!``
    
        Parameters:
    
        shorten: Long urls will be shortened for display.
    
        extra_params: Extra text to include in the link tag, or a callable
            taking the link as an argument and returning the extra text
            e.g. ``linkify(text, extra_params='rel="nofollow" class="external"')``,
            or::
    
                def extra_params_cb(url):
                    if url.startswith("http://example.com"):
                        return 'class="internal"'
                    else:
                        return 'class="external" rel="nofollow"'
                linkify(text, extra_params=extra_params_cb)
    
        require_protocol: Only linkify urls which include a protocol. If this is
            False, urls such as www.facebook.com will also be linkified.
    
        permitted_protocols: List (or set) of protocols which should be linkified,
            e.g. linkify(text, permitted_protocols=["http", "ftp", "mailto"]).
            It is very unsafe to include protocols such as "javascript".
        """
        if extra_params and not callable(extra_params):
            extra_params = " " + extra_params.strip()
    
        def make_link(m):
            url = m.group(1)
            proto = m.group(2)
            if require_protocol and not proto:
                return url  # not protocol, no linkify
    
            if proto and proto not in permitted_protocols:
                return url  # bad protocol, no linkify
    
            href = m.group(1)
            if not proto:
                href = "http://" + href   # no proto specified, use http
    
            if callable(extra_params):
                params = " " + extra_params(href).strip()
            else:
                params = extra_params
    
            # clip long urls. max_len is just an approximation
            max_len = 30
            if shorten and len(url) > max_len:
                before_clip = url
                if proto:
                    proto_len = len(proto) + 1 + len(m.group(3) or "")  # +1 for :
                else:
                    proto_len = 0
    
                parts = url[proto_len:].split("/")
                if len(parts) > 1:
                    # Grab the whole host part plus the first bit of the path
                    # The path is usually not that interesting once shortened
                    # (no more slug, etc), so it really just provides a little
                    # extra indication of shortening.
                    url = url[:proto_len] + parts[0] + "/" + \
                            parts[1][:8].split('?')[0].split('.')[0]
    
                if len(url) > max_len * 1.5:  # still too long
                    url = url[:max_len]
    
                if url != before_clip:
                    amp = url.rfind('&')
                    # avoid splitting html char entities
                    if amp > max_len - 5:
                        url = url[:amp]
                    url += "..."
    
                    if len(url) >= len(before_clip):
                        url = before_clip
                    else:
                        # full url is visible on mouse-over (for those who don't
                        # have a status bar, such as Safari by default)
                        params += ' title="%s"' % href
    
            return ('%s' %
                    (href, params, url)).decode("unicode_escape")
    
        # First HTML-escape so that our strings are all safe.
        # The regex is modified to avoid character entites other than & so
        # that we won't pick up ", etc.
        text = _unicode(xhtml_escape(text))
        return _URL_RE.sub(make_link, text)
    
    
    def _convert_entity(m):
        if m.group(1) == "#":
            try:
                return unichr(int(m.group(2)))
            except ValueError:
                return "&#%s;" % m.group(2)
        try:
            return _HTML_UNICODE_MAP[m.group(2)]
        except KeyError:
            return "&%s;" % m.group(2)
    
    
    def _build_unicode_map():
        unicode_map = {}
        for name, value in htmlentitydefs.name2codepoint.items():
            unicode_map[name] = unichr(value)
        return unicode_map
    
    _HTML_UNICODE_MAP = _build_unicode_map()
    python-cyclone-1.1/cyclone/locale.py0000644000175000017500000005137112124336260016625 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """Translation methods for generating localized strings.
    
    To load a locale and generate a translated string::
    
        user_locale = locale.get("es_LA")
        print user_locale.translate("Sign out")
    
    locale.get() returns the closest matching locale, not necessarily the
    specific locale you requested. You can support pluralization with
    additional arguments to translate(), e.g.::
    
        people = [...]
        message = user_locale.translate(
            "%(list)s is online", "%(list)s are online", len(people))
        print message % {"list": user_locale.list(people)}
    
    The first string is chosen if len(people) == 1, otherwise the second
    string is chosen.
    
    Applications should call one of load_translations (which uses a simple
    CSV format) or load_gettext_translations (which uses the .mo format
    supported by gettext and related tools).  If neither method is called,
    the locale.translate method will simply return the original string.
    """
    
    from __future__ import absolute_import, division, with_statement
    
    import csv
    import datetime
    import os
    import re
    
    from twisted.python import log
    from twisted.internet import reactor
    
    _default_locale = "en_US"
    _translations = {}
    _supported_locales = frozenset([_default_locale])
    _use_gettext = False
    
    
    def get(*locale_codes):
        """Returns the closest match for the given locale codes.
    
        We iterate over all given locale codes in order. If we have a tight
        or a loose match for the code (e.g., "en" for "en_US"), we return
        the locale. Otherwise we move to the next code in the list.
    
        By default we return en_US if no translations are found for any of
        the specified locales. You can change the default locale with
        set_default_locale() below.
        """
        return Locale.get_closest(*locale_codes)
    
    
    def set_default_locale(code):
        """Sets the default locale, used in get_closest_locale().
    
        The default locale is assumed to be the language used for all strings
        in the system. The translations loaded from disk are mappings from
        the default locale to the destination locale. Consequently, you don't
        need to create a translation file for the default locale.
        """
        global _default_locale
        global _supported_locales
        _default_locale = code
        _supported_locales = frozenset(list(_translations.keys()) +
                                        [_default_locale])
    
    
    def load_translations(directory):
        u"""Loads translations from CSV files in a directory.
    
        Translations are strings with optional Python-style named placeholders
        (e.g., "My name is %(name)s") and their associated translations.
    
        The directory should have translation files of the form LOCALE.csv,
        e.g. es_GT.csv. The CSV files should have two or three columns: string,
        translation, and an optional plural indicator. Plural indicators should
        be one of "plural" or "singular". A given string can have both singular
        and plural forms. For example "%(name)s liked this" may have a
        different verb conjugation depending on whether %(name)s is one
        name or a list of names. There should be two rows in the CSV file for
        that string, one with plural indicator "singular", and one "plural".
        For strings with no verbs that would change on translation, simply
        use "unknown" or the empty string (or don't include the column at all).
    
        The file is read using the csv module in the default "excel" dialect.
        In this format there should not be spaces after the commas.
    
        Example translation es_LA.csv:
    
            "I love you","Te amo"
            "%(name)s liked this","A %(name)s les gust\u00f3 esto","plural"
            "%(name)s liked this","A %(name)s le gust\u00f3 esto","singular"
    
        """
        global _translations
        global _supported_locales
        _translations = {}
        for path in os.listdir(directory):
            if not path.endswith(".csv"):
                continue
            locale, extension = path.split(".")
            if not re.match("[a-z]+(_[A-Z]+)?$", locale):
                log.msg("Unrecognized locale %r (path: %s)" %
                        (locale, os.path.join(directory, path)))
                continue
            f = open(os.path.join(directory, path), "r")
            _translations[locale] = {}
            for i, row in enumerate(csv.reader(f)):
                if not row or len(row) < 2:
                    continue
                row = [c.decode("utf-8").strip() for c in row]
                english, translation = row[:2]
                if len(row) > 2:
                    plural = row[2] or "unknown"
                else:
                    plural = "unknown"
                if plural not in ("plural", "singular", "unknown"):
                    log.msg("Unrecognized plural indicator %r in %s line %d" %
                            (plural, path, i + 1))
                    continue
                _translations[locale].setdefault(plural, {})[english] = translation
            f.close()
        _supported_locales = frozenset(list(_translations.keys()) +
                                            [_default_locale])
        log.msg("Supported locales: %s" % sorted(_supported_locales))
    
    
    def load_gettext_translations(directory, domain):
        """Loads translations from gettext's locale tree
    
        Locale tree is similar to system's /usr/share/locale, like:
    
        {directory}/{lang}/LC_MESSAGES/{domain}.mo
    
        Three steps are required to have you app translated:
    
        1. Generate POT translation file
            xgettext --language=Python --keyword=_:1,2 -d cyclone file1.py fileN..
    
        2. Merge against existing POT file:
            msgmerge old.po cyclone.po > new.po
    
        3. Compile:
            msgfmt cyclone.po -o {directory}/pt_BR/LC_MESSAGES/cyclone.mo
        """
        import gettext
        global _translations
        global _supported_locales
        global _use_gettext
        _translations = {}
        for lang in os.listdir(directory):
            if lang.startswith('.'):
                continue  # skip .svn, etc
            if os.path.isfile(os.path.join(directory, lang)):
                continue
            try:
                os.stat(os.path.join(directory, lang,
                        "LC_MESSAGES", domain + ".mo"))
                _translations[lang] = gettext.translation(domain, directory,
                                                          languages=[lang])
            except Exception, e:
                # These messages are not printed in twistd, because it's
                # before the log is open.
                print("Cannot load translation for '%s': %s" % (lang, str(e)))
                #log.msg("Cannot load translation for '%s': %s" % (lang, str(e)))
                continue
        _supported_locales = frozenset(list(_translations.keys()) +
                                        [_default_locale])
        _use_gettext = True
        reactor.callWhenRunning(log.msg,
                            "Supported locales: %s" % sorted(_supported_locales))
    
    
    def get_supported_locales():
        """Returns a list of all the supported locale codes."""
        return _supported_locales
    
    
    class Locale(object):
        """Object representing a locale.
    
        After calling one of `load_translations` or `load_gettext_translations`,
        call `get` or `get_closest` to get a Locale object.
        """
        @classmethod
        def get_closest(cls, *locale_codes):
            """Returns the closest match for the given locale code."""
            for code in locale_codes:
                if not code:
                    continue
                code = code.replace("-", "_")
                parts = code.split("_")
                if len(parts) > 2:
                    continue
                elif len(parts) == 2:
                    code = parts[0].lower() + "_" + parts[1].upper()
                if code in _supported_locales:
                    return cls.get(code)
                if parts[0].lower() in _supported_locales:
                    return cls.get(parts[0].lower())
            return cls.get(_default_locale)
    
        @classmethod
        def get(cls, code):
            """Returns the Locale for the given locale code.
    
            If it is not supported, we raise an exception.
            """
            if not hasattr(cls, "_cache"):
                cls._cache = {}
            if code not in cls._cache:
                assert code in _supported_locales
                translations = _translations.get(code, None)
                if translations is None:
                    locale = CSVLocale(code, {})
                elif _use_gettext:
                    locale = GettextLocale(code, translations)
                else:
                    locale = CSVLocale(code, translations)
                cls._cache[code] = locale
            return cls._cache[code]
    
        def __init__(self, code, translations):
            self.code = code
            self.name = LOCALE_NAMES.get(code, {}).get("name", u"Unknown")
            self.rtl = False
            for prefix in ["fa", "ar", "he"]:
                if self.code.startswith(prefix):
                    self.rtl = True
                    break
            self.translations = translations
    
            # Initialize strings for date formatting
            _ = self.translate
            self._months = [
                _("January"), _("February"), _("March"), _("April"),
                _("May"), _("June"), _("July"), _("August"),
                _("September"), _("October"), _("November"), _("December")]
            self._weekdays = [
                _("Monday"), _("Tuesday"), _("Wednesday"), _("Thursday"),
                _("Friday"), _("Saturday"), _("Sunday")]
    
        def translate(self, message, plural_message=None, count=None):
            """Returns the translation for the given message for this locale.
    
            If plural_message is given, you must also provide count. We return
            plural_message when count != 1, and we return the singular form
            for the given message when count == 1.
            """
            raise NotImplementedError()
    
        def format_date(self, date, gmt_offset=0, relative=True, shorter=False,
                        full_format=False):
            """Formats the given date (which should be GMT).
    
            By default, we return a relative time (e.g., "2 minutes ago"). You
            can return an absolute date string with relative=False.
    
            You can force a full format date ("July 10, 1980") with
            full_format=True.
    
            This method is primarily intended for dates in the past.
            For dates in the future, we fall back to full format.
            """
            if self.code.startswith("ru"):
                relative = False
            if type(date) in (int, long, float):
                date = datetime.datetime.utcfromtimestamp(date)
            now = datetime.datetime.utcnow()
            if date > now:
                if relative and (date - now).seconds < 60:
                    # Due to click skew, things are some things slightly
                    # in the future. Round timestamps in the immediate
                    # future down to now in relative mode.
                    date = now
                else:
                    # Otherwise, future dates always use the full format.
                    full_format = True
            local_date = date - datetime.timedelta(minutes=gmt_offset)
            local_now = now - datetime.timedelta(minutes=gmt_offset)
            local_yesterday = local_now - datetime.timedelta(hours=24)
            difference = now - date
            seconds = difference.seconds
            days = difference.days
    
            _ = self.translate
            format = None
            if not full_format:
                if relative and days == 0:
                    if seconds < 50:
                        return _("1 second ago", "%(seconds)d seconds ago",
                                 seconds) % {"seconds": seconds}
    
                    if seconds < 50 * 60:
                        minutes = round(seconds / 60.0)
                        return _("1 minute ago", "%(minutes)d minutes ago",
                                 minutes) % {"minutes": minutes}
    
                    hours = round(seconds / (60.0 * 60))
                    return _("1 hour ago", "%(hours)d hours ago",
                             hours) % {"hours": hours}
    
                if days == 0:
                    format = _("%(time)s")
                elif days == 1 and local_date.day == local_yesterday.day and \
                     relative:
                    format = _("yesterday") if shorter else \
                             _("yesterday at %(time)s")
                elif days < 5:
                    format = _("%(weekday)s") if shorter else \
                             _("%(weekday)s at %(time)s")
                elif days < 334:  # 11mo, since confusing for same month last year
                    format = _("%(month_name)s %(day)s") if shorter else \
                             _("%(month_name)s %(day)s at %(time)s")
    
            if format is None:
                format = _("%(month_name)s %(day)s, %(year)s") if shorter else \
                         _("%(month_name)s %(day)s, %(year)s at %(time)s")
    
            tfhour_clock = self.code not in ("en", "en_US", "zh_CN")
            if tfhour_clock:
                str_time = "%d:%02d" % (local_date.hour, local_date.minute)
            elif self.code == "zh_CN":
                str_time = "%s%d:%02d" % (
                    (u'\u4e0a\u5348', u'\u4e0b\u5348')[local_date.hour >= 12],
                    local_date.hour % 12 or 12, local_date.minute)
            else:
                str_time = "%d:%02d %s" % (
                    local_date.hour % 12 or 12, local_date.minute,
                    ("am", "pm")[local_date.hour >= 12])
    
            return format % {
                "month_name": self._months[local_date.month - 1],
                "weekday": self._weekdays[local_date.weekday()],
                "day": str(local_date.day),
                "year": str(local_date.year),
                "time": str_time
            }
    
        def format_day(self, date, gmt_offset=0, dow=True):
            """Formats the given date as a day of week.
    
            Example: "Monday, January 22". You can remove the day of week with
            dow=False.
            """
            local_date = date - datetime.timedelta(minutes=gmt_offset)
            _ = self.translate
            if dow:
                return _("%(weekday)s, %(month_name)s %(day)s") % {
                    "month_name": self._months[local_date.month - 1],
                    "weekday": self._weekdays[local_date.weekday()],
                    "day": str(local_date.day),
                }
            else:
                return _("%(month_name)s %(day)s") % {
                    "month_name": self._months[local_date.month - 1],
                    "day": str(local_date.day),
                }
    
        def list(self, parts):
            """Returns a comma-separated list for the given list of parts.
    
            The format is, e.g., "A, B and C", "A and B" or just "A" for lists
            of size 1.
            """
            _ = self.translate
            if len(parts) == 0:
                return ""
            if len(parts) == 1:
                return parts[0]
            comma = u' \u0648 ' if self.code.startswith("fa") else u", "
            return _("%(commas)s and %(last)s") % {
                "commas": comma.join(parts[:-1]),
                "last": parts[len(parts) - 1],
            }
    
        def friendly_number(self, value):
            """Returns a comma-separated number for the given integer."""
            if self.code not in ("en", "en_US"):
                return str(value)
            value = str(value)
            parts = []
            while value:
                parts.append(value[-3:])
                value = value[:-3]
            return ",".join(reversed(parts))
    
    
    class CSVLocale(Locale):
        """Locale implementation using tornado's CSV translation format."""
        def translate(self, message, plural_message=None, count=None):
            if plural_message is not None:
                assert count is not None
                if count != 1:
                    message = plural_message
                    message_dict = self.translations.get("plural", {})
                else:
                    message_dict = self.translations.get("singular", {})
            else:
                message_dict = self.translations.get("unknown", {})
            return message_dict.get(message, message)
    
    
    class GettextLocale(Locale):
        """Locale implementation using the gettext module."""
        def translate(self, message, plural_message=None, count=None):
            if plural_message is not None:
                assert count is not None
                return self.translations.ungettext(message, plural_message, count)
            else:
                return self.translations.ugettext(message)
    
    LOCALE_NAMES = {
        "af_ZA": {"name_en": u"Afrikaans", "name": u"Afrikaans"},
        "am_ET": {"name_en": u"Amharic", "name": u'\u12a0\u121b\u122d\u129b'},
        "ar_AR": {"name_en": u"Arabic",
        "name": u"\u0627\u0644\u0639\u0631\u0628\u064a\u0629"},
        "bg_BG": {"name_en": u"Bulgarian",
        "name": u"\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438"},
        "bn_IN": {"name_en": u"Bengali",
        "name": u"\u09ac\u09be\u0982\u09b2\u09be"},
        "bs_BA": {"name_en": u"Bosnian", "name": u"Bosanski"},
        "ca_ES": {"name_en": u"Catalan", "name": u"Catal\xe0"},
        "cs_CZ": {"name_en": u"Czech", "name": u"\u010ce\u0161tina"},
        "cy_GB": {"name_en": u"Welsh", "name": u"Cymraeg"},
        "da_DK": {"name_en": u"Danish", "name": u"Dansk"},
        "de_DE": {"name_en": u"German", "name": u"Deutsch"},
        "el_GR": {"name_en": u"Greek",
        "name": u"\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac"},
        "en_GB": {"name_en": u"English (UK)", "name": u"English (UK)"},
        "en_US": {"name_en": u"English (US)", "name": u"English (US)"},
        "es_ES": {"name_en": u"Spanish (Spain)",
        "name": u"Espa\xf1ol (Espa\xf1a)"},
        "es_LA": {"name_en": u"Spanish", "name": u"Espa\xf1ol"},
        "et_EE": {"name_en": u"Estonian", "name": u"Eesti"},
        "eu_ES": {"name_en": u"Basque", "name": u"Euskara"},
        "fa_IR": {"name_en": u"Persian",
        "name": u"\u0641\u0627\u0631\u0633\u06cc"},
        "fi_FI": {"name_en": u"Finnish", "name": u"Suomi"},
        "fr_CA": {"name_en": u"French (Canada)", "name": u"Fran\xe7ais (Canada)"},
        "fr_FR": {"name_en": u"French", "name": u"Fran\xe7ais"},
        "ga_IE": {"name_en": u"Irish", "name": u"Gaeilge"},
        "gl_ES": {"name_en": u"Galician", "name": u"Galego"},
        "he_IL": {"name_en": u"Hebrew",
        "name": u"\u05e2\u05d1\u05e8\u05d9\u05ea"},
        "hi_IN": {"name_en": u"Hindi",
        "name": u"\u0939\u093f\u0928\u094d\u0926\u0940"},
        "hr_HR": {"name_en": u"Croatian", "name": u"Hrvatski"},
        "hu_HU": {"name_en": u"Hungarian", "name": u"Magyar"},
        "id_ID": {"name_en": u"Indonesian", "name": u"Bahasa Indonesia"},
        "is_IS": {"name_en": u"Icelandic", "name": u"\xcdslenska"},
        "it_IT": {"name_en": u"Italian", "name": u"Italiano"},
        "ja_JP": {"name_en": u"Japanese", "name": u"\u65e5\u672c\u8a9e"},
        "ko_KR": {"name_en": u"Korean", "name": u"\ud55c\uad6d\uc5b4"},
        "lt_LT": {"name_en": u"Lithuanian", "name": u"Lietuvi\u0173"},
        "lv_LV": {"name_en": u"Latvian", "name": u"Latvie\u0161u"},
        "mk_MK": {"name_en": u"Macedonian",
        "name": u"\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438"},
        "ml_IN": {"name_en": u"Malayalam",
        "name": u"\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02"},
        "ms_MY": {"name_en": u"Malay", "name": u"Bahasa Melayu"},
        "nb_NO": {"name_en": u"Norwegian (bokmal)", "name": u"Norsk (bokm\xe5l)"},
        "nl_NL": {"name_en": u"Dutch", "name": u"Nederlands"},
        "nn_NO": {"name_en": u"Norwegian (nynorsk)", "name": u"Norsk (nynorsk)"},
        "pa_IN": {"name_en": u"Punjabi",
        "name": u"\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40"},
        "pl_PL": {"name_en": u"Polish", "name": u"Polski"},
        "pt_BR": {"name_en": u"Portuguese (Brazil)",
        "name": u"Portugu\xeas (Brasil)"},
        "pt_PT": {"name_en": u"Portuguese (Portugal)",
        "name": u"Portugu\xeas (Portugal)"},
        "ro_RO": {"name_en": u"Romanian", "name": u"Rom\xe2n\u0103"},
        "ru_RU": {"name_en": u"Russian",
        "name": u"\u0420\u0443\u0441\u0441\u043a\u0438\u0439"},
        "sk_SK": {"name_en": u"Slovak", "name": u"Sloven\u010dina"},
        "sl_SI": {"name_en": u"Slovenian", "name": u"Sloven\u0161\u010dina"},
        "sq_AL": {"name_en": u"Albanian", "name": u"Shqip"},
        "sr_RS": {"name_en": u"Serbian",
        "name": u"\u0421\u0440\u043f\u0441\u043a\u0438"},
        "sv_SE": {"name_en": u"Swedish", "name": u"Svenska"},
        "sw_KE": {"name_en": u"Swahili", "name": u"Kiswahili"},
        "ta_IN": {"name_en": u"Tamil",
        "name": u"\u0ba4\u0bae\u0bbf\u0bb4\u0bcd"},
        "te_IN": {"name_en": u"Telugu",
        "name": u"\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41"},
        "th_TH": {"name_en": u"Thai",
        "name": u"\u0e20\u0e32\u0e29\u0e32\u0e44\u0e17\u0e22"},
        "tl_PH": {"name_en": u"Filipino", "name": u"Filipino"},
        "tr_TR": {"name_en": u"Turkish", "name": u"T\xfcrk\xe7e"},
        "uk_UA": {"name_en": u"Ukraini ",
        "name": u"\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430"},
        "vi_VN": {"name_en": u"Vietnamese", "name": u"Ti\u1ebfng Vi\u1ec7t"},
        "zh_CN": {"name_en": u"Chinese (Simplified)",
        "name": u"\u4e2d\u6587(\u7b80\u4f53)"},
        "zh_TW": {"name_en": u"Chinese (Traditional)",
        "name": u"\u4e2d\u6587(\u7e41\u9ad4)"},
    }
    python-cyclone-1.1/cyclone/auth.py0000644000175000017500000013731512124336260016332 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """Implementations of various third-party authentication schemes.
    
    All the classes in this file are class Mixins designed to be used with
    web.py RequestHandler classes. The primary methods for each service are
    authenticate_redirect(), authorize_redirect(), and get_authenticated_user().
    The former should be called to redirect the user to, e.g., the OpenID
    authentication page on the third party service, and the latter should
    be called upon return to get the user data from the data returned by
    the third party service.
    
    They all take slightly different arguments due to the fact all these
    services implement authentication and authorization slightly differently.
    See the individual service classes below for complete documentation.
    
    Example usage for Google OpenID::
    
        class GoogleHandler(cyclone.web.RequestHandler, cyclone.auth.GoogleMixin):
            @cyclone.web.asynchronous
            def get(self):
                if self.get_argument("openid.mode", None):
                    self.get_authenticated_user(self.async_callback(self._on_auth))
                    return
                self.authenticate_redirect()
    
            def _on_auth(self, user):
                if not user:
                    raise cyclone.web.HTTPError(500, "Google auth failed")
                # Save the user with, e.g., set_secure_cookie()
    """
    
    from cyclone import escape
    from cyclone import httpclient
    from cyclone.util import bytes_type
    from cyclone.util import unicode_type
    from cyclone.httputil import url_concat
    from twisted.python import log
    
    import base64
    import binascii
    import hashlib
    import hmac
    import time
    import urllib
    import urlparse
    import uuid
    
    
    class OpenIdMixin(object):
        """Abstract implementation of OpenID and Attribute Exchange.
    
        See GoogleMixin below for example implementations.
        """
        def authenticate_redirect(self, callback_uri=None,
                                  ax_attrs=["name", "email",
                                            "language", "username"]):
            """Returns the authentication URL for this service.
    
            After authentication, the service will redirect back to the given
            callback URI.
    
            We request the given attributes for the authenticated user by
            default (name, email, language, and username). If you don't need
            all those attributes for your app, you can request fewer with
            the ax_attrs keyword argument.
            """
            callback_uri = callback_uri or self.request.uri
            args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
            self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
    
        def get_authenticated_user(self, callback):
            """Fetches the authenticated user data upon redirect.
    
            This method should be called by the handler that receives the
            redirect from the authenticate_redirect() or authorize_redirect()
            methods.
            """
            # Verify the OpenID response via direct request to the OP
            args = dict((k, v[-1]) for k, v in self.request.arguments.items())
            args["openid.mode"] = u"check_authentication"
            url = self._OPENID_ENDPOINT
            callback = self.async_callback(self._on_authentication_verified,
                                           callback)
            httpclient.fetch(url, method="POST",
                    postdata=urllib.urlencode(args)).addBoth(callback)
    
        def _openid_args(self, callback_uri, ax_attrs=[], oauth_scope=None):
            url = urlparse.urljoin(self.request.full_url(), callback_uri)
            args = {
                "openid.ns": "http://specs.openid.net/auth/2.0",
                "openid.claimed_id":
                    "http://specs.openid.net/auth/2.0/identifier_select",
                "openid.identity":
                    "http://specs.openid.net/auth/2.0/identifier_select",
                "openid.return_to": url,
                "openid.realm": urlparse.urljoin(url, '/'),
                "openid.mode": "checkid_setup",
            }
            if ax_attrs:
                args.update({
                    "openid.ns.ax": "http://openid.net/srv/ax/1.0",
                    "openid.ax.mode": "fetch_request",
                })
                ax_attrs = set(ax_attrs)
                required = []
                if "name" in ax_attrs:
                    ax_attrs -= set(["name", "firstname", "fullname", "lastname"])
                    required += ["firstname", "fullname", "lastname"]
                    args.update({
                        "openid.ax.type.firstname":
                            "http://axschema.org/namePerson/first",
                        "openid.ax.type.fullname":
                            "http://axschema.org/namePerson",
                        "openid.ax.type.lastname":
                            "http://axschema.org/namePerson/last",
                    })
                known_attrs = {
                    "email": "http://axschema.org/contact/email",
                    "language": "http://axschema.org/pref/language",
                    "username": "http://axschema.org/namePerson/friendly",
                }
                for name in ax_attrs:
                    args["openid.ax.type." + name] = known_attrs[name]
                    required.append(name)
                args["openid.ax.required"] = ",".join(required)
            if oauth_scope:
                args.update({
                    "openid.ns.oauth":
                        "http://specs.openid.net/extensions/oauth/1.0",
                    "openid.oauth.consumer": self.request.host.split(":")[0],
                    "openid.oauth.scope": oauth_scope,
                })
            return args
    
        def _on_authentication_verified(self, callback, response):
            if response.error or "is_valid:true" not in response.body:
                log.msg("Invalid OpenID response: %s" %
                        (response.error or response.body))
                callback(None)
                return
    
            # Make sure we got back at least an email from attribute exchange
            ax_ns = None
            for name in self.request.arguments.keys():
                if name.startswith("openid.ns.") and \
                   self.get_argument(name) == u"http://openid.net/srv/ax/1.0":
                    ax_ns = name[10:]
                    break
    
            def get_ax_arg(uri):
                if not ax_ns:
                    return u""
                prefix = "openid." + ax_ns + ".type."
                ax_name = None
                for name in self.request.arguments.keys():
                    if self.get_argument(name) == uri and name.startswith(prefix):
                        part = name[len(prefix):]
                        ax_name = "openid." + ax_ns + ".value." + part
                        break
                if not ax_name:
                    return u""
                return self.get_argument(ax_name, u"")
    
            email = get_ax_arg("http://axschema.org/contact/email")
            name = get_ax_arg("http://axschema.org/namePerson")
            first_name = get_ax_arg("http://axschema.org/namePerson/first")
            last_name = get_ax_arg("http://axschema.org/namePerson/last")
            username = get_ax_arg("http://axschema.org/namePerson/friendly")
            locale = get_ax_arg("http://axschema.org/pref/language").lower()
            user = dict()
            name_parts = []
            if first_name:
                user["first_name"] = first_name
                name_parts.append(first_name)
            if last_name:
                user["last_name"] = last_name
                name_parts.append(last_name)
            if name:
                user["name"] = name
            elif name_parts:
                user["name"] = u" ".join(name_parts)
            elif email:
                user["name"] = email.split("@")[0]
            if email:
                user["email"] = email
            if locale:
                user["locale"] = locale
            if username:
                user["username"] = username
            claimed_id = self.get_argument("openid.claimed_id", None)
            if claimed_id:
                user["claimed_id"] = claimed_id
            callback(user)
    
    
    class OAuthMixin(object):
        """Abstract implementation of OAuth.
    
        See TwitterMixin and FriendFeedMixin below for example implementations.
        """
        def authorize_redirect(self, callback_uri=None):
            """Redirects the user to obtain OAuth authorization for this service.
    
            Twitter and FriendFeed both require that you register a Callback
            URL with your application. You should call this method to log the
            user in, and then call get_authenticated_user() in the handler
            you registered as your Callback URL to complete the authorization
            process.
    
            This method sets a cookie called _oauth_request_token which is
            subsequently used (and cleared) in get_authenticated_user for
            security purposes.
            """
            if callback_uri and getattr(self, "_OAUTH_NO_CALLBACKS", False):
                raise Exception("This service does not support oauth_callback")
            callback = self.async_callback(
                self._on_request_token, self._OAUTH_AUTHORIZE_URL, callback_uri)
            httpclient.fetch(self._oauth_request_token_url()).addCallback(callback)
    
        def get_authenticated_user(self, callback):
            """Gets the OAuth authorized user and access token on callback.
    
            This method should be called from the handler for your registered
            OAuth Callback URL to complete the registration process. We call
            callback with the authenticated user, which in addition to standard
            attributes like 'name' includes the 'access_key' attribute, which
            contains the OAuth access you can use to make authorized requests
            to this service on behalf of the user.
            """
            request_key = escape.utf8(self.get_argument("oauth_token"))
            oauth_verifier = self.get_argument("oauth_verifier", None)
            request_cookie = self.get_cookie("_oauth_request_token")
            if not request_cookie:
                log.msg("Missing OAuth request token cookie")
                callback(None)
                return
            self.clear_cookie("_oauth_request_token")
            cookie_key, cookie_secret = [base64.b64decode(escape.utf8(i))
                                         for i in request_cookie.split("|")]
            if cookie_key != request_key:
                log.msg("Request token does not match cookie")
                callback(None)
                return
            token = dict(key=cookie_key, secret=cookie_secret)
            if oauth_verifier:
                token["verifier"] = oauth_verifier
            d = httpclient.fetch(self._oauth_access_token_url(token))
            d.addCallback(self.async_callback(self._on_access_token, callback))
    
        def _oauth_request_token_url(self, callback_uri=None, extra_params=None):
            consumer_token = self._oauth_consumer_token()
            url = self._OAUTH_REQUEST_TOKEN_URL
            args = dict(
                oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
                oauth_signature_method="HMAC-SHA1",
                oauth_timestamp=str(int(time.time())),
                oauth_nonce=escape.to_basestring(binascii.b2a_hex(
                                                 uuid.uuid4().bytes)),
                oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
            )
            if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
                if callback_uri == "oob":
                    args["oauth_callback"] = "oob"
                elif callback_uri:
                    args["oauth_callback"] = urlparse.urljoin(
                        self.request.full_url(), callback_uri)
                if extra_params:
                    args.update(extra_params)
                signature = _oauth10a_signature(consumer_token, "GET", url, args)
            else:
                signature = _oauth_signature(consumer_token, "GET", url, args)
    
            args["oauth_signature"] = signature
            return url + "?" + urllib.urlencode(args)
    
        def _on_request_token(self, authorize_url, callback_uri, response):
            if response.error:
                raise Exception("Could not get request token")
            request_token = _oauth_parse_response(response.body)
            data = (base64.b64encode(request_token["key"]) + "|" +
                    base64.b64encode(request_token["secret"]))
            self.set_cookie("_oauth_request_token", data)
            args = dict(oauth_token=request_token["key"])
            if callback_uri == "oob":
                self.finish(authorize_url + "?" + urllib.urlencode(args))
                return
            elif callback_uri:
                args["oauth_callback"] = urlparse.urljoin(
                    self.request.full_url(), callback_uri)
            self.redirect(authorize_url + "?" + urllib.urlencode(args))
    
        def _oauth_access_token_url(self, request_token):
            consumer_token = self._oauth_consumer_token()
            url = self._OAUTH_ACCESS_TOKEN_URL
            args = dict(
                oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
                oauth_token=escape.to_basestring(request_token["key"]),
                oauth_signature_method="HMAC-SHA1",
                oauth_timestamp=str(int(time.time())),
                oauth_nonce=escape.to_basestring(binascii.b2a_hex(
                                                 uuid.uuid4().bytes)),
                oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
            )
            if "verifier" in request_token:
                args["oauth_verifier"] = request_token["verifier"]
    
            if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
                signature = _oauth10a_signature(consumer_token, "GET", url, args,
                                                request_token)
            else:
                signature = _oauth_signature(consumer_token, "GET", url, args,
                                             request_token)
    
            args["oauth_signature"] = signature
            return url + "?" + urllib.urlencode(args)
    
        def _on_access_token(self, callback, response):
            if response.error:
                log.msg("Could not fetch access token")
                callback(None)
                return
    
            access_token = _oauth_parse_response(response.body)
            self._oauth_get_user(access_token, self.async_callback(
                 self._on_oauth_get_user, access_token, callback))
    
        def _oauth_get_user(self, access_token, callback):
            raise NotImplementedError()
    
        def _on_oauth_get_user(self, access_token, callback, user):
            if not user:
                callback(None)
                return
            user["access_token"] = access_token
            callback(user)
    
        def _oauth_request_parameters(self, url, access_token, parameters={},
                                      method="GET"):
            """Returns the OAuth parameters as a dict for the given request.
    
            parameters should include all POST arguments and query string arguments
            that will be sent with the request.
            """
            consumer_token = self._oauth_consumer_token()
            base_args = dict(
                oauth_consumer_key=escape.to_basestring(consumer_token["key"]),
                oauth_token=escape.to_basestring(access_token["key"]),
                oauth_signature_method="HMAC-SHA1",
                oauth_timestamp=str(int(time.time())),
                oauth_nonce=escape.to_basestring(binascii.b2a_hex(
                                                 uuid.uuid4().bytes)),
                oauth_version=getattr(self, "_OAUTH_VERSION", "1.0a"),
            )
            args = {}
            args.update(base_args)
            args.update(parameters)
            if getattr(self, "_OAUTH_VERSION", "1.0a") == "1.0a":
                signature = _oauth10a_signature(consumer_token, method, url, args,
                                             access_token)
            else:
                signature = _oauth_signature(consumer_token, method, url, args,
                                             access_token)
            base_args["oauth_signature"] = signature
            return base_args
    
    
    class OAuth2Mixin(object):
        """Abstract implementation of OAuth v 2."""
    
        def authorize_redirect(self, redirect_uri=None, client_id=None,
                               client_secret=None, extra_params=None):
            """Redirects the user to obtain OAuth authorization for this service.
    
            Some providers require that you register a Callback
            URL with your application. You should call this method to log the
            user in, and then call get_authenticated_user() in the handler
            you registered as your Callback URL to complete the authorization
            process.
            """
            args = {
              "redirect_uri": redirect_uri,
              "client_id": client_id
            }
            if extra_params:
                args.update(extra_params)
            self.redirect(
                    url_concat(self._OAUTH_AUTHORIZE_URL, args))
    
        def _oauth_request_token_url(self, redirect_uri=None, client_id=None,
                                     client_secret=None, code=None,
                                     extra_params=None):
            url = self._OAUTH_ACCESS_TOKEN_URL
            args = dict(
                redirect_uri=redirect_uri,
                code=code,
                client_id=client_id,
                client_secret=client_secret,
                )
            if extra_params:
                args.update(extra_params)
            return url_concat(url, args)
    
    
    class TwitterMixin(OAuthMixin):
        """Twitter OAuth authentication.
    
        To authenticate with Twitter, register your application with
        Twitter at http://twitter.com/apps. Then copy your Consumer Key and
        Consumer Secret to the application settings 'twitter_consumer_key' and
        'twitter_consumer_secret'. Use this Mixin on the handler for the URL
        you registered as your application's Callback URL.
    
        When your application is set up, you can use this Mixin like this
        to authenticate the user with Twitter and get access to their stream::
    
            class TwitterHandler(cyclone.web.RequestHandler,
                                 cyclone.auth.TwitterMixin):
                @cyclone.web.asynchronous
                def get(self):
                    if self.get_argument("oauth_token", None):
                        self.get_authenticated_user(
                                            self.async_callback(self._on_auth))
                        return
                    self.authorize_redirect()
    
                def _on_auth(self, user):
                    if not user:
                        raise cyclone.web.HTTPError(500, "Twitter auth failed")
                    # Save the user using, e.g., set_secure_cookie()
    
        The user object returned by get_authenticated_user() includes the
        attributes 'username', 'name', and all of the custom Twitter user
        attributes describe at
        http://apiwiki.twitter.com/Twitter-REST-API-Method%3A-users%C2%A0show
        in addition to 'access_token'. You should save the access token with
        the user; it is required to make requests on behalf of the user later
        with twitter_request().
        """
        _OAUTH_REQUEST_TOKEN_URL = "http://api.twitter.com/oauth/request_token"
        _OAUTH_ACCESS_TOKEN_URL = "http://api.twitter.com/oauth/access_token"
        _OAUTH_AUTHORIZE_URL = "http://api.twitter.com/oauth/authorize"
        _OAUTH_AUTHENTICATE_URL = "http://api.twitter.com/oauth/authenticate"
        _OAUTH_NO_CALLBACKS = False
        _TWITTER_BASE_URL = "http://api.twitter.com/1"
    
        def authenticate_redirect(self):
            """Just like authorize_redirect(), but auto-redirects if authorized.
    
            This is generally the right interface to use if you are using
            Twitter for single-sign on.
            """
            httpclient.fetch(self._oauth_request_token_url()).addCallback(
                self.async_callback(
                self._on_request_token, self._OAUTH_AUTHENTICATE_URL, None))
    
        def twitter_request(self, path, callback, access_token=None,
                               post_args=None, **args):
            """Fetches the given API path, e.g., "/statuses/user_timeline/btaylor"
    
            The path should not include the format (we automatically append
            ".json" and parse the JSON output).
    
            If the request is a POST, post_args should be provided. Query
            string arguments should be given as keyword arguments.
    
            All the Twitter methods are documented at
            http://apiwiki.twitter.com/Twitter-API-Documentation.
    
            Many methods require an OAuth access token which you can obtain
            through authorize_redirect() and get_authenticated_user(). The
            user returned through that process includes an 'access_token'
            attribute that can be used to make authenticated requests via
            this method. Example usage::
    
                class MainHandler(cyclone.web.RequestHandler,
                                  cyclone.auth.TwitterMixin):
                    @cyclone.web.authenticated
                    @cyclone.web.asynchronous
                    def get(self):
                        self.twitter_request(
                            "/statuses/update",
                            post_args={"status": "Testing cyclone Web Server"},
                            access_token=user["access_token"],
                            callback=self.async_callback(self._on_post))
    
                    def _on_post(self, new_entry):
                        if not new_entry:
                            # Call failed; perhaps missing permission?
                            self.authorize_redirect()
                            return
                        self.finish("Posted a message!")
    
            """
            if path.startswith('http:') or path.startswith('https:'):
                # Raw urls are useful for e.g. search which doesn't follow the
                # usual pattern: http://search.twitter.com/search.json
                url = path
            else:
                url = self._TWITTER_BASE_URL + path + ".json"
            # Add the OAuth resource request signature if we have credentials
            url = "http://twitter.com" + path + ".json"
            if access_token:
                all_args = {}
                all_args.update(args)
                all_args.update(post_args or {})
                self._oauth_consumer_token()
                method = "POST" if post_args is not None else "GET"
                oauth = self._oauth_request_parameters(
                    url, access_token, all_args, method=method)
                args.update(oauth)
            if args:
                url += "?" + urllib.urlencode(args)
            if post_args is not None:
                d = httpclient.fetch(url, method="POST",
                                     postdata=urllib.urlencode(post_args))
                d.addCallback(self.async_callback(self._on_twitter_request,
                              callback))
            else:
                d = httpclient.fetch(url)
                d.addCallback(self.async_callback(self._on_twitter_request,
                              callback))
    
        def _on_twitter_request(self, callback, response):
            if response.error:
                log.msg("Error response %s fetching %s" % (response.error,
                                response.request.url))
                callback(None)
                return
            callback(escape.json_decode(response.body))
    
        def _oauth_consumer_token(self):
            self.require_setting("twitter_consumer_key", "Twitter OAuth")
            self.require_setting("twitter_consumer_secret", "Twitter OAuth")
            return dict(
                key=self.settings["twitter_consumer_key"],
                secret=self.settings["twitter_consumer_secret"])
    
        def _oauth_get_user(self, access_token, callback):
            callback = self.async_callback(self._parse_user_response, callback)
            self.twitter_request(
                "/users/show/" + escape.native_str(access_token["screen_name"]),
                access_token=access_token, callback=callback)
    
        def _parse_user_response(self, callback, user):
            if user:
                user["username"] = user["screen_name"]
            callback(user)
    
    
    class FriendFeedMixin(OAuthMixin):
        """FriendFeed OAuth authentication.
    
        To authenticate with FriendFeed, register your application with
        FriendFeed at http://friendfeed.com/api/applications. Then
        copy your Consumer Key and Consumer Secret to the application settings
        'friendfeed_consumer_key' and 'friendfeed_consumer_secret'. Use
        this Mixin on the handler for the URL you registered as your
        application's Callback URL.
    
        When your application is set up, you can use this Mixin like this
        to authenticate the user with FriendFeed and get access to their feed::
    
            class FriendFeedHandler(cyclone.web.RequestHandler,
                                    cyclone.auth.FriendFeedMixin):
                @cyclone.web.asynchronous
                def get(self):
                    if self.get_argument("oauth_token", None):
                        self.get_authenticated_user(
                                            self.async_callback(self._on_auth))
                        return
                    self.authorize_redirect()
    
                def _on_auth(self, user):
                    if not user:
                        raise cyclone.web.HTTPError(500, "FriendFeed auth failed")
                    # Save the user using, e.g., set_secure_cookie()
    
        The user object returned by get_authenticated_user() includes the
        attributes 'username', 'name', and 'description' in addition to
        'access_token'. You should save the access token with the user;
        it is required to make requests on behalf of the user later with
        friendfeed_request().
        """
        _OAUTH_VERSION = "1.0"
        _OAUTH_REQUEST_TOKEN_URL = \
            "https://friendfeed.com/account/oauth/request_token"
        _OAUTH_ACCESS_TOKEN_URL = \
            "https://friendfeed.com/account/oauth/access_token"
        _OAUTH_AUTHORIZE_URL = "https://friendfeed.com/account/oauth/authorize"
        _OAUTH_NO_CALLBACKS = True
    
        def friendfeed_request(self, path, callback, access_token=None,
                               post_args=None, **args):
            """Fetches the given relative API path, e.g., "/bret/friends"
    
            If the request is a POST, post_args should be provided. Query
            string arguments should be given as keyword arguments.
    
            All the FriendFeed methods are documented at
            http://friendfeed.com/api/documentation.
    
            Many methods require an OAuth access token which you can obtain
            through authorize_redirect() and get_authenticated_user(). The
            user returned through that process includes an 'access_token'
            attribute that can be used to make authenticated requests via
            this method. Example usage::
    
                class MainHandler(cyclone.web.RequestHandler,
                                  cyclone.auth.FriendFeedMixin):
                    @cyclone.web.authenticated
                    @cyclone.web.asynchronous
                    def get(self):
                        self.friendfeed_request(
                            "/entry",
                            post_args={"body": "Testing cyclone Web Server"},
                            access_token=self.current_user["access_token"],
                            callback=self.async_callback(self._on_post))
    
                    def _on_post(self, new_entry):
                        if not new_entry:
                            # Call failed; perhaps missing permission?
                            self.authorize_redirect()
                            return
                        self.finish("Posted a message!")
            """
            # Add the OAuth resource request signature if we have credentials
            url = "http://friendfeed-api.com/v2" + path
            if access_token:
                all_args = {}
                all_args.update(args)
                all_args.update(post_args or {})
                method = "POST" if post_args is not None else "GET"
                oauth = self._oauth_request_parameters(
                    url, access_token, all_args, method=method)
                args.update(oauth)
            if args:
                url += "?" + urllib.urlencode(args)
            if post_args is not None:
                d = httpclient.fetch(url, method="POST",
                                     postdata=urllib.urlencode(post_args))
                d.addCallback(self.async_callback(self._on_friendfeed_request,
                              callback))
            else:
                httpclient.fetch(url).addCallback(self.async_callback(
                    self._on_friendfeed_request, callback))
    
        def _on_friendfeed_request(self, callback, response):
            if response.error:
                log.msg("Error response %s fetching %s" % (response.error,
                                response.request.url))
                callback(None)
                return
            callback(escape.json_decode(response.body))
    
        def _oauth_consumer_token(self):
            self.require_setting("friendfeed_consumer_key", "FriendFeed OAuth")
            self.require_setting("friendfeed_consumer_secret", "FriendFeed OAuth")
            return dict(
                key=self.settings["friendfeed_consumer_key"],
                secret=self.settings["friendfeed_consumer_secret"])
    
        def _oauth_get_user(self, access_token, callback):
            callback = self.async_callback(self._parse_user_response, callback)
            self.friendfeed_request(
                "/feedinfo/" + access_token["username"],
                include="id,name,description", access_token=access_token,
                callback=callback)
    
        def _parse_user_response(self, callback, user):
            if user:
                user["username"] = user["id"]
            callback(user)
    
    
    class GoogleMixin(OpenIdMixin, OAuthMixin):
        """Google Open ID / OAuth authentication.
    
        No application registration is necessary to use Google for authentication
        or to access Google resources on behalf of a user. To authenticate with
        Google, redirect with authenticate_redirect(). On return, parse the
        response with get_authenticated_user(). We send a dict containing the
        values for the user, including 'email', 'name', and 'locale'.
        Example usage::
    
            class GoogleHandler(cyclone.web.RequestHandler,
                                cyclone.auth.GoogleMixin):
               @cyclone.web.asynchronous
               def get(self):
                   if self.get_argument("openid.mode", None):
                       self.get_authenticated_user(
                                            self.async_callback(self._on_auth))
                       return
                self.authenticate_redirect()
    
                def _on_auth(self, user):
                    if not user:
                        raise cyclone.web.HTTPError(500, "Google auth failed")
                    # Save the user with, e.g., set_secure_cookie()
        """
        _OPENID_ENDPOINT = "https://www.google.com/accounts/o8/ud"
        _OAUTH_ACCESS_TOKEN_URL = \
            "https://www.google.com/accounts/OAuthGetAccessToken"
    
        def authorize_redirect(self, oauth_scope, callback_uri=None,
                               ax_attrs=["name", "email", "language", "username"]):
            """Authenticates and authorizes for the given Google resource.
    
            Some of the available resources are:
    
               Gmail Contacts - http://www.google.com/m8/feeds/
               Calendar - http://www.google.com/calendar/feeds/
               Finance - http://finance.google.com/finance/feeds/
    
            You can authorize multiple resources by separating the resource
            URLs with a space.
            """
            callback_uri = callback_uri or self.request.uri
            args = self._openid_args(callback_uri, ax_attrs=ax_attrs,
                                     oauth_scope=oauth_scope)
            self.redirect(self._OPENID_ENDPOINT + "?" + urllib.urlencode(args))
    
        def get_authenticated_user(self, callback):
            """Fetches the authenticated user data upon redirect."""
            # Look to see if we are doing combined OpenID/OAuth
            oauth_ns = ""
            for name, values in self.request.arguments.iteritems():
                if name.startswith("openid.ns.") and \
                   values[-1] == u"http://specs.openid.net/extensions/oauth/1.0":
                    oauth_ns = name[10:]
                    break
            token = self.get_argument("openid." + oauth_ns + ".request_token", "")
            if token:
                token = dict(key=token, secret="")
                d = httpclient.fetch(self._oauth_access_token_url(token))
                d.addCallback(self.async_callback(self._on_access_token, callback))
            else:
                OpenIdMixin.get_authenticated_user(self, callback)
    
        def _oauth_consumer_token(self):
            self.require_setting("google_consumer_key", "Google OAuth")
            self.require_setting("google_consumer_secret", "Google OAuth")
            return dict(
                key=self.settings["google_consumer_key"],
                secret=self.settings["google_consumer_secret"])
    
        def _oauth_get_user(self, access_token, callback):
            OpenIdMixin.get_authenticated_user(self, callback)
    
    
    class FacebookMixin(object):
        """Facebook Connect authentication.
    
        New applications should consider using `FacebookGraphMixin` below instead
        of this class.
    
        To authenticate with Facebook, register your application with
        Facebook at http://www.facebook.com/developers/apps.php. Then
        copy your API Key and Application Secret to the application settings
        'facebook_api_key' and 'facebook_secret'.
    
        When your application is set up, you can use this Mixin like this
        to authenticate the user with Facebook::
    
            class FacebookHandler(cyclone.web.RequestHandler,
                                  cyclone.auth.FacebookMixin):
                @cyclone.web.asynchronous
                def get(self):
                    if self.get_argument("session", None):
                        self.get_authenticated_user(
                                            self.async_callback(self._on_auth))
                        return
                    self.authenticate_redirect()
    
                def _on_auth(self, user):
                    if not user:
                        raise cyclone.web.HTTPError(500, "Facebook auth failed")
                    # Save the user using, e.g., set_secure_cookie()
    
        The user object returned by get_authenticated_user() includes the
        attributes 'facebook_uid' and 'name' in addition to session attributes
        like 'session_key'. You should save the session key with the user; it is
        required to make requests on behalf of the user later with
        facebook_request().
        """
        def authenticate_redirect(self, callback_uri=None, cancel_uri=None,
                                  extended_permissions=None):
            """Authenticates/installs this app for the current user."""
            self.require_setting("facebook_api_key", "Facebook Connect")
            callback_uri = callback_uri or self.request.uri
            args = {
                "api_key": self.settings["facebook_api_key"],
                "v": "1.0",
                "fbconnect": "true",
                "display": "page",
                "next": urlparse.urljoin(self.request.full_url(), callback_uri),
                "return_session": "true",
            }
            if cancel_uri:
                args["cancel_url"] = urlparse.urljoin(
                    self.request.full_url(), cancel_uri)
            if extended_permissions:
                if isinstance(extended_permissions, (unicode_type, bytes_type)):
                    extended_permissions = [extended_permissions]
                args["req_perms"] = ",".join(extended_permissions)
            self.redirect("http://www.facebook.com/login.php?" +
                          urllib.urlencode(args))
    
        def authorize_redirect(self, extended_permissions, callback_uri=None,
                               cancel_uri=None):
            """Redirects to an authorization request for the given FB resource.
    
            The available resource names are listed at
            http://wiki.developers.facebook.com/index.php/Extended_permission.
            The most common resource types include:
    
            * publish_stream
            * read_stream
            * email
            * sms
    
            extended_permissions can be a single permission name or a list of
            names. To get the session secret and session key, call
            get_authenticated_user() just as you would with
            authenticate_redirect().
            """
            self.authenticate_redirect(callback_uri, cancel_uri,
                                       extended_permissions)
    
        def get_authenticated_user(self, callback):
            """Fetches the authenticated Facebook user.
    
            The authenticated user includes the special Facebook attributes
            'session_key' and 'facebook_uid' in addition to the standard
            user attributes like 'name'.
            """
            self.require_setting("facebook_api_key", "Facebook Connect")
            session = escape.json_decode(self.get_argument("session"))
            self.facebook_request(
                method="facebook.users.getInfo",
                callback=self.async_callback(
                    self._on_get_user_info, callback, session),
                session_key=session["session_key"],
                uids=session["uid"],
                fields="uid,first_name,last_name,name,locale,pic_square,"
                       "profile_url,username")
    
        def facebook_request(self, method, callback, **args):
            """Makes a Facebook API REST request.
    
            We automatically include the Facebook API key and signature, but
            it is the callers responsibility to include 'session_key' and any
            other required arguments to the method.
    
            The available Facebook methods are documented here:
            http://wiki.developers.facebook.com/index.php/API
    
            Here is an example for the stream.get() method::
    
                class MainHandler(cyclone.web.RequestHandler,
                                  cyclone.auth.FacebookMixin):
                    @cyclone.web.authenticated
                    @cyclone.web.asynchronous
                    def get(self):
                        self.facebook_request(
                            method="stream.get",
                            callback=self.async_callback(self._on_stream),
                            session_key=self.current_user["session_key"])
    
                    def _on_stream(self, stream):
                        if stream is None:
                           # Not authorized to read the stream yet?
                           self.redirect(self.authorize_redirect("read_stream"))
                           return
                        self.render("stream.html", stream=stream)
            """
            self.require_setting("facebook_api_key", "Facebook Connect")
            self.require_setting("facebook_secret", "Facebook Connect")
            if not method.startswith("facebook."):
                method = "facebook." + method
            args["api_key"] = self.settings["facebook_api_key"]
            args["v"] = "1.0"
            args["method"] = method
            args["call_id"] = str(long(time.time() * 1e6))
            args["format"] = "json"
            args["sig"] = self._signature(args)
            url = "http://api.facebook.com/restserver.php?" + \
                urllib.urlencode(args)
            d = httpclient.fetch(url)
            d.addCallback(self.async_callback(self._parse_response, callback))
    
        def _on_get_user_info(self, callback, session, users):
            if users is None:
                callback(None)
                return
            callback({
                "name": users[0]["name"],
                "first_name": users[0]["first_name"],
                "last_name": users[0]["last_name"],
                "uid": users[0]["uid"],
                "locale": users[0]["locale"],
                "pic_square": users[0]["pic_square"],
                "profile_url": users[0]["profile_url"],
                "username": users[0].get("username"),
                "session_key": session["session_key"],
                "session_expires": session.get("expires"),
            })
    
        def _parse_response(self, callback, response):
            if response.error:
                log.msg("HTTP error from Facebook: %s" % response.error)
                callback(None)
                return
            try:
                json = escape.json_decode(response.body)
            except:
                log.msg("Invalid JSON from Facebook: %r" % response.body)
                callback(None)
                return
            if isinstance(json, dict) and json.get("error_code"):
                log.msg("Facebook error: %d: %r" %
                        (json["error_code"], json.get("error_msg")))
                callback(None)
                return
            callback(json)
    
        def _signature(self, args):
            parts = ["%s=%s" % (n, args[n]) for n in sorted(args.keys())]
            body = "".join(parts) + self.settings["facebook_secret"]
            if isinstance(body, unicode):
                body = body.encode("utf-8")
            return hashlib.md5(body).hexdigest()
    
    
    class FacebookGraphMixin(OAuth2Mixin):
        """Facebook authentication using the new Graph API and OAuth2."""
        _OAUTH_ACCESS_TOKEN_URL = "https://graph.facebook.com/oauth/access_token?"
        _OAUTH_AUTHORIZE_URL = "https://graph.facebook.com/oauth/authorize?"
        _OAUTH_NO_CALLBACKS = False
    
        def get_authenticated_user(self, redirect_uri, client_id, client_secret,
                                  code, callback, extra_fields=None):
            """Handles the login for the Facebook user, returning a user object.
    
            Example usage::
    
                class FacebookGraphLoginHandler(LoginHandler,
                                                tornado.auth.FacebookGraphMixin):
                  @tornado.web.asynchronous
                  def get(self):
                      if self.get_argument("code", False):
                          self.get_authenticated_user(
                            redirect_uri='/auth/facebookgraph/',
                            client_id=self.settings["facebook_api_key"],
                            client_secret=self.settings["facebook_secret"],
                            code=self.get_argument("code"),
                            callback=self.async_callback(
                              self._on_login))
                          return
                      self.authorize_redirect(
                            redirect_uri='/auth/facebookgraph/',
                            client_id=self.settings["facebook_api_key"],
                            extra_params={"scope": "read_stream,offline_access"})
    
                  def _on_login(self, user):
                    logging.error(user)
                    self.finish()
    
            """
            args = {
              "redirect_uri": redirect_uri,
              "code": code,
              "client_id": client_id,
              "client_secret": client_secret,
            }
    
            fields = set(['id', 'name', 'first_name', 'last_name',
                          'locale', 'picture', 'link'])
            if extra_fields:
                fields.update(extra_fields)
    
            httpclient.fetch(self._oauth_request_token_url(**args))\
            .addCallback(self.async_callback(
                                self._on_access_token, redirect_uri, client_id,
                                client_secret, callback, fields))
    
        def _on_access_token(self, redirect_uri, client_id, client_secret,
                            callback, fields, response):
            if response.error:
                log.warning('Facebook auth error: %s' % str(response))
                callback(None)
                return
    
            args = escape.parse_qs_bytes(escape.native_str(response.body))
            session = {
                "access_token": args["access_token"][-1],
                "expires": args.get("expires")
            }
    
            self.facebook_request(
                path="/me",
                callback=self.async_callback(
                    self._on_get_user_info, callback, session, fields),
                access_token=session["access_token"],
                fields=",".join(fields)
                )
    
        def _on_get_user_info(self, callback, session, fields, user):
            if user is None:
                callback(None)
                return
            
            fieldmap = {}
            for field in fields:
                fieldmap[field] = user.get(field)
    
            fieldmap.update({"access_token": session["access_token"],
                             "session_expires": session.get("expires")})
            callback(fieldmap)
    
        def facebook_request(self, path, callback, access_token=None,
                               post_args=None, **args):
            """Fetches the given relative API path, e.g., "/btaylor/picture"
    
            If the request is a POST, post_args should be provided. Query
            string arguments should be given as keyword arguments.
    
            An introduction to the Facebook Graph API can be found at
            http://developers.facebook.com/docs/api
    
            Many methods require an OAuth access token which you can obtain
            through authorize_redirect() and get_authenticated_user(). The
            user returned through that process includes an 'access_token'
            attribute that can be used to make authenticated requests via
            this method. Example usage::
    
                class MainHandler(tornado.web.RequestHandler,
                                  tornado.auth.FacebookGraphMixin):
                    @tornado.web.authenticated
                    @tornado.web.asynchronous
                    def get(self):
                        self.facebook_request(
                            "/me/feed",
                            post_args={"message": "Posting from my cyclone app!"},
                            access_token=self.current_user["access_token"],
                            callback=self.async_callback(self._on_post))
    
                    def _on_post(self, new_entry):
                        if not new_entry:
                            # Call failed; perhaps missing permission?
                            self.authorize_redirect()
                            return
                        self.finish("Posted a message!")
    
            """
            url = "https://graph.facebook.com" + path
            all_args = {}
            if access_token:
                all_args["access_token"] = access_token
                all_args.update(args)
    
            if all_args:
                url += "?" + urllib.urlencode(all_args)
            callback = self.async_callback(self._on_facebook_request, callback)
            if post_args is not None:
                httpclient.fetch(url, method="POST",
                        body=urllib.urlencode(post_args)).addCallback(callback)
            else:
                httpclient.fetch(url).addCallback(callback)
    
        def _on_facebook_request(self, callback, response):
            if response.error:
                log.warning("Error response %s fetching %s", response.error,
                                response.request.url)
                callback(None)
                return
            callback(escape.json_decode(response.body))
    
    
    def _oauth_signature(consumer_token, method, url, parameters={}, token=None):
        """Calculates the HMAC-SHA1 OAuth signature for the given request.
    
        See http://oauth.net/core/1.0/#signing_process
        """
        parts = urlparse.urlparse(url)
        scheme, netloc, path = parts[:3]
        normalized_url = scheme.lower() + "://" + netloc.lower() + path
    
        base_elems = []
        base_elems.append(method.upper())
        base_elems.append(normalized_url)
        base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
                                   for k, v in sorted(parameters.items())))
        base_string = "&".join(_oauth_escape(e) for e in base_elems)
    
        key_elems = [escape.utf8(consumer_token["secret"])]
        key_elems.append(escape.utf8(token["secret"] if token else ""))
        key = "&".join(key_elems)
    
        hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
        return binascii.b2a_base64(hash.digest())[:-1]
    
    
    def _oauth10a_signature(consumer_token,
                            method, url, parameters={}, token=None):
        """Calculates the HMAC-SHA1 OAuth 1.0a signature for the given request.
    
        See http://oauth.net/core/1.0a/#signing_process
        """
        parts = urlparse.urlparse(url)
        scheme, netloc, path = parts[:3]
        normalized_url = scheme.lower() + "://" + netloc.lower() + path
    
        base_elems = []
        base_elems.append(method.upper())
        base_elems.append(normalized_url)
        base_elems.append("&".join("%s=%s" % (k, _oauth_escape(str(v)))
                                   for k, v in sorted(parameters.items())))
    
        base_string = "&".join(_oauth_escape(e) for e in base_elems)
        key_elems = [escape.utf8(
                     urllib.quote(consumer_token["secret"], safe='~'))]
        key_elems.append(escape.utf8(
                         urllib.quote(token["secret"], safe='~') if token else ""))
        key = "&".join(key_elems)
    
        hash = hmac.new(key, escape.utf8(base_string), hashlib.sha1)
        return binascii.b2a_base64(hash.digest())[:-1]
    
    
    def _oauth_escape(val):
        if isinstance(val, unicode_type):
            val = val.encode("utf-8")
        return urllib.quote(val, safe="~")
    
    
    def _oauth_parse_response(body):
        p = escape.parse_qs(body, keep_blank_values=False)
        token = dict(key=p["oauth_token"][0], secret=p["oauth_token_secret"][0])
    
        # Add the extra parameters the Provider included to the token
        special = ("oauth_token", "oauth_token_secret")
        token.update((k, p[k][0]) for k in p if k not in special)
        return token
    python-cyclone-1.1/cyclone/websocket.py0000644000175000017500000003252612124336260017355 0ustar  lunarlunar# coding: utf-8
    #
    # Copyright 2010 Alexandre Fiori
    # based on the original Tornado by Facebook
    #
    # Licensed under the Apache License, Version 2.0 (the "License"); you may
    # not use this file except in compliance with the License. You may obtain
    # a copy of the License at
    #
    #     http://www.apache.org/licenses/LICENSE-2.0
    #
    # Unless required by applicable law or agreed to in writing, software
    # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    # License for the specific language governing permissions and limitations
    # under the License.
    
    """Server-side implementation of the WebSocket protocol.
    
    `WebSocket `_  is a web technology
    providing full-duplex communications channels over a single TCP connection.
    
    For more information, check out the `WebSocket demos
    `_.
    """
    import base64
    import functools
    import hashlib
    import struct
    
    import cyclone
    import cyclone.web
    import cyclone.escape
    
    from twisted.python import log
    
    
    class _NotEnoughFrame(Exception):
        pass
    
    
    class WebSocketHandler(cyclone.web.RequestHandler):
        """Subclass this class to create a basic WebSocket handler.
    
        Override messageReceived to handle incoming messages.
    
        See http://dev.w3.org/html5/websockets/ for details on the
        JavaScript interface.  The protocol is specified at
        http://tools.ietf.org/html/rfc6455.
    
        Here is an example Web Socket handler that echos back all received messages
        back to the client::
    
          class EchoWebSocket(websocket.WebSocketHandler):
              def connectionMade(self):
                  print "WebSocket connected"
    
              def messageReceived(self, message):
                  self.sendMessage(u"You said: " + message)
    
              def connectionLost(self, reason):
                  print "WebSocket disconnected"
    
        Web Sockets are not standard HTTP connections. The "handshake" is HTTP,
        but after the handshake, the protocol is message-based. Consequently,
        most of the Cyclone HTTP facilities are not available in handlers of this
        type. The only communication methods available to you is sendMessage().
    
        If you map the handler above to "/websocket" in your application, you can
        invoke it in JavaScript with::
    
          var ws = new WebSocket("ws://localhost:8888/websocket");
          ws.onopen = function() {
             ws.send("Hello, world");
          };
          ws.onmessage = function (evt) {
             alert(evt.data);
          };
    
        This script pops up an alert box that says "You said: Hello, world".
        """
        def __init__(self, application, request, **kwargs):
            cyclone.web.RequestHandler.__init__(self, application, request,
                                                **kwargs)
            self.application = application
            self.request = request
            self.transport = request.connection.transport
            self.ws_protocol = None
            self.notifyFinish().addCallback(self.connectionLost)
    
        def headersReceived(self):
            pass
    
        def connectionMade(self, *args, **kwargs):
            pass
    
        def connectionLost(self, reason):
            pass
    
        def messageReceived(self, message):
            """Gets called when a message is received from the peer."""
            pass
    
        def sendMessage(self, message):
            """Sends the given message to the client of this Web Socket.
    
            The message may be either a string or a dict (which will be
            encoded as json).
            """
            if isinstance(message, dict):
                message = cyclone.escape.json_encode(message)
            if isinstance(message, unicode):
                message = message.encode("utf-8")
            assert isinstance(message, str)
            self.ws_protocol.sendMessage(message)
    
        def _rawDataReceived(self, data):
            self.ws_protocol.handleRawData(data)
    
        def _execute(self, transforms, *args, **kwargs):
            self._transforms = transforms or list()
            try:
                assert self.request.headers["Upgrade"].lower() == "websocket"
            except:
                return self.forbidConnection("Expected WebSocket Headers")
    
            self._connectionMade = functools.partial(self.connectionMade,
                                                     *args, **kwargs)
    
            if "Sec-Websocket-Version" in self.request.headers and \
                self.request.headers['Sec-Websocket-Version'] in ('7', '8', '13'):
                self.ws_protocol = WebSocketProtocol17(self)
            elif "Sec-WebSocket-Version" in self.request.headers:
                self.transport.write(cyclone.escape.utf8(
                    "HTTP/1.1 426 Upgrade Required\r\n"
                    "Sec-WebSocket-Version: 8\r\n\r\n"))
                self.transport.loseConnection()
            else:
                self.ws_protocol = WebSocketProtocol76(self)
    
            self.request.connection.setRawMode()
            self.request.connection.rawDataReceived = \
                self.ws_protocol.rawDataReceived
            self.ws_protocol.acceptConnection()
    
        def forbidConnection(self, message):
            self.transport.write(
                "HTTP/1.1 403 Forbidden\r\nContent-Length: %s\r\n\r\n%s" %
                (str(len(message)), message))
            return self.transport.loseConnection()
    
    
    class WebSocketProtocol(object):
        def __init__(self, handler):
            self.handler = handler
            self.request = handler.request
            self.transport = handler.transport
    
        def acceptConnection(self):
            pass
    
        def rawDataReceived(self, data):
            pass
    
        def sendMessage(self, message):
            pass
    
    
    class WebSocketProtocol17(WebSocketProtocol):
        def __init__(self, handler):
            WebSocketProtocol.__init__(self, handler)
    
            self._partial_data = None
    
            self._frame_fin = None
            self._frame_rsv = None
            self._frame_ops = None
            self._frame_mask = None
            self._frame_payload_length = None
            self._frame_header_length = None
    
            self._data_len = None
            self._header_index = None
    
            self._message_buffer = ""
    
        def acceptConnection(self):
            log.msg('Using ws spec (draft 17)')
    
            # The difference between version 8 and 13 is that in 8 the
            # client sends a "Sec-Websocket-Origin" header and in 13 it's
            # simply "Origin".
            if 'Origin' in self.request.headers:
                origin = self.request.headers['Origin']
            else:
                origin = self.request.headers['Sec-Websocket-Origin']
    
            key = self.request.headers['Sec-Websocket-Key']
            accept = base64.b64encode(hashlib.sha1("%s%s" %
                (key, '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')).digest())
    
            self.transport.write(
                "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
                "Upgrade: WebSocket\r\n"
                "Connection: Upgrade\r\n"
                "Sec-WebSocket-Accept: %s\r\n"
                "Server: cyclone/%s\r\n"
                "WebSocket-Origin: %s\r\n"
                "WebSocket-Location: ws://%s%s\r\n\r\n" %
                (accept, cyclone.version, origin,
                 self.request.host, self.request.path))
    
            self.handler._connectionMade()
    
        def rawDataReceived(self, data):
    
            if self._partial_data:
                data = self._partial_data + data
                self._partial_data = None
    
            try:
                self._processFrameHeader(data)
            except _NotEnoughFrame:
                self._partial_data = data
                return
    
            self._message_buffer += self._extractMessageFromFrame(data)
    
            if self._frame_fin:
                if self._frame_ops == 8:
                    self.sendMessage(self._message_buffer, code=0x88)
                    #self.handler.connectionLost(self._message_buffer)
                elif self._frame_ops == 9:
                    self.sendMessage(self._message_buffer, code=0x8A)
                else:
                    self.handler.messageReceived(self._message_buffer)
                self._message_buffer = ""
    
            # if there is still data after this frame, process again
            current_len = self._frame_header_len + self._frame_payload_len
            if current_len < self._data_len:
                self.rawDataReceived(data[current_len:])
    
        def _processFrameHeader(self, data):
    
            self._data_len = len(data)
    
            # we need at least 2 bytes to start processing a frame
            if self._data_len < 2:
                raise _NotEnoughFrame()
    
            # first byte contains fin, rsv and ops
            b = ord(data[0])
            self._frame_fin = (b & 0x80) != 0
            self._frame_rsv = (b & 0x70) >> 4
            self._frame_ops = b & 0x0f
    
            # second byte contains mask and payload length
            b = ord(data[1])
            self._frame_mask = (b & 0x80) != 0
            frame_payload_len1 = b & 0x7f
    
            # accumulating for self._frame_header_len
            i = 2
    
            if frame_payload_len1 < 126:
                self._frame_payload_len = frame_payload_len1
            elif frame_payload_len1 == 126:
                i += 2
                if self._data_len < i:
                    raise _NotEnoughFrame()
                self._frame_payload_len = struct.unpack("!H", data[i - 2:i])[0]
            elif frame_payload_len1 == 127:
                i += 8
                if self._data_len < i:
                    raise _NotEnoughFrame()
                self._frame_payload_len = struct.unpack("!Q", data[i - 8:i])[0]
    
            if (self._frame_mask):
                i += 4
    
            if (self._data_len - i) < self._frame_payload_len:
                raise _NotEnoughFrame()
    
            self._frame_header_len = i
    
        def _extractMessageFromFrame(self, data):
            i = self._frame_header_len
    
            # when payload is masked, extract frame mask
            frame_mask = None
            frame_mask_array = []
            if self._frame_mask:
                frame_mask = data[i - 4:i]
                for j in range(0, 4):
                    frame_mask_array.append(ord(frame_mask[j]))
                payload = bytearray(data[i:i + self._frame_payload_len])
                for k in xrange(0, self._frame_payload_len):
                    payload[k] ^= frame_mask_array[k % 4]
    
                return str(payload)
    
        def sendMessage(self, message, code=0x81):
            if isinstance(message, unicode):
                message = message.encode('utf8')
            length = len(message)
            newFrame = []
            newFrame.append(code)
            newFrame = bytearray(newFrame)
            if length <= 125:
                newFrame.append(length)
            elif length > 125 and length < 65536:
                newFrame.append(126)
                newFrame += struct.pack('!H', length)
            elif length >= 65536:
                newFrame.append(127)
                newFrame += struct.pack('!Q', length)
    
            newFrame += message
            self.transport.write(str(newFrame))
    
    
    class WebSocketProtocol76(WebSocketProtocol):
        def __init__(self, handler):
            WebSocketProtocol.__init__(self, handler)
    
            self._k1 = None
            self._k2 = None
            self._nonce = None
    
            self._postheader = False
            self._protocol = None
    
        def acceptConnection(self):
            if "Sec-Websocket-Key1" not in self.request.headers or \
                "Sec-Websocket-Key2" not in self.request.headers:
                log.msg('Using old ws spec (draft 75)')
                self.transport.write(
                    "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
                    "Upgrade: WebSocket\r\n"
                    "Connection: Upgrade\r\n"
                    "Server: cyclone/%s\r\n"
                    "WebSocket-Origin: %s\r\n"
                    "WebSocket-Location: ws://%s%s\r\n\r\n" %
                    (cyclone.version, self.request.headers["Origin"],
                     self.request.host, self.request.path))
                self._protocol = 75
            else:
                log.msg('Using ws draft 76 header exchange')
                self._k1 = self.request.headers["Sec-WebSocket-Key1"]
                self._k2 = self.request.headers["Sec-WebSocket-Key2"]
                self._protocol = 76
            self._postheader = True
    
        def rawDataReceived(self, data):
            if self._postheader is True and \
               self._protocol >= 76 and len(data) == 8:
                self._nonce = data.strip()
                token = self._calculate_token(self._k1, self._k2, self._nonce)
                self.transport.write(
                    "HTTP/1.1 101 Web Socket Protocol Handshake\r\n"
                    "Upgrade: WebSocket\r\n"
                    "Connection: Upgrade\r\n"
                    "Server: cyclone/%s\r\n"
                    "Sec-WebSocket-Origin: %s\r\n"
                    "Sec-WebSocket-Location: ws://%s%s\r\n\r\n%s\r\n\r\n" %
                    (cyclone.version, self.request.headers["Origin"],
                     self.request.host, self.request.path, token))
                self._postheader = False
                self.handler.flush()
                self.handler._connectionMade()
                return
    
            try:
                messages = data.split('\xff')
                for message in messages[:-1]:
                    self.handler.messageReceived(message[1:])
            except Exception, e:
                log.msg("Invalid WebSocket Message '%s': %s" % (repr(data), e))
                self.handler._handle_request_exception(e)
    
        def sendMessage(self, message):
            self.transport.write("\x00" + message + "\xff")
    
        def _calculate_token(self, k1, k2, k3):
            token = struct.pack('>II8s', self._filterella(k1),
                                self._filterella(k2), k3)
            return hashlib.md5(token).digest()
    
        def _filterella(self, w):
            nums = []
            spaces = 0
            for l in w:
                if l.isdigit():
                    nums.append(l)
                if l.isspace():
                    spaces = spaces + 1
            x = int(''.join(nums)) / spaces
            return x