├── web ├── contrib │ ├── __init__.py │ └── template.py ├── __init__.py ├── test.py ├── wsgi.py ├── webopenid.py ├── http.py ├── net.py ├── browser.py ├── httpserver.py ├── session.py ├── form.py ├── webapi.py ├── debugerror.py ├── application.py └── utils.py ├── netpub.sh ├── kee ├── ct.css ├── index.html └── zepto.min.js ├── pplevels └── alexpong ├── jsonBunch.py └── passenger_wsgi.py /web/contrib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /netpub.sh: -------------------------------------------------------------------------------- 1 | rsync passenger_wsgi.py alexpb@chozabu.net:kiventedserve.chozabu.net/ 2 | rsync kee/index.html alexpb@chozabu.net:kiventedserve.chozabu.net/kee/ 3 | ssh alexpb@chozabu.net touch kiventedserve.chozabu.net/tmp/restart.txt 4 | -------------------------------------------------------------------------------- /web/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """web.py: makes web apps (http://webpy.org)""" 3 | 4 | from __future__ import generators 5 | 6 | __version__ = "0.33" 7 | __author__ = [ 8 | "Aaron Swartz ", 9 | "Anand Chitipothu " 10 | ] 11 | __license__ = "public domain" 12 | __contributors__ = "see http://webpy.org/changes" 13 | 14 | import utils, db, net, wsgi, http, webapi, httpserver, debugerror 15 | import template, form 16 | 17 | import session 18 | 19 | from utils import * 20 | from db import * 21 | from net import * 22 | from wsgi import * 23 | from http import * 24 | from webapi import * 25 | from httpserver import * 26 | from debugerror import * 27 | from application import * 28 | from browser import * 29 | import test 30 | try: 31 | import webopenid as openid 32 | except ImportError: 33 | pass # requires openid module 34 | 35 | -------------------------------------------------------------------------------- /kee/ct.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: e9f5f1; 3 | color: #222222; 4 | text-shadow: rgba(250,250,255, .4) 0em 0em 4px; 5 | font: 12pt Ubuntu, Verdana, Arial, Helvetica, sans-serif; 6 | margin: 10px; 7 | } 8 | 9 | #chatout { 10 | margin-left: 520px; 11 | } 12 | 13 | .PIPanel, body > ul > li { 14 | background: white; 15 | padding: 5px 10px; 16 | border: #bbbbbb 1px solid; 17 | text-align: left; 18 | margin: 5px; 19 | border-radius: 5px; 20 | box-shadow: 2px 2px 2px #bbbbbb; 21 | } 22 | 23 | .PIPanel:hover { 24 | border: gray 1px solid; 25 | border-color: #666666; 26 | } 27 | 28 | .PIPanel > span { 29 | font-size: 10pt; 30 | } 31 | 32 | .PIPanel > div { 33 | margin-top: 10px; 34 | } 35 | 36 | span.postname { 37 | font-weight: bold; 38 | } 39 | 40 | span.postid { 41 | font-size: 8pt; 42 | float: right; 43 | } 44 | 45 | body > ul { 46 | margin: 0px; 47 | padding: 0px; 48 | list-style-type: none; 49 | text-align: left; 50 | float: left; 51 | width: 480px; 52 | } 53 | -------------------------------------------------------------------------------- /web/test.py: -------------------------------------------------------------------------------- 1 | """test utilities 2 | (part of web.py) 3 | """ 4 | import unittest 5 | import sys, os 6 | import web 7 | 8 | TestCase = unittest.TestCase 9 | TestSuite = unittest.TestSuite 10 | 11 | def load_modules(names): 12 | return [__import__(name, None, None, "x") for name in names] 13 | 14 | def module_suite(module, classnames=None): 15 | """Makes a suite from a module.""" 16 | if classnames: 17 | return unittest.TestLoader().loadTestsFromNames(classnames, module) 18 | elif hasattr(module, 'suite'): 19 | return module.suite() 20 | else: 21 | return unittest.TestLoader().loadTestsFromModule(module) 22 | 23 | def doctest_suite(module_names): 24 | """Makes a test suite from doctests.""" 25 | import doctest 26 | suite = TestSuite() 27 | for mod in load_modules(module_names): 28 | suite.addTest(doctest.DocTestSuite(mod)) 29 | return suite 30 | 31 | def suite(module_names): 32 | """Creates a suite from multiple modules.""" 33 | suite = TestSuite() 34 | for mod in load_modules(module_names): 35 | suite.addTest(module_suite(mod)) 36 | return suite 37 | 38 | def runTests(suite): 39 | runner = unittest.TextTestRunner() 40 | return runner.run(suite) 41 | 42 | def main(suite=None): 43 | if not suite: 44 | main_module = __import__('__main__') 45 | # allow command line switches 46 | args = [a for a in sys.argv[1:] if not a.startswith('-')] 47 | suite = module_suite(main_module, args or None) 48 | 49 | result = runTests(suite) 50 | sys.exit(not result.wasSuccessful()) 51 | 52 | -------------------------------------------------------------------------------- /web/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI Utilities 3 | (from web.py) 4 | """ 5 | 6 | import os, sys 7 | 8 | import http 9 | import webapi as web 10 | from utils import listget 11 | from net import validaddr, validip 12 | import httpserver 13 | 14 | def runfcgi(func, addr=('localhost', 8000)): 15 | """Runs a WSGI function as a FastCGI server.""" 16 | import flup.server.fcgi as flups 17 | return flups.WSGIServer(func, multiplexed=True, bindAddress=addr).run() 18 | 19 | def runscgi(func, addr=('localhost', 4000)): 20 | """Runs a WSGI function as an SCGI server.""" 21 | import flup.server.scgi as flups 22 | return flups.WSGIServer(func, bindAddress=addr).run() 23 | 24 | def runwsgi(func): 25 | """ 26 | Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server, 27 | as appropriate based on context and `sys.argv`. 28 | """ 29 | 30 | if os.environ.has_key('SERVER_SOFTWARE'): # cgi 31 | os.environ['FCGI_FORCE_CGI'] = 'Y' 32 | 33 | if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi 34 | or os.environ.has_key('SERVER_SOFTWARE')): 35 | return runfcgi(func, None) 36 | 37 | if 'fcgi' in sys.argv or 'fastcgi' in sys.argv: 38 | args = sys.argv[1:] 39 | if 'fastcgi' in args: args.remove('fastcgi') 40 | elif 'fcgi' in args: args.remove('fcgi') 41 | if args: 42 | return runfcgi(func, validaddr(args[0])) 43 | else: 44 | return runfcgi(func, None) 45 | 46 | if 'scgi' in sys.argv: 47 | args = sys.argv[1:] 48 | args.remove('scgi') 49 | if args: 50 | return runscgi(func, validaddr(args[0])) 51 | else: 52 | return runscgi(func) 53 | 54 | return httpserver.runsimple(func, validip(listget(sys.argv, 1, ''))) 55 | 56 | def _is_dev_mode(): 57 | # quick hack to check if the program is running in dev mode. 58 | if os.environ.has_key('SERVER_SOFTWARE') \ 59 | or os.environ.has_key('PHP_FCGI_CHILDREN') \ 60 | or 'fcgi' in sys.argv or 'fastcgi' in sys.argv \ 61 | or 'mod_wsgi' in sys.argv: 62 | return False 63 | return True 64 | 65 | # When running the builtin-server, enable debug mode if not already set. 66 | web.config.setdefault('debug', _is_dev_mode()) 67 | -------------------------------------------------------------------------------- /web/contrib/template.py: -------------------------------------------------------------------------------- 1 | """ 2 | Interface to various templating engines. 3 | """ 4 | import os.path 5 | 6 | __all__ = [ 7 | "render_cheetah", "render_genshi", "render_mako", 8 | "cache", 9 | ] 10 | 11 | class render_cheetah: 12 | """Rendering interface to Cheetah Templates. 13 | 14 | Example: 15 | 16 | render = render_cheetah('templates') 17 | render.hello(name="cheetah") 18 | """ 19 | def __init__(self, path): 20 | # give error if Chetah is not installed 21 | from Cheetah.Template import Template 22 | self.path = path 23 | 24 | def __getattr__(self, name): 25 | from Cheetah.Template import Template 26 | path = os.path.join(self.path, name + ".html") 27 | 28 | def template(**kw): 29 | t = Template(file=path, searchList=[kw]) 30 | return t.respond() 31 | 32 | return template 33 | 34 | class render_genshi: 35 | """Rendering interface genshi templates. 36 | Example: 37 | 38 | for xml/html templates. 39 | 40 | render = render_genshi(['templates/']) 41 | render.hello(name='genshi') 42 | 43 | For text templates: 44 | 45 | render = render_genshi(['templates/'], type='text') 46 | render.hello(name='genshi') 47 | """ 48 | 49 | def __init__(self, *a, **kwargs): 50 | from genshi.template import TemplateLoader 51 | 52 | self._type = kwargs.pop('type', None) 53 | self._loader = TemplateLoader(*a, **kwargs) 54 | 55 | def __getattr__(self, name): 56 | # Assuming all templates are html 57 | path = name + ".html" 58 | 59 | if self._type == "text": 60 | from genshi.template import TextTemplate 61 | cls = TextTemplate 62 | type = "text" 63 | else: 64 | cls = None 65 | type = None 66 | 67 | t = self._loader.load(path, cls=cls) 68 | def template(**kw): 69 | stream = t.generate(**kw) 70 | if type: 71 | return stream.render(type) 72 | else: 73 | return stream.render() 74 | return template 75 | 76 | class render_jinja: 77 | """Rendering interface to Jinja2 Templates 78 | 79 | Example: 80 | 81 | render= render_jinja('templates') 82 | render.hello(name='jinja2') 83 | """ 84 | def __init__(self, *a, **kwargs): 85 | extensions = kwargs.pop('extensions', []) 86 | globals = kwargs.pop('globals', {}) 87 | 88 | from jinja2 import Environment,FileSystemLoader 89 | self._lookup = Environment(loader=FileSystemLoader(*a, **kwargs), extensions=extensions) 90 | self._lookup.globals.update(globals) 91 | 92 | def __getattr__(self, name): 93 | # Assuming all templates end with .html 94 | path = name + '.html' 95 | t = self._lookup.get_template(path) 96 | return t.render 97 | 98 | class render_mako: 99 | """Rendering interface to Mako Templates. 100 | 101 | Example: 102 | 103 | render = render_mako(directories=['templates']) 104 | render.hello(name="mako") 105 | """ 106 | def __init__(self, *a, **kwargs): 107 | from mako.lookup import TemplateLookup 108 | self._lookup = TemplateLookup(*a, **kwargs) 109 | 110 | def __getattr__(self, name): 111 | # Assuming all templates are html 112 | path = name + ".html" 113 | t = self._lookup.get_template(path) 114 | return t.render 115 | 116 | class cache: 117 | """Cache for any rendering interface. 118 | 119 | Example: 120 | 121 | render = cache(render_cheetah("templates/")) 122 | render.hello(name='cache') 123 | """ 124 | def __init__(self, render): 125 | self._render = render 126 | self._cache = {} 127 | 128 | def __getattr__(self, name): 129 | if name not in self._cache: 130 | self._cache[name] = getattr(self._render, name) 131 | return self._cache[name] 132 | -------------------------------------------------------------------------------- /web/webopenid.py: -------------------------------------------------------------------------------- 1 | """openid.py: an openid library for web.py 2 | 3 | Notes: 4 | 5 | - This will create a file called .openid_secret_key in the 6 | current directory with your secret key in it. If someone 7 | has access to this file they can log in as any user. And 8 | if the app can't find this file for any reason (e.g. you 9 | moved the app somewhere else) then each currently logged 10 | in user will get logged out. 11 | 12 | - State must be maintained through the entire auth process 13 | -- this means that if you have multiple web.py processes 14 | serving one set of URLs or if you restart your app often 15 | then log ins will fail. You have to replace sessions and 16 | store for things to work. 17 | 18 | - We set cookies starting with "openid_". 19 | 20 | """ 21 | 22 | import os 23 | import random 24 | import hmac 25 | import __init__ as web 26 | import openid.consumer.consumer 27 | import openid.store.memstore 28 | 29 | sessions = {} 30 | store = openid.store.memstore.MemoryStore() 31 | 32 | def _secret(): 33 | try: 34 | secret = file('.openid_secret_key').read() 35 | except IOError: 36 | # file doesn't exist 37 | secret = os.urandom(20) 38 | file('.openid_secret_key', 'w').write(secret) 39 | return secret 40 | 41 | def _hmac(identity_url): 42 | return hmac.new(_secret(), identity_url).hexdigest() 43 | 44 | def _random_session(): 45 | n = random.random() 46 | while n in sessions: 47 | n = random.random() 48 | n = str(n) 49 | return n 50 | 51 | def status(): 52 | oid_hash = web.cookies().get('openid_identity_hash', '').split(',', 1) 53 | if len(oid_hash) > 1: 54 | oid_hash, identity_url = oid_hash 55 | if oid_hash == _hmac(identity_url): 56 | return identity_url 57 | return None 58 | 59 | def form(openid_loc): 60 | oid = status() 61 | if oid: 62 | return ''' 63 |
64 | OpenID 65 | %s 66 | 67 | 68 | 69 |
''' % (openid_loc, oid, web.ctx.fullpath) 70 | else: 71 | return ''' 72 |
73 | 75 | 76 | 77 |
''' % (openid_loc, web.ctx.fullpath) 78 | 79 | def logout(): 80 | web.setcookie('openid_identity_hash', '', expires=-1) 81 | 82 | class host: 83 | def POST(self): 84 | # unlike the usual scheme of things, the POST is actually called 85 | # first here 86 | i = web.input(return_to='/') 87 | if i.get('action') == 'logout': 88 | logout() 89 | return web.redirect(i.return_to) 90 | 91 | i = web.input('openid', return_to='/') 92 | 93 | n = _random_session() 94 | sessions[n] = {'webpy_return_to': i.return_to} 95 | 96 | c = openid.consumer.consumer.Consumer(sessions[n], store) 97 | a = c.begin(i.openid) 98 | f = a.redirectURL(web.ctx.home, web.ctx.home + web.ctx.fullpath) 99 | 100 | web.setcookie('openid_session_id', n) 101 | return web.redirect(f) 102 | 103 | def GET(self): 104 | n = web.cookies('openid_session_id').openid_session_id 105 | web.setcookie('openid_session_id', '', expires=-1) 106 | return_to = sessions[n]['webpy_return_to'] 107 | 108 | c = openid.consumer.consumer.Consumer(sessions[n], store) 109 | a = c.complete(web.input(), web.ctx.home + web.ctx.fullpath) 110 | 111 | if a.status.lower() == 'success': 112 | web.setcookie('openid_identity_hash', _hmac(a.identity_url) + ',' + a.identity_url) 113 | 114 | del sessions[n] 115 | return web.redirect(return_to) 116 | -------------------------------------------------------------------------------- /web/http.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTTP Utilities 3 | (from web.py) 4 | """ 5 | 6 | __all__ = [ 7 | "expires", "lastmodified", 8 | "prefixurl", "modified", 9 | "changequery", "url", 10 | "profiler", 11 | ] 12 | 13 | import sys, os, threading, urllib, urlparse 14 | try: import datetime 15 | except ImportError: pass 16 | import net, utils, webapi as web 17 | 18 | def prefixurl(base=''): 19 | """ 20 | Sorry, this function is really difficult to explain. 21 | Maybe some other time. 22 | """ 23 | url = web.ctx.path.lstrip('/') 24 | for i in xrange(url.count('/')): 25 | base += '../' 26 | if not base: 27 | base = './' 28 | return base 29 | 30 | def expires(delta): 31 | """ 32 | Outputs an `Expires` header for `delta` from now. 33 | `delta` is a `timedelta` object or a number of seconds. 34 | """ 35 | if isinstance(delta, (int, long)): 36 | delta = datetime.timedelta(seconds=delta) 37 | date_obj = datetime.datetime.utcnow() + delta 38 | web.header('Expires', net.httpdate(date_obj)) 39 | 40 | def lastmodified(date_obj): 41 | """Outputs a `Last-Modified` header for `datetime`.""" 42 | web.header('Last-Modified', net.httpdate(date_obj)) 43 | 44 | def modified(date=None, etag=None): 45 | """ 46 | Checks to see if the page has been modified since the version in the 47 | requester's cache. 48 | 49 | When you publish pages, you can include `Last-Modified` and `ETag` 50 | with the date the page was last modified and an opaque token for 51 | the particular version, respectively. When readers reload the page, 52 | the browser sends along the modification date and etag value for 53 | the version it has in its cache. If the page hasn't changed, 54 | the server can just return `304 Not Modified` and not have to 55 | send the whole page again. 56 | 57 | This function takes the last-modified date `date` and the ETag `etag` 58 | and checks the headers to see if they match. If they do, it returns 59 | `True` and sets the response status to `304 Not Modified`. It also 60 | sets `Last-Modified and `ETag` output headers. 61 | """ 62 | try: 63 | from __builtin__ import set 64 | except ImportError: 65 | # for python 2.3 66 | from sets import Set as set 67 | 68 | n = set([x.strip('" ') for x in web.ctx.env.get('HTTP_IF_NONE_MATCH', '').split(',')]) 69 | m = net.parsehttpdate(web.ctx.env.get('HTTP_IF_MODIFIED_SINCE', '').split(';')[0]) 70 | validate = False 71 | if etag: 72 | if '*' in n or etag in n: 73 | validate = True 74 | if date and m: 75 | # we subtract a second because 76 | # HTTP dates don't have sub-second precision 77 | if date-datetime.timedelta(seconds=1) <= m: 78 | validate = True 79 | 80 | if validate: web.ctx.status = '304 Not Modified' 81 | if date: lastmodified(date) 82 | if etag: web.header('ETag', '"' + etag + '"') 83 | return not validate 84 | 85 | def urlencode(query, doseq=0): 86 | """ 87 | Same as urllib.urlencode, but supports unicode strings. 88 | 89 | >>> urlencode({'text':'foo bar'}) 90 | 'text=foo+bar' 91 | >>> urlencode({'x': [1, 2]}, doseq=True) 92 | 'x=1&x=2' 93 | """ 94 | def convert(value, doseq=False): 95 | if doseq and isinstance(value, list): 96 | return [convert(v) for v in value] 97 | else: 98 | return utils.utf8(value) 99 | 100 | query = dict([(k, convert(v, doseq)) for k, v in query.items()]) 101 | return urllib.urlencode(query, doseq=doseq) 102 | 103 | def changequery(query=None, **kw): 104 | """ 105 | Imagine you're at `/foo?a=1&b=2`. Then `changequery(a=3)` will return 106 | `/foo?a=3&b=2` -- the same URL but with the arguments you requested 107 | changed. 108 | """ 109 | if query is None: 110 | query = web.rawinput(method='get') 111 | for k, v in kw.iteritems(): 112 | if v is None: 113 | query.pop(k, None) 114 | else: 115 | query[k] = v 116 | out = web.ctx.path 117 | if query: 118 | out += '?' + urlencode(query, doseq=True) 119 | return out 120 | 121 | def url(path=None, **kw): 122 | """ 123 | Makes url by concatinating web.ctx.homepath and path and the 124 | query string created using the arguments. 125 | """ 126 | if path is None: 127 | path = web.ctx.path 128 | if path.startswith("/"): 129 | out = web.ctx.homepath + path 130 | else: 131 | out = path 132 | 133 | if kw: 134 | out += '?' + urlencode(kw) 135 | 136 | return out 137 | 138 | def profiler(app): 139 | """Outputs basic profiling information at the bottom of each response.""" 140 | from utils import profile 141 | def profile_internal(e, o): 142 | out, result = profile(app)(e, o) 143 | return list(out) + ['
' + net.websafe(result) + '
'] 144 | return profile_internal 145 | 146 | if __name__ == "__main__": 147 | import doctest 148 | doctest.testmod() 149 | -------------------------------------------------------------------------------- /web/net.py: -------------------------------------------------------------------------------- 1 | """ 2 | Network Utilities 3 | (from web.py) 4 | """ 5 | 6 | __all__ = [ 7 | "validipaddr", "validipport", "validip", "validaddr", 8 | "urlquote", 9 | "httpdate", "parsehttpdate", 10 | "htmlquote", "htmlunquote", "websafe", 11 | ] 12 | 13 | import urllib, time 14 | try: import datetime 15 | except ImportError: pass 16 | 17 | def validipaddr(address): 18 | """ 19 | Returns True if `address` is a valid IPv4 address. 20 | 21 | >>> validipaddr('192.168.1.1') 22 | True 23 | >>> validipaddr('192.168.1.800') 24 | False 25 | >>> validipaddr('192.168.1') 26 | False 27 | """ 28 | try: 29 | octets = address.split('.') 30 | if len(octets) != 4: 31 | return False 32 | for x in octets: 33 | if not (0 <= int(x) <= 255): 34 | return False 35 | except ValueError: 36 | return False 37 | return True 38 | 39 | def validipport(port): 40 | """ 41 | Returns True if `port` is a valid IPv4 port. 42 | 43 | >>> validipport('9000') 44 | True 45 | >>> validipport('foo') 46 | False 47 | >>> validipport('1000000') 48 | False 49 | """ 50 | try: 51 | if not (0 <= int(port) <= 65535): 52 | return False 53 | except ValueError: 54 | return False 55 | return True 56 | 57 | def validip(ip, defaultaddr="0.0.0.0", defaultport=8080): 58 | """Returns `(ip_address, port)` from string `ip_addr_port`""" 59 | addr = defaultaddr 60 | port = defaultport 61 | 62 | ip = ip.split(":", 1) 63 | if len(ip) == 1: 64 | if not ip[0]: 65 | pass 66 | elif validipaddr(ip[0]): 67 | addr = ip[0] 68 | elif validipport(ip[0]): 69 | port = int(ip[0]) 70 | else: 71 | raise ValueError, ':'.join(ip) + ' is not a valid IP address/port' 72 | elif len(ip) == 2: 73 | addr, port = ip 74 | if not validipaddr(addr) and validipport(port): 75 | raise ValueError, ':'.join(ip) + ' is not a valid IP address/port' 76 | port = int(port) 77 | else: 78 | raise ValueError, ':'.join(ip) + ' is not a valid IP address/port' 79 | return (addr, port) 80 | 81 | def validaddr(string_): 82 | """ 83 | Returns either (ip_address, port) or "/path/to/socket" from string_ 84 | 85 | >>> validaddr('/path/to/socket') 86 | '/path/to/socket' 87 | >>> validaddr('8000') 88 | ('0.0.0.0', 8000) 89 | >>> validaddr('127.0.0.1') 90 | ('127.0.0.1', 8080) 91 | >>> validaddr('127.0.0.1:8000') 92 | ('127.0.0.1', 8000) 93 | >>> validaddr('fff') 94 | Traceback (most recent call last): 95 | ... 96 | ValueError: fff is not a valid IP address/port 97 | """ 98 | if '/' in string_: 99 | return string_ 100 | else: 101 | return validip(string_) 102 | 103 | def urlquote(val): 104 | """ 105 | Quotes a string for use in a URL. 106 | 107 | >>> urlquote('://?f=1&j=1') 108 | '%3A//%3Ff%3D1%26j%3D1' 109 | >>> urlquote(None) 110 | '' 111 | >>> urlquote(u'\u203d') 112 | '%E2%80%BD' 113 | """ 114 | if val is None: return '' 115 | if not isinstance(val, unicode): val = str(val) 116 | else: val = val.encode('utf-8') 117 | return urllib.quote(val) 118 | 119 | def httpdate(date_obj): 120 | """ 121 | Formats a datetime object for use in HTTP headers. 122 | 123 | >>> import datetime 124 | >>> httpdate(datetime.datetime(1970, 1, 1, 1, 1, 1)) 125 | 'Thu, 01 Jan 1970 01:01:01 GMT' 126 | """ 127 | return date_obj.strftime("%a, %d %b %Y %H:%M:%S GMT") 128 | 129 | def parsehttpdate(string_): 130 | """ 131 | Parses an HTTP date into a datetime object. 132 | 133 | >>> parsehttpdate('Thu, 01 Jan 1970 01:01:01 GMT') 134 | datetime.datetime(1970, 1, 1, 1, 1, 1) 135 | """ 136 | try: 137 | t = time.strptime(string_, "%a, %d %b %Y %H:%M:%S %Z") 138 | except ValueError: 139 | return None 140 | return datetime.datetime(*t[:6]) 141 | 142 | def htmlquote(text): 143 | """ 144 | Encodes `text` for raw use in HTML. 145 | 146 | >>> htmlquote("<'&\\">") 147 | '<'&">' 148 | """ 149 | text = text.replace("&", "&") # Must be done first! 150 | text = text.replace("<", "<") 151 | text = text.replace(">", ">") 152 | text = text.replace("'", "'") 153 | text = text.replace('"', """) 154 | return text 155 | 156 | def htmlunquote(text): 157 | """ 158 | Decodes `text` that's HTML quoted. 159 | 160 | >>> htmlunquote('<'&">') 161 | '<\\'&">' 162 | """ 163 | text = text.replace(""", '"') 164 | text = text.replace("'", "'") 165 | text = text.replace(">", ">") 166 | text = text.replace("<", "<") 167 | text = text.replace("&", "&") # Must be done last! 168 | return text 169 | 170 | def websafe(val): 171 | """ 172 | Converts `val` so that it's safe for use in UTF-8 HTML. 173 | 174 | >>> websafe("<'&\\">") 175 | '<'&">' 176 | >>> websafe(None) 177 | '' 178 | >>> websafe(u'\u203d') 179 | '\\xe2\\x80\\xbd' 180 | """ 181 | if val is None: 182 | return '' 183 | if isinstance(val, unicode): 184 | val = val.encode('utf-8') 185 | val = str(val) 186 | return htmlquote(val) 187 | 188 | if __name__ == "__main__": 189 | import doctest 190 | doctest.testmod() 191 | -------------------------------------------------------------------------------- /pplevels/alexpong: -------------------------------------------------------------------------------- 1 | {"jointslist": [], "ents": [{"physics_renderer": {"width": 190.0, "texture": "plank", "height": 10.0}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 57, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 540.0, "x": 288.0}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 10.0, "width": 190.0}], "shape_type": "box", "body": {"position": [288.0, 540.0], "angle": 0.0, "vel_limit": Infinity, "mass": Infinity, "ang_vel_limit": Infinity, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": 0.0}}, {"physics_renderer": {"width": 464.0, "texture": "plank", "height": 10.0}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 58, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 307.0, "x": 193.0}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 10.0, "width": 464.0}], "shape_type": "box", "body": {"position": [193.0, 307.0], "angle": 1.5700000524520874, "vel_limit": Infinity, "mass": Infinity, "ang_vel_limit": Infinity, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": 1.5700000524520874}}, {"physics_renderer": {"width": 190.0, "texture": "plank", "height": 10.0}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 59, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 76.0, "x": 288.0}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 10.0, "width": 190.0}], "shape_type": "box", "body": {"position": [288.0, 76.0], "angle": 3.140000104904175, "vel_limit": Infinity, "mass": Infinity, "ang_vel_limit": Infinity, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": 3.140000104904175}}, {"physics_renderer": {"width": 185.00270080566406, "texture": "plank", "height": 10.0}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 60, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 76.5, "x": 553.5}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 10.0, "width": 185.00270080566406}], "shape_type": "box", "body": {"position": [553.5, 76.5], "angle": 3.1361873149871826, "vel_limit": Infinity, "mass": Infinity, "ang_vel_limit": Infinity, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": 3.1361873149871826}}, {"physics_renderer": {"width": 464.00970458984375, "texture": "plank", "height": 10.0}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 61, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 310.0, "x": 644.5}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 10.0, "width": 464.00970458984375}], "shape_type": "box", "body": {"position": [644.5, 310.0], "angle": -1.559999942779541, "vel_limit": Infinity, "mass": Infinity, "ang_vel_limit": Infinity, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": -1.559999942779541}}, {"physics_renderer": {"width": 171.0029296875, "texture": "plank", "height": 10.0}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 62, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 542.5, "x": 557.5}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 10.0, "width": 171.0029296875}], "shape_type": "box", "body": {"position": [557.5, 542.5], "angle": -0.009999999776482582, "vel_limit": Infinity, "mass": Infinity, "ang_vel_limit": Infinity, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": -0.009999999776482582}}, {"physics_renderer": {"width": 17.20465087890625, "texture": "sheep", "height": 17.20465087890625}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 63, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 279.5981750488281, "x": 362.67828369140625}, "physics": {"shapes": [{"collision_type": "default", "radius": 8.602325439453125, "group": 0, "elasticity": 2.0, "friction": 1.0}], "shape_type": "circle", "body": {"position": [362.67828369140625, 279.5981750488281], "angle": 241.3647955586093, "vel_limit": 2048.0, "mass": 10.0, "ang_vel_limit": 34.906585693359375, "velocity": [-113.36307525634766, -103.90167236328125], "angular_velocity": 12.307011981441374}}, "rotate": {"r": 241.3647918701172}}, {"physics_renderer": {"width": 77.0, "texture": "face_box", "height": 18.0}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 64, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 107.0, "x": 422.5}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 18.0, "width": 77.0}], "shape_type": "box", "body": {"position": [422.5, 107.0], "angle": 0.0, "vel_limit": 2048.0, "mass": 10.0, "ang_vel_limit": 34.906585693359375, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": 0.0}}, {"physics_renderer": {"width": 90.0, "texture": "face_box", "height": 21.0}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 56, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 511.5, "x": 428.0}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 21.0, "width": 90.0}], "shape_type": "box", "body": {"position": [428.0, 511.5], "angle": 0.0, "vel_limit": 2048.0, "mass": 10.0, "ang_vel_limit": 34.906585693359375, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": 0.0}}, {"physics_renderer": {"width": 65.11528015136719, "texture": "grassyrock", "height": 65.11528015136719}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 65, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 330.0, "x": 191.0}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 65.11528015136719, "width": 65.11528015136719}], "shape_type": "box", "body": {"position": [191.0, 330.0], "angle": -2.4000000953674316, "vel_limit": Infinity, "mass": Infinity, "ang_vel_limit": Infinity, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": -2.4000000953674316}}, {"physics_renderer": {"width": 60.8276252746582, "texture": "grassyrock", "height": 60.8276252746582}, "color": [1.0, 1.0, 1.0, 1.0], "orig_id": 66, "load_order": ["color", "position", "rotate", "physics", "physics_renderer"], "position": {"y": 327.0, "x": 640.0}, "physics": {"shapes": [{"collision_type": "default", "group": 0, "elasticity": 0.5, "friction": 1.0, "height": 60.8276252746582, "width": 60.8276252746582}], "shape_type": "box", "body": {"position": [640.0, 327.0], "angle": -0.7599999904632568, "vel_limit": Infinity, "mass": Infinity, "ang_vel_limit": Infinity, "velocity": [0.0, 0.0], "angular_velocity": 0.0}}, "rotate": {"r": -0.7599999904632568}}], "collision_typesdict": {"customphyszone": {"default": {"pre_solve": "physicsZone"}}, "pusher": {"default": {"begin": "None", "pre_solve": "pushSecondWithFirst"}, "pusher": {"pre_solve": "returnFalse"}}, "default": {"default": {"begin": "None", "pre_solve": "None"}}, "physzone": {"default": {"begin": "None", "pre_solve": "physicsZone"}}, "vortex": {"default": {"pre_solve": "grav2first"}, "physzone": {"pre_solve": "grav2first"}, "vortex": {"pre_solve": "grav2first"}}, "dragzone": {"default": {"pre_solve": "dragZoneSecond"}}}, "settings": {"startID": -1, "gravity": [0.0, 0.0], "finishID": -1}, "collision_typeslist": ["default", "vortex", "physzone", "dragzone", "customphyszone", "pusher"]} -------------------------------------------------------------------------------- /web/browser.py: -------------------------------------------------------------------------------- 1 | """Browser to test web applications. 2 | (from web.py) 3 | """ 4 | from utils import re_compile 5 | from net import htmlunquote 6 | 7 | import httplib, urllib, urllib2 8 | import copy 9 | from StringIO import StringIO 10 | 11 | DEBUG = False 12 | 13 | __all__ = [ 14 | "BrowserError", 15 | "Browser", "AppBrowser", 16 | "AppHandler" 17 | ] 18 | 19 | class BrowserError(Exception): 20 | pass 21 | 22 | class Browser: 23 | def __init__(self): 24 | import cookielib 25 | self.cookiejar = cookielib.CookieJar() 26 | self._cookie_processor = urllib2.HTTPCookieProcessor(self.cookiejar) 27 | self.form = None 28 | 29 | self.url = "http://0.0.0.0:8080/" 30 | self.path = "/" 31 | 32 | self.status = None 33 | self.data = None 34 | self._response = None 35 | self._forms = None 36 | 37 | def reset(self): 38 | """Clears all cookies and history.""" 39 | self.cookiejar.clear() 40 | 41 | def build_opener(self): 42 | """Builds the opener using urllib2.build_opener. 43 | Subclasses can override this function to prodive custom openers. 44 | """ 45 | return urllib2.build_opener() 46 | 47 | def do_request(self, req): 48 | if DEBUG: 49 | print 'requesting', req.get_method(), req.get_full_url() 50 | opener = self.build_opener() 51 | opener.add_handler(self._cookie_processor) 52 | try: 53 | self._response = opener.open(req) 54 | except urllib2.HTTPError, e: 55 | self._response = e 56 | 57 | self.url = self._response.geturl() 58 | self.path = urllib2.Request(self.url).get_selector() 59 | self.data = self._response.read() 60 | self.status = self._response.code 61 | self._forms = None 62 | self.form = None 63 | return self.get_response() 64 | 65 | def open(self, url, data=None, headers={}): 66 | """Opens the specified url.""" 67 | url = urllib.basejoin(self.url, url) 68 | req = urllib2.Request(url, data, headers) 69 | return self.do_request(req) 70 | 71 | def show(self): 72 | """Opens the current page in real web browser.""" 73 | f = open('page.html', 'w') 74 | f.write(self.data) 75 | f.close() 76 | 77 | import webbrowser, os 78 | url = 'file://' + os.path.abspath('page.html') 79 | webbrowser.open(url) 80 | 81 | def get_response(self): 82 | """Returns a copy of the current response.""" 83 | return urllib.addinfourl(StringIO(self.data), self._response.info(), self._response.geturl()) 84 | 85 | def get_soup(self): 86 | """Returns beautiful soup of the current document.""" 87 | import BeautifulSoup 88 | return BeautifulSoup.BeautifulSoup(self.data) 89 | 90 | def get_text(self, e=None): 91 | """Returns content of e or the current document as plain text.""" 92 | e = e or self.get_soup() 93 | return ''.join([htmlunquote(c) for c in e.recursiveChildGenerator() if isinstance(c, unicode)]) 94 | 95 | def _get_links(self): 96 | soup = self.get_soup() 97 | return [a for a in soup.findAll(name='a')] 98 | 99 | def get_links(self, text=None, text_regex=None, url=None, url_regex=None, predicate=None): 100 | """Returns all links in the document.""" 101 | return self._filter_links(self._get_links(), 102 | text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate) 103 | 104 | def follow_link(self, link=None, text=None, text_regex=None, url=None, url_regex=None, predicate=None): 105 | if link is None: 106 | links = self._filter_links(self.get_links(), 107 | text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate) 108 | link = links and links[0] 109 | 110 | if link: 111 | return self.open(link['href']) 112 | else: 113 | raise BrowserError("No link found") 114 | 115 | def find_link(self, text=None, text_regex=None, url=None, url_regex=None, predicate=None): 116 | links = self._filter_links(self.get_links(), 117 | text=text, text_regex=text_regex, url=url, url_regex=url_regex, predicate=predicate) 118 | return links and links[0] or None 119 | 120 | def _filter_links(self, links, 121 | text=None, text_regex=None, 122 | url=None, url_regex=None, 123 | predicate=None): 124 | predicates = [] 125 | if text is not None: 126 | predicates.append(lambda link: link.string == text) 127 | if text_regex is not None: 128 | predicates.append(lambda link: re_compile(text_regex).search(link.string or '')) 129 | if url is not None: 130 | predicates.append(lambda link: link.get('href') == url) 131 | if url_regex is not None: 132 | predicates.append(lambda link: re_compile(url_regex).search(link.get('href', ''))) 133 | if predicate: 134 | predicate.append(predicate) 135 | 136 | def f(link): 137 | for p in predicates: 138 | if not p(link): 139 | return False 140 | return True 141 | 142 | return [link for link in links if f(link)] 143 | 144 | def get_forms(self): 145 | """Returns all forms in the current document. 146 | The returned form objects implement the ClientForm.HTMLForm interface. 147 | """ 148 | if self._forms is None: 149 | import ClientForm 150 | self._forms = ClientForm.ParseResponse(self.get_response(), backwards_compat=False) 151 | return self._forms 152 | 153 | def select_form(self, name=None, predicate=None, index=0): 154 | """Selects the specified form.""" 155 | forms = self.get_forms() 156 | 157 | if name is not None: 158 | forms = [f for f in forms if f.name == name] 159 | if predicate: 160 | forms = [f for f in forms if predicate(f)] 161 | 162 | if forms: 163 | self.form = forms[index] 164 | return self.form 165 | else: 166 | raise BrowserError("No form selected.") 167 | 168 | def submit(self, **kw): 169 | """submits the currently selected form.""" 170 | if self.form is None: 171 | raise BrowserError("No form selected.") 172 | req = self.form.click(**kw) 173 | return self.do_request(req) 174 | 175 | def __getitem__(self, key): 176 | return self.form[key] 177 | 178 | def __setitem__(self, key, value): 179 | self.form[key] = value 180 | 181 | class AppBrowser(Browser): 182 | """Browser interface to test web.py apps. 183 | 184 | b = AppBrowser(app) 185 | b.open('/') 186 | b.follow_link(text='Login') 187 | 188 | b.select_form(name='login') 189 | b['username'] = 'joe' 190 | b['password'] = 'secret' 191 | b.submit() 192 | 193 | assert b.path == '/' 194 | assert 'Welcome joe' in b.get_text() 195 | """ 196 | def __init__(self, app): 197 | Browser.__init__(self) 198 | self.app = app 199 | 200 | def build_opener(self): 201 | return urllib2.build_opener(AppHandler(self.app)) 202 | 203 | class AppHandler(urllib2.HTTPHandler): 204 | """urllib2 handler to handle requests using web.py application.""" 205 | handler_order = 100 206 | 207 | def __init__(self, app): 208 | self.app = app 209 | 210 | def http_open(self, req): 211 | result = self.app.request( 212 | localpart=req.get_selector(), 213 | method=req.get_method(), 214 | host=req.get_host(), 215 | data=req.get_data(), 216 | headers=dict(req.header_items()), 217 | https=req.get_type() == "https" 218 | ) 219 | return self._make_response(result, req.get_full_url()) 220 | 221 | def https_open(self, req): 222 | return self.http_open(req) 223 | 224 | try: 225 | https_request = urllib2.HTTPHandler.do_request_ 226 | except AttributeError: 227 | # for python 2.3 228 | pass 229 | 230 | def _make_response(self, result, url): 231 | data = "\r\n".join(["%s: %s" % (k, v) for k, v in result.header_items]) 232 | headers = httplib.HTTPMessage(StringIO(data)) 233 | response = urllib.addinfourl(StringIO(result.data), headers, url) 234 | code, msg = result.status.split(None, 1) 235 | response.code, response.msg = int(code), msg 236 | return response 237 | -------------------------------------------------------------------------------- /jsonBunch.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import json 4 | def factory(methodname, cls=dict, flag=True): 5 | def method(self, *args, **kwargs): 6 | #print 'in teh factory' 7 | nargs = bunchify(args) 8 | nkwargs = bunchify(kwargs) 9 | retval = getattr(cls, methodname)(self, *nargs, **nkwargs) 10 | self.sync() 11 | return (retval) 12 | return method 13 | """ Bunch is a subclass of dict with attribute-style access. 14 | 15 | >>> b = Bunch() 16 | >>> b.hello = 'world' 17 | >>> b.hello 18 | 'world' 19 | >>> b['hello'] += "!" 20 | >>> b.hello 21 | 'world!' 22 | >>> b.foo = Bunch(lol=True) 23 | >>> b.foo.lol 24 | True 25 | >>> b.foo is b['foo'] 26 | True 27 | 28 | It is safe to import * from this module: 29 | 30 | __all__ = ('Bunch', 'bunchify','unbunchify') 31 | 32 | un/bunchify provide dictionary conversion; Bunches can also be 33 | converted via Bunch.to/fromDict(). 34 | """ 35 | 36 | __all__ = ('Bunch', 'bunchify','unbunchify') 37 | 38 | superself = 1 39 | 40 | def normalPrint(string): 41 | print (string) 42 | printer = normalPrint 43 | class Bunch(dict): 44 | __setitem__ = factory('__setitem__') 45 | __delitem__ = factory('__delitem__') 46 | clear = factory('clear') 47 | pop = factory('pop') 48 | popitem = factory('popitem') 49 | setdefault = factory('setdefault') 50 | update = factory('update') 51 | """ A dictionary that provides attribute-style access. 52 | 53 | >>> b = Bunch() 54 | >>> b.hello = 'world' 55 | >>> b.hello 56 | 'world' 57 | >>> b['hello'] += "!" 58 | >>> b.hello 59 | 'world!' 60 | >>> b.foo = Bunch(lol=True) 61 | >>> b.foo.lol 62 | True 63 | >>> b.foo is b['foo'] 64 | True 65 | 66 | A Bunch is a subclass of dict; it supports all the methods a dict does... 67 | 68 | >>> b.keys() 69 | ['foo', 'hello'] 70 | 71 | Including update()... 72 | 73 | >>> b.update({ 'ponies': 'are pretty!' }, hello=42) 74 | >>> print repr(b) 75 | Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') 76 | 77 | As well as iteration... 78 | 79 | >>> [ (k,b[k]) for k in b ] 80 | [('ponies', 'are pretty!'), ('foo', Bunch(lol=True)), ('hello', 42)] 81 | 82 | And "splats". 83 | 84 | >>> "The {knights} who say {ni}!".format(**Bunch(knights='lolcats', ni='can haz')) 85 | 'The lolcats who say can haz!' 86 | 87 | See unbunchify/Bunch.toDict, bunchify/Bunch.fromDict for notes about conversion. 88 | """ 89 | 90 | def __contains__(self, k): 91 | """ >>> b = Bunch(ponies='are pretty!') 92 | >>> 'ponies' in b 93 | True 94 | >>> 'foo' in b 95 | False 96 | >>> b['foo'] = 42 97 | >>> 'foo' in b 98 | True 99 | >>> b.hello = 'hai' 100 | >>> 'hello' in b 101 | True 102 | """ 103 | try: 104 | return hasattr(self, k) or dict.__contains__(self, k) 105 | except: 106 | return False 107 | 108 | # only called if k not found in normal places 109 | def __getattr__(self, k): 110 | #print 'in __getattr__ of jsonBunch' 111 | """ Gets key if it exists, otherwise throws AttributeError. 112 | 113 | nb. __getattr__ is only called if key is not found in normal places. 114 | 115 | >>> b = Bunch(bar='baz', lol={}) 116 | >>> b.foo 117 | Traceback (most recent call last): 118 | ... 119 | AttributeError: foo 120 | 121 | >>> b.bar 122 | 'baz' 123 | >>> getattr(b, 'bar') 124 | 'baz' 125 | >>> b['bar'] 126 | 'baz' 127 | 128 | >>> b.lol is b['lol'] 129 | True 130 | >>> b.lol is getattr(b, 'lol') 131 | True 132 | """ 133 | try: 134 | return self[k] 135 | except KeyError: 136 | raise AttributeError(k) 137 | 138 | def __setattr__(self, k, v): 139 | """ Sets attribute k if it exists, otherwise sets key k. A KeyError 140 | raised by set-item (only likely if you subclass Bunch) will 141 | propagate as an AttributeError instead. 142 | 143 | >>> b = Bunch(foo='bar', this_is='useful when subclassing') 144 | >>> b.values #doctest: +ELLIPSIS 145 | 146 | >>> b.values = 'uh oh' 147 | >>> b.values 148 | 'uh oh' 149 | >>> b['values'] 150 | Traceback (most recent call last): 151 | ... 152 | KeyError: 'values' 153 | """ 154 | 155 | try: 156 | # Throws exception if not in prototype chain 157 | object.__getattribute__(self, k) 158 | except AttributeError: 159 | try: 160 | self[k] = v 161 | 162 | except: 163 | raise AttributeError(k) 164 | else: 165 | object.__setattr__(self, k, v) 166 | 167 | 168 | def __delattr__(self, k): 169 | """ Deletes attribute k if it exists, otherwise deletes key k. A KeyError 170 | raised by deleting the key--such as when the key is missing--will 171 | propagate as an AttributeError instead. 172 | 173 | >>> b = Bunch(lol=42) 174 | >>> del b.values 175 | Traceback (most recent call last): 176 | ... 177 | AttributeError: 'Bunch' object attribute 'values' is read-only 178 | >>> del b.lol 179 | >>> b.lol 180 | Traceback (most recent call last): 181 | ... 182 | AttributeError: lol 183 | """ 184 | try: 185 | # Throws exception if not in prototype chain 186 | object.__getattribute__(self, k) 187 | except AttributeError: 188 | try: 189 | del self[k] 190 | except KeyError: 191 | raise AttributeError(k) 192 | else: 193 | object.__delattr__(self, k) 194 | self.sync() 195 | def toDict(self): 196 | """ Recursively converts a bunch back into a dictionary. 197 | 198 | >>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') 199 | >>> b.toDict() 200 | {'ponies': 'are pretty!', 'foo': {'lol': True}, 'hello': 42} 201 | 202 | See unbunchify for more info. 203 | """ 204 | return unbunchify(self) 205 | 206 | def __repr__(self): 207 | """ Invertible* string-form of a Bunch. 208 | 209 | >>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') 210 | >>> print repr(b) 211 | Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') 212 | >>> eval(repr(b)) 213 | Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') 214 | 215 | (*) Invertible so long as collection contents are each repr-invertible. 216 | """ 217 | keys = self.keys() 218 | keys.sort() 219 | args = ', '.join(['%s=%r' % (key, self[key]) for key in keys]) 220 | return '%s(%s)' % (self.__class__.__name__, args) 221 | 222 | @staticmethod 223 | def fromDict(d): 224 | """ Recursively transforms a dictionary into a Bunch via copy. 225 | 226 | >>> b = Bunch.fromDict({'urmom': {'sez': {'what': 'what'}}}) 227 | >>> b.urmom.sez.what 228 | 'what' 229 | 230 | See bunchify for more info. 231 | """ 232 | return bunchify(d) 233 | 234 | def staticToDict(self): 235 | global superself 236 | return (Bunch.toDict(superself)) 237 | def sync(self): 238 | print 'saving database changes' 239 | wholeDB = self.staticToDict() 240 | keys = self.keys() 241 | f = open('db.json','w') 242 | #global printer 243 | #printer('wholeDB = ' + str(wholeDB)) 244 | f.write(json.dumps(wholeDB)) 245 | f.close() 246 | #print 'saved' 247 | # While we could convert abstract types like Mapping or Iterable, I think 248 | # bunchify is more likely to "do what you mean" if it is conservative about 249 | # casting (ex: isinstance(str,Iterable) == True ). 250 | # 251 | # Should you disagree, it is not difficult to duplicate this function with 252 | # more aggressive coercion to suit your own purposes. 253 | 254 | def bunchify(x): 255 | """ Recursively transforms a dictionary into a Bunch via copy. 256 | 257 | >>> b = bunchify({'urmom': {'sez': {'what': 'what'}}}) 258 | >>> b.urmom.sez.what 259 | 'what' 260 | 261 | bunchify can handle intermediary dicts, lists and tuples (as well as 262 | their subclasses), but ymmv on custom datatypes. 263 | 264 | >>> b = bunchify({ 'lol': ('cats', {'hah':'i win again'}), 'hello': [{'french':'salut', 'german':'hallo'}] }) 265 | >>> b.hello[0].french 266 | 'salut' 267 | >>> b.lol[1].hah 268 | 'i win again' 269 | 270 | nb. As dicts are not hashable, they cannot be nested in sets/frozensets. 271 | """ 272 | if isinstance(x, dict): 273 | return Bunch( (k, bunchify(v)) for k,v in x.iteritems() ) 274 | elif isinstance(x, (list, tuple)): 275 | return type(x)( bunchify(v) for v in x ) 276 | else: 277 | return x 278 | 279 | def unbunchify(x): 280 | """ Recursively converts a Bunch into a dictionary. 281 | 282 | >>> b = Bunch(foo=Bunch(lol=True), hello=42, ponies='are pretty!') 283 | >>> unbunchify(b) 284 | {'ponies': 'are pretty!', 'foo': {'lol': True}, 'hello': 42} 285 | 286 | unbunchify will handle intermediary dicts, lists and tuples (as well as 287 | their subclasses), but ymmv on custom datatypes. 288 | 289 | >>> b = Bunch(foo=['bar', Bunch(lol=True)], hello=42, ponies=('are pretty!', Bunch(lies='are trouble!'))) 290 | >>> unbunchify(b) 291 | {'ponies': ('are pretty!', {'lies': 'are trouble!'}), 'foo': ['bar', {'lol': True}], 'hello': 42} 292 | 293 | nb. As dicts are not hashable, they cannot be nested in sets/frozensets. 294 | """ 295 | if isinstance(x, dict): 296 | return dict( (k, unbunchify(v)) for k,v in x.iteritems() ) 297 | elif isinstance(x, (list, tuple)): 298 | return type(x)( unbunchify(v) for v in x ) 299 | else: 300 | return x 301 | 302 | 303 | if __name__ == "__main__": 304 | import doctest 305 | doctest.testmod() 306 | -------------------------------------------------------------------------------- /kee/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | KivEnt Server 4 | 5 | 6 | 7 |

KivEntEd Levels

8 |
9 | Sort by: 10 | 19 | 20 | Per Page: 21 | Start at page: 22 | 23 | 24 | Reverse sort
25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | 236 | 237 | 238 | -------------------------------------------------------------------------------- /passenger_wsgi.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import time 4 | 5 | if 'alexpb' not in os.getcwd(): 6 | localServer = True 7 | else: 8 | localServer = False 9 | #if localServer == False: 10 | # INTERP = "/home/alexpb/bin/python2.6" 11 | # if sys.executable != INTERP: os.execl(INTERP, INTERP, *sys.argv) 12 | #sys.path.append(os.getcwd()) 13 | 14 | import jsonBunch 15 | 16 | import web, os, time, re, cgi, urllib, urllib2, json, string, zipfile, urlparse 17 | import datetime 18 | 19 | 20 | def log(*args): 21 | web.debug(args) 22 | 23 | 24 | db = None 25 | 26 | 27 | if not os.path.exists("ppsshots"):os.makedirs("ppsshots") 28 | if not os.path.exists("crashs"):os.makedirs("crashs") 29 | 30 | def loadDB(): 31 | dbname = "db.json" 32 | if not os.path.isfile(dbname): 33 | output = open(dbname, 'w') 34 | output.write("{}") 35 | output.close() 36 | t = open('db.json', 'r').read() 37 | f = json.loads(t) 38 | globals()['db'] = jsonBunch.bunchify(f) 39 | jsonBunch.superself = globals()['db'] 40 | jsonBunch.printer = web.debug 41 | 42 | # log('database reloaded with:') 43 | #log(globals()['db']) 44 | #print('test') 45 | #db = Shove('sqlite:///test.db') 46 | loadDB() 47 | 48 | urls = ( 49 | '/', 'home', 50 | '/uploadLevel', 'uploadLevel', 51 | '/uploadCrash', 'uploadCrash', 52 | '/crashLogs', 'crashLogs', 53 | '/dlCrashLog', 'dlCrashLog', 54 | '/downloadLevel', 'downloadLevel', 55 | '/queryLevels', 'queryLevels', 56 | '/listLevels', 'listLevels', 57 | '/createUser', 'createUser', 58 | '/listLevel', 'listLevel', 59 | '/downloadSS', 'downloadSS', 60 | '/rateLevel', 'rateLevel', 61 | '/(js|css|images|kee|ppsshots|crashs)/(.*)', 'static' 62 | ) 63 | 64 | class static: 65 | def GET(self, media, file): 66 | try: 67 | f = open(media+'/'+file, 'r') 68 | return f.read() 69 | except: 70 | print "Unexpected error:", sys.exc_info()[0] 71 | print "looking for file: " + media+'/'+file 72 | return media+'/'+file # you can send an 404 error here if you want 73 | 74 | def fail(input=None): 75 | return json.dumps({"result":"fail", "data":input}) 76 | def OK(input=None): 77 | return json.dumps({"result":"OK", "data":input}) 78 | 79 | def compareScores(b, a): 80 | return int(a[1]) - int(b[1]) 81 | 82 | 83 | class listLevel: 84 | def GET(self): 85 | i = web.input() 86 | if "levelid" in i: 87 | return OK(db.ppLevels[i.levelid]) 88 | else: 89 | return fail("level not found") 90 | 91 | class crashLogs: 92 | def GET(self): 93 | web.header("Content-Type", "text/html; charset=utf-8") 94 | files = os.listdir('crashs') 95 | #result = "".join(''+f+'
' for f in files) 96 | result = "".join(''+f+'
' for f in files) 97 | return result 98 | 99 | class dlCrashLog: 100 | def GET(self): 101 | #image/png 102 | web.header("Content-Type", "text/plain") 103 | i = web.input() 104 | namepath = "crashs/" + i.fullname 105 | return open(namepath, 'r').read() 106 | class rateLevel: 107 | def GET(self): 108 | return OK(db.ppLevels) 109 | 110 | '''class listScores: 111 | def GET(self): 112 | 113 | web.header("Content-Type", "text/html; charset=utf-8") 114 | 115 | #content.Add(H1('TEST')) 116 | i = web.input() 117 | print "db.scores =", db.scores 118 | if i.game not in db.scores: 119 | return ('Game Scores Not Found') 120 | #adding score 121 | if 'name' in i and 'score' in i and 'code' in i: 122 | db.scores[i.game].scores.append([i.name, int(i.score)]) 123 | print str(db.scores[i.game].scores) 124 | db.scores[i.game].scores.sort(compareScores) 125 | db.sync() 126 | 127 | #print scores 128 | result = 'High scores for ' + db.scores[i.game].name + "\n" 129 | for score in db.scores[i.game].scores: 130 | result += score[0] + ": " + str(score[1]) + "\n" 131 | return (result)''' 132 | 133 | class queryLevels: 134 | def POST(self): 135 | web.header("Content-Type", "text/html; charset=utf-8") 136 | i = web.input() 137 | print i 138 | result = [] 139 | if "ppLevels" not in db:return fail("no levels in database!") 140 | for levelid in db.ppLevels: 141 | level = db.ppLevels[levelid] 142 | result.append(level) 143 | sortKey = "dateAdded" 144 | if "sortKey" in i:sortKey = i.sortKey 145 | print sortKey 146 | from operator import itemgetter 147 | print result 148 | #result = sorted(result, key=itemgetter(sortKey)) 149 | #result = sorted(result, key=lambda k: k[sortKey]) 150 | result.sort(key=itemgetter(sortKey)) 151 | #result.sort(sortmethod) 152 | print result 153 | cursor = 0 154 | limit = 20 155 | if "cursor" in i:cursor = int(i.cursor) 156 | if "limit" in i:limit = int(i.limit) 157 | 158 | if "reverse" in i:result.reverse() 159 | print cursor, cursor+limit 160 | 161 | result = result[cursor:cursor+limit] 162 | 163 | print "returning: ", result 164 | return OK(result) 165 | 166 | 167 | def pythonicVarName(field): 168 | firstLetter = True 169 | for word in field.split(' '): 170 | if firstLetter == False: 171 | wordCapped = str(word[0]).upper() + word[1:] 172 | id = id + wordCapped 173 | else: 174 | id = word.lower() 175 | firstLetter = False 176 | for i in string.punctuation: 177 | if i in id: 178 | id = id.replace(i, "") 179 | return id 180 | 181 | def authUser(i): 182 | if "users" not in db: return "No user accounts! create one!" 183 | if i.author not in db.users: 184 | return "User " + i.author + " not Found! Create an account." 185 | user = db.users[i.author] 186 | print "passes:", user.passHash, i.passHash 187 | if user.passHash != i.passHash: 188 | return "Invalid Password!" 189 | return True 190 | 191 | class uploadCrash: 192 | def POST(self): 193 | i = web.input() 194 | print "POST uploading crash" 195 | 196 | now = str(datetime.datetime.now()) 197 | 198 | namepath = "crashs/" + now+"--" + str(i.version) 199 | output = open(namepath, 'wb') 200 | output.write(i.crashData) 201 | output.close() 202 | 203 | return OK("Success!") 204 | 205 | class uploadLevel: 206 | def POST(self): 207 | i = web.input() 208 | print "POST uploading level" 209 | userResult = authUser(i) 210 | print userResult 211 | if userResult != True: return fail(userResult) 212 | 213 | fullname = pythonicVarName(i.author + i.name) 214 | 215 | namepath = "pplevels/" + fullname 216 | output = open(namepath, 'wb') 217 | output.write(i.levelData) 218 | output.close() 219 | 220 | import base64 221 | ssdata = base64.b64decode(i.sshot) 222 | namepath = "ppsshots/" + fullname+".png" 223 | output = open(namepath, 'wb') 224 | output.write(ssdata) 225 | output.close() 226 | 227 | newLevel = {} 228 | isNew = True 229 | if "ppLevels" not in db: 230 | db.ppLevels = {} 231 | try: 232 | newLevel = db.ppLevels[fullname] 233 | isNew = False 234 | except KeyError: 235 | pass 236 | newLevel['name'] = i.name 237 | newLevel['author'] = i.author 238 | newLevel['filename'] = fullname 239 | now = str(datetime.datetime.now()) 240 | nowStamp = time.time() 241 | if (isNew): 242 | newLevel['rating'] = 2.5 243 | newLevel['ratingCount'] = 0 244 | newLevel['dateAdded'] = nowStamp 245 | newLevel['downloads'] = 0 246 | newLevel['dateModified'] = nowStamp 247 | newLevel['description'] = "description" 248 | newLevel['screenshot'] = "none" 249 | db.ppLevels[fullname] = newLevel 250 | return OK("Success!") 251 | 252 | 253 | class listLevels: 254 | def GET(self): 255 | return OK(json.dumps(db.ppLevels)) 256 | 257 | def POST(self): 258 | reval = json.dumps(db.ppLevels) 259 | return OK(retval) 260 | '''levelList = [] 261 | for lI in db.ppLevels: 262 | level = db.ppLevels[lI] 263 | levelList.append([level.name, level.filename]) 264 | return json.dumps(levelList)''' 265 | 266 | 267 | class downloadSS: 268 | def GET(self): 269 | #image/png 270 | web.header("Content-Type", "image/png") 271 | i = web.input() 272 | namepath = "ppsshots/" + i.fullname 273 | return open(namepath, 'r').read() 274 | 275 | class downloadLevel: 276 | def POST(self): 277 | i = web.input() 278 | print i 279 | if "fullname" in i: 280 | fullname = i.fullname 281 | else: 282 | #userResult = authUser(i) 283 | #print userResult 284 | #if userResult != True: return fail(userResult) 285 | 286 | fullname = pythonicVarName(i.author + i.name) 287 | if fullname in db.ppLevels: 288 | db.ppLevels[fullname].downloads+=1 289 | namepath = "pplevels/" + fullname 290 | return OK(open(namepath, 'r').read()) 291 | else: 292 | return fail("level not found") 293 | 294 | def makeUser(name,passHash): 295 | user = {} 296 | user['name'] = name 297 | user['passHash'] = passHash 298 | db.users[name] = user 299 | 300 | 301 | class createUser: 302 | def POST(self): 303 | i = web.input() 304 | name = i.userName 305 | passHash = i.passHash 306 | if "users" not in db: 307 | db.users = {} 308 | if name in db.users: 309 | user = db.users[name] 310 | if user.passHash == i.passHash: 311 | return OK("Password Matches - You can upload!") 312 | return fail("User Exists with different password.") 313 | if "creating" in i and i.creating == "false": 314 | return fail("Account not found") 315 | makeUser(name,passHash) 316 | return OK("Account " + name + " created!") 317 | 318 | 319 | class home: 320 | def GET(self): 321 | web.header("Content-Type", "text/html; charset=utf-8") 322 | #content = contentBaseDiv() 323 | #content.add("test") 324 | #return templateOrMinimal(content) 325 | return open("kee/index.html", 'r').read() 326 | #return OK("basic Functions working on kivented server") 327 | 328 | #return db.scores 329 | def POST(self): 330 | i = web.input() 331 | print i 332 | return OK() 333 | 334 | #from paste.exceptions.errormiddleware import ErrorMiddleware 335 | if localServer == False: 336 | app = web.application(urls, globals(), autoreload=False) 337 | application = app.wsgifunc() 338 | #def application(environ, start_response): 339 | #write = start_response('200 OK', [('Content-type', 'text/plain')]) 340 | #return ["Hello then, world!"] 341 | #return applicationc.wsgi(environ,start_response) 342 | else: 343 | if __name__ == "__main__": 344 | app = web.application(urls, globals(), autoreload=True) 345 | app.run() 346 | -------------------------------------------------------------------------------- /web/httpserver.py: -------------------------------------------------------------------------------- 1 | __all__ = ["runsimple"] 2 | 3 | import sys, os 4 | from SimpleHTTPServer import SimpleHTTPRequestHandler 5 | 6 | import webapi as web 7 | import net 8 | import utils 9 | 10 | def runbasic(func, server_address=("0.0.0.0", 8080)): 11 | """ 12 | Runs a simple HTTP server hosting WSGI app `func`. The directory `static/` 13 | is hosted statically. 14 | 15 | Based on [WsgiServer][ws] from [Colin Stewart][cs]. 16 | 17 | [ws]: http://www.owlfish.com/software/wsgiutils/documentation/wsgi-server-api.html 18 | [cs]: http://www.owlfish.com/ 19 | """ 20 | # Copyright (c) 2004 Colin Stewart (http://www.owlfish.com/) 21 | # Modified somewhat for simplicity 22 | # Used under the modified BSD license: 23 | # http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 24 | 25 | import SimpleHTTPServer, SocketServer, BaseHTTPServer, urlparse 26 | import socket, errno 27 | import traceback 28 | 29 | class WSGIHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): 30 | def run_wsgi_app(self): 31 | protocol, host, path, parameters, query, fragment = \ 32 | urlparse.urlparse('http://dummyhost%s' % self.path) 33 | 34 | # we only use path, query 35 | env = {'wsgi.version': (1, 0) 36 | ,'wsgi.url_scheme': 'http' 37 | ,'wsgi.input': self.rfile 38 | ,'wsgi.errors': sys.stderr 39 | ,'wsgi.multithread': 1 40 | ,'wsgi.multiprocess': 0 41 | ,'wsgi.run_once': 0 42 | ,'REQUEST_METHOD': self.command 43 | ,'REQUEST_URI': self.path 44 | ,'PATH_INFO': path 45 | ,'QUERY_STRING': query 46 | ,'CONTENT_TYPE': self.headers.get('Content-Type', '') 47 | ,'CONTENT_LENGTH': self.headers.get('Content-Length', '') 48 | ,'REMOTE_ADDR': self.client_address[0] 49 | ,'SERVER_NAME': self.server.server_address[0] 50 | ,'SERVER_PORT': str(self.server.server_address[1]) 51 | ,'SERVER_PROTOCOL': self.request_version 52 | } 53 | 54 | for http_header, http_value in self.headers.items(): 55 | env ['HTTP_%s' % http_header.replace('-', '_').upper()] = \ 56 | http_value 57 | 58 | # Setup the state 59 | self.wsgi_sent_headers = 0 60 | self.wsgi_headers = [] 61 | 62 | try: 63 | # We have there environment, now invoke the application 64 | result = self.server.app(env, self.wsgi_start_response) 65 | try: 66 | try: 67 | for data in result: 68 | if data: 69 | self.wsgi_write_data(data) 70 | finally: 71 | if hasattr(result, 'close'): 72 | result.close() 73 | except socket.error, socket_err: 74 | # Catch common network errors and suppress them 75 | if (socket_err.args[0] in \ 76 | (errno.ECONNABORTED, errno.EPIPE)): 77 | return 78 | except socket.timeout, socket_timeout: 79 | return 80 | except: 81 | print >> web.debug, traceback.format_exc(), 82 | 83 | if (not self.wsgi_sent_headers): 84 | # We must write out something! 85 | self.wsgi_write_data(" ") 86 | return 87 | 88 | do_POST = run_wsgi_app 89 | do_PUT = run_wsgi_app 90 | do_DELETE = run_wsgi_app 91 | 92 | def do_GET(self): 93 | if self.path.startswith('/static/'): 94 | SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self) 95 | else: 96 | self.run_wsgi_app() 97 | 98 | def wsgi_start_response(self, response_status, response_headers, 99 | exc_info=None): 100 | if (self.wsgi_sent_headers): 101 | raise Exception \ 102 | ("Headers already sent and start_response called again!") 103 | # Should really take a copy to avoid changes in the application.... 104 | self.wsgi_headers = (response_status, response_headers) 105 | return self.wsgi_write_data 106 | 107 | def wsgi_write_data(self, data): 108 | if (not self.wsgi_sent_headers): 109 | status, headers = self.wsgi_headers 110 | # Need to send header prior to data 111 | status_code = status[:status.find(' ')] 112 | status_msg = status[status.find(' ') + 1:] 113 | self.send_response(int(status_code), status_msg) 114 | for header, value in headers: 115 | self.send_header(header, value) 116 | self.end_headers() 117 | self.wsgi_sent_headers = 1 118 | # Send the data 119 | self.wfile.write(data) 120 | 121 | class WSGIServer(SocketServer.ThreadingMixIn, BaseHTTPServer.HTTPServer): 122 | def __init__(self, func, server_address): 123 | BaseHTTPServer.HTTPServer.__init__(self, 124 | server_address, 125 | WSGIHandler) 126 | self.app = func 127 | self.serverShuttingDown = 0 128 | 129 | print "http://%s:%d/" % server_address 130 | WSGIServer(func, server_address).serve_forever() 131 | 132 | def runsimple(func, server_address=("0.0.0.0", 8080)): 133 | """ 134 | Runs [CherryPy][cp] WSGI server hosting WSGI app `func`. 135 | The directory `static/` is hosted statically. 136 | 137 | [cp]: http://www.cherrypy.org 138 | """ 139 | func = StaticMiddleware(func) 140 | func = LogMiddleware(func) 141 | 142 | server = WSGIServer(server_address, func) 143 | 144 | print "http://%s:%d/" % server_address 145 | try: 146 | server.start() 147 | except KeyboardInterrupt: 148 | server.stop() 149 | 150 | def WSGIServer(server_address, wsgi_app): 151 | """Creates CherryPy WSGI server listening at `server_address` to serve `wsgi_app`. 152 | This function can be overwritten to customize the webserver or use a different webserver. 153 | """ 154 | from wsgiserver import CherryPyWSGIServer 155 | return CherryPyWSGIServer(server_address, wsgi_app, server_name="localhost") 156 | 157 | class StaticApp(SimpleHTTPRequestHandler): 158 | """WSGI application for serving static files.""" 159 | def __init__(self, environ, start_response): 160 | self.headers = [] 161 | self.environ = environ 162 | self.start_response = start_response 163 | 164 | def send_response(self, status, msg=""): 165 | self.status = str(status) + " " + msg 166 | 167 | def send_header(self, name, value): 168 | self.headers.append((name, value)) 169 | 170 | def end_headers(self): 171 | pass 172 | 173 | def log_message(*a): pass 174 | 175 | def __iter__(self): 176 | environ = self.environ 177 | 178 | self.path = environ.get('PATH_INFO', '') 179 | self.client_address = environ.get('REMOTE_ADDR','-'), \ 180 | environ.get('REMOTE_PORT','-') 181 | self.command = environ.get('REQUEST_METHOD', '-') 182 | 183 | from cStringIO import StringIO 184 | self.wfile = StringIO() # for capturing error 185 | 186 | f = self.send_head() 187 | self.start_response(self.status, self.headers) 188 | 189 | if f: 190 | block_size = 16 * 1024 191 | while True: 192 | buf = f.read(block_size) 193 | if not buf: 194 | break 195 | yield buf 196 | f.close() 197 | else: 198 | value = self.wfile.getvalue() 199 | yield value 200 | 201 | class StaticMiddleware: 202 | """WSGI middleware for serving static files.""" 203 | def __init__(self, app, prefix='/static/'): 204 | self.app = app 205 | self.prefix = prefix 206 | 207 | def __call__(self, environ, start_response): 208 | path = environ.get('PATH_INFO', '') 209 | if path.startswith(self.prefix): 210 | return StaticApp(environ, start_response) 211 | else: 212 | return self.app(environ, start_response) 213 | 214 | class LogMiddleware: 215 | """WSGI middleware for logging the status.""" 216 | def __init__(self, app): 217 | self.app = app 218 | self.format = '%s - - [%s] "%s %s %s" - %s' 219 | 220 | from BaseHTTPServer import BaseHTTPRequestHandler 221 | import StringIO 222 | f = StringIO.StringIO() 223 | 224 | class FakeSocket: 225 | def makefile(self, *a): 226 | return f 227 | 228 | # take log_date_time_string method from BaseHTTPRequestHandler 229 | self.log_date_time_string = BaseHTTPRequestHandler(FakeSocket(), None, None).log_date_time_string 230 | 231 | def __call__(self, environ, start_response): 232 | def xstart_response(status, response_headers, *args): 233 | out = start_response(status, response_headers, *args) 234 | self.log(status, environ) 235 | return out 236 | 237 | return self.app(environ, xstart_response) 238 | 239 | def log(self, status, environ): 240 | outfile = environ.get('wsgi.errors', web.debug) 241 | req = environ.get('PATH_INFO', '_') 242 | protocol = environ.get('ACTUAL_SERVER_PROTOCOL', '-') 243 | method = environ.get('REQUEST_METHOD', '-') 244 | host = "%s:%s" % (environ.get('REMOTE_ADDR','-'), 245 | environ.get('REMOTE_PORT','-')) 246 | 247 | time = self.log_date_time_string() 248 | 249 | msg = self.format % (host, time, protocol, method, req, status) 250 | print >> outfile, utils.safestr(msg) 251 | -------------------------------------------------------------------------------- /web/session.py: -------------------------------------------------------------------------------- 1 | """ 2 | Session Management 3 | (from web.py) 4 | """ 5 | 6 | import os, time, datetime, random, base64 7 | try: 8 | import cPickle as pickle 9 | except ImportError: 10 | import pickle 11 | try: 12 | import hashlib 13 | sha1 = hashlib.sha1 14 | except ImportError: 15 | import sha 16 | sha1 = sha.new 17 | 18 | import utils 19 | import webapi as web 20 | 21 | __all__ = [ 22 | 'Session', 'SessionExpired', 23 | 'Store', 'DiskStore', 'DBStore', 24 | ] 25 | 26 | web.config.session_parameters = utils.storage({ 27 | 'cookie_name': 'webpy_session_id', 28 | 'cookie_domain': None, 29 | 'timeout': 86400, #24 * 60 * 60, # 24 hours in seconds 30 | 'ignore_expiry': True, 31 | 'ignore_change_ip': True, 32 | 'secret_key': 'fLjUfxqXtfNoIldA0A0J', 33 | 'expired_message': 'Session expired', 34 | }) 35 | 36 | class SessionExpired(web.HTTPError): 37 | def __init__(self, message): 38 | web.HTTPError.__init__(self, '200 OK', {}, data=message) 39 | 40 | class Session(utils.ThreadedDict): 41 | """Session management for web.py 42 | """ 43 | 44 | def __init__(self, app, store, initializer=None): 45 | self.__dict__['store'] = store 46 | self.__dict__['_initializer'] = initializer 47 | self.__dict__['_last_cleanup_time'] = 0 48 | self.__dict__['_config'] = utils.storage(web.config.session_parameters) 49 | 50 | if app: 51 | app.add_processor(self._processor) 52 | 53 | def _processor(self, handler): 54 | """Application processor to setup session for every request""" 55 | self._cleanup() 56 | self._load() 57 | 58 | try: 59 | return handler() 60 | finally: 61 | self._save() 62 | 63 | def _load(self): 64 | """Load the session from the store, by the id from cookie""" 65 | cookie_name = self._config.cookie_name 66 | cookie_domain = self._config.cookie_domain 67 | self.session_id = web.cookies().get(cookie_name) 68 | 69 | # protection against session_id tampering 70 | if self.session_id and not self._valid_session_id(self.session_id): 71 | self.session_id = None 72 | 73 | self._check_expiry() 74 | if self.session_id: 75 | d = self.store[self.session_id] 76 | self.update(d) 77 | self._validate_ip() 78 | 79 | if not self.session_id: 80 | self.session_id = self._generate_session_id() 81 | 82 | if self._initializer: 83 | if isinstance(self._initializer, dict): 84 | self.update(self._initializer) 85 | elif hasattr(self._initializer, '__call__'): 86 | self._initializer() 87 | 88 | self.ip = web.ctx.ip 89 | 90 | def _check_expiry(self): 91 | # check for expiry 92 | if self.session_id and self.session_id not in self.store: 93 | if self._config.ignore_expiry: 94 | self.session_id = None 95 | else: 96 | return self.expired() 97 | 98 | def _validate_ip(self): 99 | # check for change of IP 100 | if self.session_id and self.get('ip', None) != web.ctx.ip: 101 | if not self._config.ignore_change_ip: 102 | return self.expired() 103 | 104 | def _save(self): 105 | cookie_name = self._config.cookie_name 106 | cookie_domain = self._config.cookie_domain 107 | if not self.get('_killed'): 108 | web.setcookie(cookie_name, self.session_id, domain=cookie_domain) 109 | self.store[self.session_id] = dict(self) 110 | else: 111 | web.setcookie(cookie_name, self.session_id, expires=-1, domain=cookie_domain) 112 | 113 | def _generate_session_id(self): 114 | """Generate a random id for session""" 115 | 116 | while True: 117 | rand = os.urandom(16) 118 | now = time.time() 119 | secret_key = self._config.secret_key 120 | session_id = sha1("%s%s%s%s" %(rand, now, utils.safestr(web.ctx.ip), secret_key)) 121 | session_id = session_id.hexdigest() 122 | if session_id not in self.store: 123 | break 124 | return session_id 125 | 126 | def _valid_session_id(self, session_id): 127 | rx = utils.re_compile('^[0-9a-fA-F]+$') 128 | return rx.match(session_id) 129 | 130 | def _cleanup(self): 131 | """Cleanup the stored sessions""" 132 | current_time = time.time() 133 | timeout = self._config.timeout 134 | if current_time - self._last_cleanup_time > timeout: 135 | self.store.cleanup(timeout) 136 | self.__dict__['_last_cleanup_time'] = current_time 137 | 138 | def expired(self): 139 | """Called when an expired session is atime""" 140 | self._killed = True 141 | self._save() 142 | raise SessionExpired(self._config.expired_message) 143 | 144 | def kill(self): 145 | """Kill the session, make it no longer available""" 146 | del self.store[self.session_id] 147 | self._killed = True 148 | 149 | class Store: 150 | """Base class for session stores""" 151 | 152 | def __contains__(self, key): 153 | raise NotImplementedError 154 | 155 | def __getitem__(self, key): 156 | raise NotImplementedError 157 | 158 | def __setitem__(self, key, value): 159 | raise NotImplementedError 160 | 161 | def cleanup(self, timeout): 162 | """removes all the expired sessions""" 163 | raise NotImplementedError 164 | 165 | def encode(self, session_dict): 166 | """encodes session dict as a string""" 167 | pickled = pickle.dumps(session_dict) 168 | return base64.encodestring(pickled) 169 | 170 | def decode(self, session_data): 171 | """decodes the data to get back the session dict """ 172 | pickled = base64.decodestring(session_data) 173 | return pickle.loads(pickled) 174 | 175 | class DiskStore(Store): 176 | """ 177 | Store for saving a session on disk. 178 | 179 | >>> import tempfile 180 | >>> root = tempfile.mkdtemp() 181 | >>> s = DiskStore(root) 182 | >>> s['a'] = 'foo' 183 | >>> s['a'] 184 | 'foo' 185 | >>> time.sleep(0.01) 186 | >>> s.cleanup(0.01) 187 | >>> s['a'] 188 | Traceback (most recent call last): 189 | ... 190 | KeyError: 'a' 191 | """ 192 | def __init__(self, root): 193 | # if the storage root doesn't exists, create it. 194 | if not os.path.exists(root): 195 | os.mkdir(root) 196 | self.root = root 197 | 198 | def _get_path(self, key): 199 | if os.path.sep in key: 200 | raise ValueError, "Bad key: %s" % repr(key) 201 | return os.path.join(self.root, key) 202 | 203 | def __contains__(self, key): 204 | path = self._get_path(key) 205 | return os.path.exists(path) 206 | 207 | def __getitem__(self, key): 208 | path = self._get_path(key) 209 | if os.path.exists(path): 210 | pickled = open(path).read() 211 | return self.decode(pickled) 212 | else: 213 | raise KeyError, key 214 | 215 | def __setitem__(self, key, value): 216 | path = self._get_path(key) 217 | pickled = self.encode(value) 218 | try: 219 | f = open(path, 'w') 220 | try: 221 | f.write(pickled) 222 | finally: 223 | f.close() 224 | except IOError: 225 | pass 226 | 227 | def __delitem__(self, key): 228 | path = self._get_path(key) 229 | if os.path.exists(path): 230 | os.remove(path) 231 | 232 | def cleanup(self, timeout): 233 | now = time.time() 234 | for f in os.listdir(self.root): 235 | path = self._get_path(f) 236 | atime = os.stat(path).st_atime 237 | if now - atime > timeout : 238 | os.remove(path) 239 | 240 | class DBStore(Store): 241 | """Store for saving a session in database 242 | Needs a table with the following columns: 243 | 244 | session_id CHAR(128) UNIQUE NOT NULL, 245 | atime DATETIME NOT NULL default current_timestamp, 246 | data TEXT 247 | """ 248 | def __init__(self, db, table_name): 249 | self.db = db 250 | self.table = table_name 251 | 252 | def __contains__(self, key): 253 | data = self.db.select(self.table, where="session_id=$key", vars=locals()) 254 | return bool(list(data)) 255 | 256 | def __getitem__(self, key): 257 | now = datetime.datetime.now() 258 | try: 259 | s = self.db.select(self.table, where="session_id=$key", vars=locals())[0] 260 | self.db.update(self.table, where="session_id=$key", atime=now, vars=locals()) 261 | except IndexError: 262 | raise KeyError 263 | else: 264 | return self.decode(s.data) 265 | 266 | def __setitem__(self, key, value): 267 | pickled = self.encode(value) 268 | now = datetime.datetime.now() 269 | if key in self: 270 | self.db.update(self.table, where="session_id=$key", data=pickled, vars=locals()) 271 | else: 272 | self.db.insert(self.table, False, session_id=key, data=pickled ) 273 | 274 | def __delitem__(self, key): 275 | self.db.delete(self.table, where="session_id=$key", vars=locals()) 276 | 277 | def cleanup(self, timeout): 278 | timeout = datetime.timedelta(timeout/(24.0*60*60)) #timedelta takes numdays as arg 279 | last_allowed_time = datetime.datetime.now() - timeout 280 | self.db.delete(self.table, where="$last_allowed_time > atime", vars=locals()) 281 | 282 | class ShelfStore: 283 | """Store for saving session using `shelve` module. 284 | 285 | import shelve 286 | store = ShelfStore(shelve.open('session.shelf')) 287 | 288 | XXX: is shelve thread-safe? 289 | """ 290 | def __init__(self, shelf): 291 | self.shelf = shelf 292 | 293 | def __contains__(self, key): 294 | return key in self.shelf 295 | 296 | def __getitem__(self, key): 297 | atime, v = self.shelf[key] 298 | self[key] = v # update atime 299 | return v 300 | 301 | def __setitem__(self, key, value): 302 | self.shelf[key] = time.time(), value 303 | 304 | def __delitem__(self, key): 305 | try: 306 | del self.shelf[key] 307 | except KeyError: 308 | pass 309 | 310 | def cleanup(self, timeout): 311 | now = time.time() 312 | for k in self.shelf.keys(): 313 | atime, v = self.shelf[k] 314 | if now - atime > timeout : 315 | del self[k] 316 | 317 | if __name__ == '__main__' : 318 | import doctest 319 | doctest.testmod() 320 | -------------------------------------------------------------------------------- /web/form.py: -------------------------------------------------------------------------------- 1 | """ 2 | HTML forms 3 | (part of web.py) 4 | """ 5 | 6 | import copy, re 7 | import webapi as web 8 | import utils, net 9 | 10 | def attrget(obj, attr, value=None): 11 | if hasattr(obj, 'has_key') and obj.has_key(attr): return obj[attr] 12 | if hasattr(obj, attr): return getattr(obj, attr) 13 | return value 14 | 15 | class Form: 16 | r""" 17 | HTML form. 18 | 19 | >>> f = Form(Textbox("x")) 20 | >>> f.render() 21 | '\n \n
' 22 | """ 23 | def __init__(self, *inputs, **kw): 24 | self.inputs = inputs 25 | self.valid = True 26 | self.note = None 27 | self.validators = kw.pop('validators', []) 28 | 29 | def __call__(self, x=None): 30 | o = copy.deepcopy(self) 31 | if x: o.validates(x) 32 | return o 33 | 34 | def render(self): 35 | out = '' 36 | out += self.rendernote(self.note) 37 | out += '\n' 38 | 39 | for i in self.inputs: 40 | html = i.pre + i.render() + self.rendernote(i.note) + i.post 41 | if i.is_hidden(): 42 | out += ' \n' % (html) 43 | else: 44 | out += ' \n' % (i.id, net.websafe(i.description), html) 45 | out += "
%s
%s
" 46 | return out 47 | 48 | def render_css(self): 49 | out = [] 50 | out.append(self.rendernote(self.note)) 51 | for i in self.inputs: 52 | if not i.is_hidden(): 53 | out.append('' % (i.id, net.websafe(i.description))) 54 | out.append(i.pre) 55 | out.append(i.render()) 56 | out.append(self.rendernote(i.note)) 57 | out.append(i.post) 58 | out.append('\n') 59 | return ''.join(out) 60 | 61 | def rendernote(self, note): 62 | if note: return '%s' % net.websafe(note) 63 | else: return "" 64 | 65 | def validates(self, source=None, _validate=True, **kw): 66 | source = source or kw or web.input() 67 | out = True 68 | for i in self.inputs: 69 | v = attrget(source, i.name) 70 | if _validate: 71 | out = i.validate(v) and out 72 | else: 73 | i.value = v 74 | if _validate: 75 | out = out and self._validate(source) 76 | self.valid = out 77 | return out 78 | 79 | def _validate(self, value): 80 | self.value = value 81 | for v in self.validators: 82 | if not v.valid(value): 83 | self.note = v.msg 84 | return False 85 | return True 86 | 87 | def fill(self, source=None, **kw): 88 | return self.validates(source, _validate=False, **kw) 89 | 90 | def __getitem__(self, i): 91 | for x in self.inputs: 92 | if x.name == i: return x 93 | raise KeyError, i 94 | 95 | def __getattr__(self, name): 96 | # don't interfere with deepcopy 97 | inputs = self.__dict__.get('inputs') or [] 98 | for x in inputs: 99 | if x.name == name: return x 100 | raise AttributeError, name 101 | 102 | def get(self, i, default=None): 103 | try: 104 | return self[i] 105 | except KeyError: 106 | return default 107 | 108 | def _get_d(self): #@@ should really be form.attr, no? 109 | return utils.storage([(i.name, i.get_value()) for i in self.inputs]) 110 | d = property(_get_d) 111 | 112 | class Input(object): 113 | def __init__(self, name, *validators, **attrs): 114 | self.name = name 115 | self.validators = validators 116 | self.attrs = attrs = AttributeList(attrs) 117 | 118 | self.description = attrs.pop('description', name) 119 | self.value = attrs.pop('value', None) 120 | self.pre = attrs.pop('pre', "") 121 | self.post = attrs.pop('post', "") 122 | self.note = None 123 | 124 | self.id = attrs.setdefault('id', self.get_default_id()) 125 | 126 | if 'class_' in attrs: 127 | attrs['class'] = attrs['class_'] 128 | del attrs['class_'] 129 | 130 | def is_hidden(self): 131 | return False 132 | 133 | def get_type(self): 134 | raise NotImplementedError 135 | 136 | def get_default_id(self): 137 | return self.name 138 | 139 | def validate(self, value): 140 | self.set_value(value) 141 | 142 | for v in self.validators: 143 | if not v.valid(value): 144 | self.note = v.msg 145 | return False 146 | return True 147 | 148 | def set_value(self, value): 149 | self.value = value 150 | 151 | def get_value(self): 152 | return self.value 153 | 154 | def render(self): 155 | attrs = self.attrs.copy() 156 | attrs['type'] = self.get_type() 157 | if self.value: 158 | attrs['value'] = self.value 159 | attrs['name'] = self.name 160 | return '' % attrs 161 | 162 | def rendernote(self, note): 163 | if note: return '%s' % net.websafe(note) 164 | else: return "" 165 | 166 | def addatts(self): 167 | return str(self.attrs) 168 | 169 | class AttributeList(dict): 170 | """List of atributes of input. 171 | 172 | >>> a = AttributeList(type='text', name='x', value=20) 173 | >>> a 174 | 175 | """ 176 | def copy(self): 177 | return AttributeList(self) 178 | 179 | def __str__(self): 180 | return " ".join('%s="%s"' % (k, net.websafe(v)) for k, v in self.items()) 181 | 182 | def __repr__(self): 183 | return '' % repr(str(self)) 184 | 185 | class Textbox(Input): 186 | """Textbox input. 187 | 188 | >>> Textbox(name='foo', value='bar').render() 189 | '' 190 | """ 191 | def get_type(self): 192 | return 'text' 193 | 194 | class Password(Input): 195 | """Password input. 196 | 197 | >>> Password(name='password', value='secret').render() 198 | '' 199 | """ 200 | 201 | def get_type(self): 202 | return 'password' 203 | 204 | class Textarea(Input): 205 | """Textarea input. 206 | 207 | >>> Textarea(name='foo', value='bar').render() 208 | '' 209 | """ 210 | def render(self): 211 | attrs = self.attrs.copy() 212 | attrs['name'] = self.name 213 | value = net.websafe(self.value or '') 214 | return '' % (attrs, value) 215 | 216 | class Dropdown(Input): 217 | r"""Dropdown/select input. 218 | 219 | >>> Dropdown(name='foo', args=['a', 'b', 'c'], value='b').render() 220 | '\n' 221 | >>> Dropdown(name='foo', args=[('a', 'aa'), ('b', 'bb'), ('c', 'cc')], value='b').render() 222 | '\n' 223 | """ 224 | def __init__(self, name, args, *validators, **attrs): 225 | self.args = args 226 | super(Dropdown, self).__init__(name, *validators, **attrs) 227 | 228 | def render(self): 229 | attrs = self.attrs.copy() 230 | attrs['name'] = self.name 231 | 232 | x = '\n' 245 | return x 246 | 247 | class Radio(Input): 248 | def __init__(self, name, args, *validators, **attrs): 249 | self.args = args 250 | super(Radio, self).__init__(name, *validators, **attrs) 251 | 252 | def render(self): 253 | x = '' 254 | for arg in self.args: 255 | if isinstance(arg, (tuple, list)): 256 | value, desc= arg 257 | else: 258 | value, desc = arg, arg 259 | attrs = self.attrs.copy() 260 | attrs['name'] = self.name 261 | attrs['type'] = 'radio' 262 | attrs['value'] = arg 263 | if self.value == arg: 264 | attrs['checked'] = 'checked' 265 | x += ' %s' % (attrs, net.websafe(desc)) 266 | x += '' 267 | return x 268 | 269 | class Checkbox(Input): 270 | """Checkbox input. 271 | 272 | >>> Checkbox('foo', value='bar', checked=True).render() 273 | '' 274 | >>> Checkbox('foo', value='bar').render() 275 | '' 276 | >>> c = Checkbox('foo', value='bar') 277 | >>> c.validate('on') 278 | True 279 | >>> c.render() 280 | '' 281 | """ 282 | def __init__(self, name, *validators, **attrs): 283 | self.checked = attrs.pop('checked', False) 284 | Input.__init__(self, name, *validators, **attrs) 285 | 286 | def get_default_id(self): 287 | value = self.value or "" 288 | return self.name + '_' + value.replace(' ', '_') 289 | 290 | def render(self): 291 | attrs = self.attrs.copy() 292 | attrs['type'] = 'checkbox' 293 | attrs['name'] = self.name 294 | attrs['value'] = self.value 295 | 296 | if self.checked: 297 | attrs['checked'] = 'checked' 298 | return '' % attrs 299 | 300 | def set_value(self, value): 301 | if value: 302 | self.checked = True 303 | 304 | def get_value(self): 305 | return self.checked 306 | 307 | class Button(Input): 308 | """HTML Button. 309 | 310 | >>> Button("save").render() 311 | '' 312 | >>> Button("action", value="save", html="Save Changes").render() 313 | '' 314 | """ 315 | def __init__(self, name, *validators, **attrs): 316 | super(Button, self).__init__(name, *validators, **attrs) 317 | self.description = "" 318 | 319 | def render(self): 320 | attrs = self.attrs.copy() 321 | attrs['name'] = self.name 322 | if self.value is not None: 323 | attrs['value'] = self.value 324 | html = attrs.pop('html', None) or net.websafe(self.name) 325 | return '' % (attrs, html) 326 | 327 | class Hidden(Input): 328 | """Hidden Input. 329 | 330 | >>> Hidden(name='foo', value='bar').render() 331 | '' 332 | """ 333 | def is_hidden(self): 334 | return True 335 | 336 | def get_type(self): 337 | return 'hidden' 338 | 339 | class File(Input): 340 | """File input. 341 | 342 | >>> File(name='f').render() 343 | '' 344 | """ 345 | def get_type(self): 346 | return 'file' 347 | 348 | class Validator: 349 | def __deepcopy__(self, memo): return copy.copy(self) 350 | def __init__(self, msg, test, jstest=None): utils.autoassign(self, locals()) 351 | def valid(self, value): 352 | try: return self.test(value) 353 | except: return False 354 | 355 | notnull = Validator("Required", bool) 356 | 357 | class regexp(Validator): 358 | def __init__(self, rexp, msg): 359 | self.rexp = re.compile(rexp) 360 | self.msg = msg 361 | 362 | def valid(self, value): 363 | return bool(self.rexp.match(value)) 364 | 365 | if __name__ == "__main__": 366 | import doctest 367 | doctest.testmod() 368 | -------------------------------------------------------------------------------- /web/webapi.py: -------------------------------------------------------------------------------- 1 | """ 2 | Web API (wrapper around WSGI) 3 | (from web.py) 4 | """ 5 | 6 | __all__ = [ 7 | "config", 8 | "header", "debug", 9 | "input", "data", 10 | "setcookie", "cookies", 11 | "ctx", 12 | "HTTPError", 13 | 14 | # 200, 201, 202 15 | "OK", "Created", "Accepted", 16 | "ok", "created", "accepted", 17 | 18 | # 301, 302, 303, 304, 407 19 | "Redirect", "Found", "SeeOther", "NotModified", "TempRedirect", 20 | "redirect", "found", "seeother", "notmodified", "tempredirect", 21 | 22 | # 400, 401, 403, 404, 405, 406, 409, 410, 412 23 | "BadRequest", "Unauthorized", "Forbidden", "NoMethod", "NotFound", "NotAcceptable", "Conflict", "Gone", "PreconditionFailed", 24 | "badrequest", "unauthorized", "forbidden", "nomethod", "notfound", "notacceptable", "conflict", "gone", "preconditionfailed", 25 | 26 | # 500 27 | "InternalError", 28 | "internalerror", 29 | ] 30 | 31 | import sys, cgi, Cookie, pprint, urlparse, urllib 32 | from utils import storage, storify, threadeddict, dictadd, intget, utf8 33 | 34 | config = storage() 35 | config.__doc__ = """ 36 | A configuration object for various aspects of web.py. 37 | 38 | `debug` 39 | : when True, enables reloading, disabled template caching and sets internalerror to debugerror. 40 | """ 41 | 42 | class HTTPError(Exception): 43 | def __init__(self, status, headers={}, data=""): 44 | ctx.status = status 45 | for k, v in headers.items(): 46 | header(k, v) 47 | self.data = data 48 | Exception.__init__(self, status) 49 | 50 | def _status_code(status, data=None, classname=None, docstring=None): 51 | if data is None: 52 | data = status.split(" ", 1)[1] 53 | classname = status.split(" ", 1)[1].replace(' ', '') # 304 Not Modified -> NotModified 54 | docstring = docstring or '`%s` status' % status 55 | 56 | def __init__(self, data=data, headers={}): 57 | HTTPError.__init__(self, status, headers, data) 58 | 59 | # trick to create class dynamically with dynamic docstring. 60 | return type(classname, (HTTPError, object), { 61 | '__doc__': docstring, 62 | '__init__': __init__ 63 | }) 64 | 65 | ok = OK = _status_code("200 OK", data="") 66 | created = Created = _status_code("201 Created") 67 | accepted = Accepted = _status_code("202 Accepted") 68 | 69 | class Redirect(HTTPError): 70 | """A `301 Moved Permanently` redirect.""" 71 | def __init__(self, url, status='301 Moved Permanently', absolute=False): 72 | """ 73 | Returns a `status` redirect to the new URL. 74 | `url` is joined with the base URL so that things like 75 | `redirect("about") will work properly. 76 | """ 77 | newloc = urlparse.urljoin(ctx.path, url) 78 | 79 | if newloc.startswith('/'): 80 | if absolute: 81 | home = ctx.realhome 82 | else: 83 | home = ctx.home 84 | newloc = home + newloc 85 | 86 | headers = { 87 | 'Content-Type': 'text/html', 88 | 'Location': newloc 89 | } 90 | HTTPError.__init__(self, status, headers, "") 91 | 92 | redirect = Redirect 93 | 94 | class Found(Redirect): 95 | """A `302 Found` redirect.""" 96 | def __init__(self, url, absolute=False): 97 | Redirect.__init__(self, url, '302 Found', absolute=absolute) 98 | 99 | found = Found 100 | 101 | class SeeOther(Redirect): 102 | """A `303 See Other` redirect.""" 103 | def __init__(self, url, absolute=False): 104 | Redirect.__init__(self, url, '303 See Other', absolute=absolute) 105 | 106 | seeother = SeeOther 107 | 108 | class NotModified(HTTPError): 109 | """A `304 Not Modified` status.""" 110 | def __init__(self): 111 | HTTPError.__init__(self, "304 Not Modified") 112 | 113 | notmodified = NotModified 114 | 115 | class TempRedirect(Redirect): 116 | """A `307 Temporary Redirect` redirect.""" 117 | def __init__(self, url, absolute=False): 118 | Redirect.__init__(self, url, '307 Temporary Redirect', absolute=absolute) 119 | 120 | tempredirect = TempRedirect 121 | 122 | class BadRequest(HTTPError): 123 | """`400 Bad Request` error.""" 124 | message = "bad request" 125 | def __init__(self): 126 | status = "400 Bad Request" 127 | headers = {'Content-Type': 'text/html'} 128 | HTTPError.__init__(self, status, headers, self.message) 129 | 130 | badrequest = BadRequest 131 | 132 | class _NotFound(HTTPError): 133 | """`404 Not Found` error.""" 134 | message = "not found" 135 | def __init__(self, message=None): 136 | status = '404 Not Found' 137 | headers = {'Content-Type': 'text/html'} 138 | HTTPError.__init__(self, status, headers, message or self.message) 139 | 140 | def NotFound(message=None): 141 | """Returns HTTPError with '404 Not Found' error from the active application. 142 | """ 143 | if message: 144 | return _NotFound(message) 145 | elif ctx.get('app_stack'): 146 | return ctx.app_stack[-1].notfound() 147 | else: 148 | return _NotFound() 149 | 150 | notfound = NotFound 151 | 152 | unauthorized = Unauthorized = _status_code("401 Unauthorized") 153 | forbidden = Forbidden = _status_code("403 Forbidden") 154 | notacceptable = NotAcceptable = _status_code("406 Not Acceptable") 155 | conflict = Conflict = _status_code("409 Conflict") 156 | preconditionfailed = PreconditionFailed = _status_code("412 Precondition Failed") 157 | 158 | class NoMethod(HTTPError): 159 | """A `405 Method Not Allowed` error.""" 160 | def __init__(self, cls=None): 161 | status = '405 Method Not Allowed' 162 | headers = {} 163 | headers['Content-Type'] = 'text/html' 164 | 165 | methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'] 166 | if cls: 167 | methods = [method for method in methods if hasattr(cls, method)] 168 | 169 | headers['Allow'] = ', '.join(methods) 170 | data = None 171 | HTTPError.__init__(self, status, headers, data) 172 | 173 | nomethod = NoMethod 174 | 175 | class Gone(HTTPError): 176 | """`410 Gone` error.""" 177 | message = "gone" 178 | def __init__(self): 179 | status = '410 Gone' 180 | headers = {'Content-Type': 'text/html'} 181 | HTTPError.__init__(self, status, headers, self.message) 182 | 183 | gone = Gone 184 | 185 | class _InternalError(HTTPError): 186 | """500 Internal Server Error`.""" 187 | message = "internal server error" 188 | 189 | def __init__(self, message=None): 190 | status = '500 Internal Server Error' 191 | headers = {'Content-Type': 'text/html'} 192 | HTTPError.__init__(self, status, headers, message or self.message) 193 | 194 | def InternalError(message=None): 195 | """Returns HTTPError with '500 internal error' error from the active application. 196 | """ 197 | if message: 198 | return _InternalError(message) 199 | elif ctx.get('app_stack'): 200 | return ctx.app_stack[-1].internalerror() 201 | else: 202 | return _InternalError() 203 | 204 | internalerror = InternalError 205 | 206 | def header(hdr, value, unique=False): 207 | """ 208 | Adds the header `hdr: value` with the response. 209 | 210 | If `unique` is True and a header with that name already exists, 211 | it doesn't add a new one. 212 | """ 213 | hdr, value = utf8(hdr), utf8(value) 214 | # protection against HTTP response splitting attack 215 | if '\n' in hdr or '\r' in hdr or '\n' in value or '\r' in value: 216 | raise ValueError, 'invalid characters in header' 217 | 218 | if unique is True: 219 | for h, v in ctx.headers: 220 | if h.lower() == hdr.lower(): return 221 | 222 | ctx.headers.append((hdr, value)) 223 | 224 | def rawinput(method=None): 225 | """Returns storage object with GET or POST arguments. 226 | """ 227 | method = method or "both" 228 | from cStringIO import StringIO 229 | 230 | def dictify(fs): 231 | # hack to make web.input work with enctype='text/plain. 232 | if fs.list is None: 233 | fs.list = [] 234 | 235 | return dict([(k, fs[k]) for k in fs.keys()]) 236 | 237 | e = ctx.env.copy() 238 | a = b = {} 239 | 240 | if method.lower() in ['both', 'post', 'put']: 241 | if e['REQUEST_METHOD'] in ['POST', 'PUT']: 242 | if e.get('CONTENT_TYPE', '').lower().startswith('multipart/'): 243 | # since wsgi.input is directly passed to cgi.FieldStorage, 244 | # it can not be called multiple times. Saving the FieldStorage 245 | # object in ctx to allow calling web.input multiple times. 246 | a = ctx.get('_fieldstorage') 247 | if not a: 248 | fp = e['wsgi.input'] 249 | a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1) 250 | ctx._fieldstorage = a 251 | else: 252 | fp = StringIO(data()) 253 | a = cgi.FieldStorage(fp=fp, environ=e, keep_blank_values=1) 254 | a = dictify(a) 255 | 256 | if method.lower() in ['both', 'get']: 257 | e['REQUEST_METHOD'] = 'GET' 258 | b = dictify(cgi.FieldStorage(environ=e, keep_blank_values=1)) 259 | 260 | def process_fieldstorage(fs): 261 | if isinstance(fs, list): 262 | return [process_fieldstorage(x) for x in fs] 263 | elif fs.filename is None: 264 | return fs.value 265 | else: 266 | return fs 267 | 268 | return storage([(k, process_fieldstorage(v)) for k, v in dictadd(b, a).items()]) 269 | 270 | def input(*requireds, **defaults): 271 | """ 272 | Returns a `storage` object with the GET and POST arguments. 273 | See `storify` for how `requireds` and `defaults` work. 274 | """ 275 | _method = defaults.pop('_method', 'both') 276 | out = rawinput(_method) 277 | try: 278 | defaults.setdefault('_unicode', True) # force unicode conversion by default. 279 | return storify(out, *requireds, **defaults) 280 | except KeyError: 281 | raise badrequest() 282 | 283 | def data(): 284 | """Returns the data sent with the request.""" 285 | if 'data' not in ctx: 286 | cl = intget(ctx.env.get('CONTENT_LENGTH'), 0) 287 | ctx.data = ctx.env['wsgi.input'].read(cl) 288 | return ctx.data 289 | 290 | def setcookie(name, value, expires="", domain=None, secure=False): 291 | """Sets a cookie.""" 292 | if expires < 0: 293 | expires = -1000000000 294 | kargs = {'expires': expires, 'path':'/'} 295 | if domain: 296 | kargs['domain'] = domain 297 | if secure: 298 | kargs['secure'] = secure 299 | # @@ should we limit cookies to a different path? 300 | cookie = Cookie.SimpleCookie() 301 | cookie[name] = urllib.quote(utf8(value)) 302 | for key, val in kargs.iteritems(): 303 | cookie[name][key] = val 304 | header('Set-Cookie', cookie.items()[0][1].OutputString()) 305 | 306 | def cookies(*requireds, **defaults): 307 | """ 308 | Returns a `storage` object with all the cookies in it. 309 | See `storify` for how `requireds` and `defaults` work. 310 | """ 311 | cookie = Cookie.SimpleCookie() 312 | cookie.load(ctx.env.get('HTTP_COOKIE', '')) 313 | try: 314 | d = storify(cookie, *requireds, **defaults) 315 | for k, v in d.items(): 316 | d[k] = v and urllib.unquote(v) 317 | return d 318 | except KeyError: 319 | badrequest() 320 | raise StopIteration 321 | 322 | def debug(*args): 323 | """ 324 | Prints a prettyprinted version of `args` to stderr. 325 | """ 326 | try: 327 | out = ctx.environ['wsgi.errors'] 328 | except: 329 | out = sys.stderr 330 | for arg in args: 331 | print >> out, pprint.pformat(arg) 332 | return '' 333 | 334 | def _debugwrite(x): 335 | try: 336 | out = ctx.environ['wsgi.errors'] 337 | except: 338 | out = sys.stderr 339 | out.write(x) 340 | debug.write = _debugwrite 341 | 342 | ctx = context = threadeddict() 343 | 344 | ctx.__doc__ = """ 345 | A `storage` object containing various information about the request: 346 | 347 | `environ` (aka `env`) 348 | : A dictionary containing the standard WSGI environment variables. 349 | 350 | `host` 351 | : The domain (`Host` header) requested by the user. 352 | 353 | `home` 354 | : The base path for the application. 355 | 356 | `ip` 357 | : The IP address of the requester. 358 | 359 | `method` 360 | : The HTTP method used. 361 | 362 | `path` 363 | : The path request. 364 | 365 | `query` 366 | : If there are no query arguments, the empty string. Otherwise, a `?` followed 367 | by the query string. 368 | 369 | `fullpath` 370 | : The full path requested, including query arguments (`== path + query`). 371 | 372 | ### Response Data 373 | 374 | `status` (default: "200 OK") 375 | : The status code to be used in the response. 376 | 377 | `headers` 378 | : A list of 2-tuples to be used in the response. 379 | 380 | `output` 381 | : A string to be used as the response. 382 | """ 383 | -------------------------------------------------------------------------------- /web/debugerror.py: -------------------------------------------------------------------------------- 1 | """ 2 | pretty debug errors 3 | (part of web.py) 4 | 5 | portions adapted from Django 6 | Copyright (c) 2005, the Lawrence Journal-World 7 | Used under the modified BSD license: 8 | http://www.xfree86.org/3.3.6/COPYRIGHT2.html#5 9 | """ 10 | 11 | __all__ = ["debugerror", "djangoerror", "emailerrors"] 12 | 13 | import sys, urlparse, pprint, traceback 14 | from net import websafe 15 | from template import Template 16 | from utils import sendmail 17 | import webapi as web 18 | 19 | import os, os.path 20 | whereami = os.path.join(os.getcwd(), __file__) 21 | whereami = os.path.sep.join(whereami.split(os.path.sep)[:-1]) 22 | djangoerror_t = """\ 23 | $def with (exception_type, exception_value, frames) 24 | 25 | 26 | 27 | 28 | 29 | $exception_type at $ctx.path 30 | 79 | 127 | 128 | 129 | 130 | $def dicttable (d, kls='req', id=None): 131 | $ items = d and d.items() or [] 132 | $items.sort() 133 | $:dicttable_items(items, kls, id) 134 | 135 | $def dicttable_items(items, kls='req', id=None): 136 | $if items: 137 | 140 | 141 | $for k, v in items: 142 | 143 | 144 |
VariableValue
$k
$prettify(v)
145 | $else: 146 |

No data.

147 | 148 |
149 |

$exception_type at $ctx.path

150 |

$exception_value

151 | 152 | 153 | 154 | 155 | 156 | 157 |
Python$frames[0].filename in $frames[0].function, line $frames[0].lineno
Web$ctx.method $ctx.home$ctx.path
158 |
159 |
160 |

Traceback (innermost first)

161 |
    162 | $for frame in frames: 163 |
  • 164 | $frame.filename in $frame.function 165 | $if frame.context_line: 166 |
    167 | $if frame.pre_context: 168 |
      169 | $for line in frame.pre_context: 170 |
    1. $line
    2. 171 |
    172 |
    1. $frame.context_line ...
    173 | $if frame.post_context: 174 |
      175 | $for line in frame.post_context: 176 |
    1. $line
    2. 177 |
    178 |
    179 | 180 | $if frame.vars: 181 |
    182 | Local vars 183 | $# $inspect.formatargvalues(*inspect.getargvalues(frame['tb'].tb_frame)) 184 |
    185 | $:dicttable(frame.vars, kls='vars', id=('v' + str(frame.id))) 186 |
  • 187 |
188 |
189 | 190 |
191 | $if ctx.output or ctx.headers: 192 |

Response so far

193 |

HEADERS

194 | $:dicttable_items(ctx.headers) 195 | 196 |

BODY

197 |

198 | $ctx.output 199 |

200 | 201 |

Request information

202 | 203 |

INPUT

204 | $:dicttable(web.input(_unicode=False)) 205 | 206 | 207 | $:dicttable(web.cookies()) 208 | 209 |

META

210 | $ newctx = [(k, v) for (k, v) in ctx.iteritems() if not k.startswith('_') and not isinstance(v, dict)] 211 | $:dicttable(dict(newctx)) 212 | 213 |

ENVIRONMENT

214 | $:dicttable(ctx.env) 215 |
216 | 217 |
218 |

219 | You're seeing this error because you have web.config.debug 220 | set to True. Set that to False if you don't to see this. 221 |

222 |
223 | 224 | 225 | 226 | """ 227 | 228 | djangoerror_r = None 229 | 230 | def djangoerror(): 231 | def _get_lines_from_file(filename, lineno, context_lines): 232 | """ 233 | Returns context_lines before and after lineno from file. 234 | Returns (pre_context_lineno, pre_context, context_line, post_context). 235 | """ 236 | try: 237 | source = open(filename).readlines() 238 | lower_bound = max(0, lineno - context_lines) 239 | upper_bound = lineno + context_lines 240 | 241 | pre_context = \ 242 | [line.strip('\n') for line in source[lower_bound:lineno]] 243 | context_line = source[lineno].strip('\n') 244 | post_context = \ 245 | [line.strip('\n') for line in source[lineno + 1:upper_bound]] 246 | 247 | return lower_bound, pre_context, context_line, post_context 248 | except (OSError, IOError): 249 | return None, [], None, [] 250 | 251 | exception_type, exception_value, tback = sys.exc_info() 252 | frames = [] 253 | while tback is not None: 254 | filename = tback.tb_frame.f_code.co_filename 255 | function = tback.tb_frame.f_code.co_name 256 | lineno = tback.tb_lineno - 1 257 | pre_context_lineno, pre_context, context_line, post_context = \ 258 | _get_lines_from_file(filename, lineno, 7) 259 | frames.append(web.storage({ 260 | 'tback': tback, 261 | 'filename': filename, 262 | 'function': function, 263 | 'lineno': lineno, 264 | 'vars': tback.tb_frame.f_locals, 265 | 'id': id(tback), 266 | 'pre_context': pre_context, 267 | 'context_line': context_line, 268 | 'post_context': post_context, 269 | 'pre_context_lineno': pre_context_lineno, 270 | })) 271 | tback = tback.tb_next 272 | frames.reverse() 273 | urljoin = urlparse.urljoin 274 | def prettify(x): 275 | try: 276 | out = pprint.pformat(x) 277 | except Exception, e: 278 | out = '[could not display: <' + e.__class__.__name__ + \ 279 | ': '+str(e)+'>]' 280 | return out 281 | 282 | global djangoerror_r 283 | if djangoerror_r is None: 284 | djangoerror_r = Template(djangoerror_t, filename=__file__, filter=websafe) 285 | 286 | t = djangoerror_r 287 | globals = {'ctx': web.ctx, 'web':web, 'dict':dict, 'str':str, 'prettify': prettify} 288 | t.t.func_globals.update(globals) 289 | return t(exception_type, exception_value, frames) 290 | 291 | def debugerror(): 292 | """ 293 | A replacement for `internalerror` that presents a nice page with lots 294 | of debug information for the programmer. 295 | 296 | (Based on the beautiful 500 page from [Django](http://djangoproject.com/), 297 | designed by [Wilson Miner](http://wilsonminer.com/).) 298 | """ 299 | return web._InternalError(djangoerror()) 300 | 301 | def emailerrors(to_address, olderror, from_address=None): 302 | """ 303 | Wraps the old `internalerror` handler (pass as `olderror`) to 304 | additionally email all errors to `to_address`, to aid in 305 | debugging production websites. 306 | 307 | Emails contain a normal text traceback as well as an 308 | attachment containing the nice `debugerror` page. 309 | """ 310 | from_address = from_address or to_address 311 | 312 | def emailerrors_internal(): 313 | error = olderror() 314 | tb = sys.exc_info() 315 | error_name = tb[0] 316 | error_value = tb[1] 317 | tb_txt = ''.join(traceback.format_exception(*tb)) 318 | path = web.ctx.path 319 | request = web.ctx.method + ' ' + web.ctx.home + web.ctx.fullpath 320 | text = ("""\ 321 | ------here---- 322 | Content-Type: text/plain 323 | Content-Disposition: inline 324 | 325 | %(request)s 326 | 327 | %(tb_txt)s 328 | 329 | ------here---- 330 | Content-Type: text/html; name="bug.html" 331 | Content-Disposition: attachment; filename="bug.html" 332 | 333 | """ % locals()) + str(djangoerror()) 334 | sendmail( 335 | "your buggy site <%s>" % from_address, 336 | "the bugfixer <%s>" % to_address, 337 | "bug: %(error_name)s: %(error_value)s (%(path)s)" % locals(), 338 | text, 339 | headers={'Content-Type': 'multipart/mixed; boundary="----here----"'}) 340 | return error 341 | 342 | return emailerrors_internal 343 | 344 | if __name__ == "__main__": 345 | urls = ( 346 | '/', 'index' 347 | ) 348 | from application import application 349 | app = application(urls, globals()) 350 | app.internalerror = debugerror 351 | 352 | class index: 353 | def GET(self): 354 | thisdoesnotexist 355 | 356 | app.run() 357 | -------------------------------------------------------------------------------- /web/application.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Web application 4 | (from web.py) 5 | """ 6 | import webapi as web 7 | import webapi, wsgi, utils 8 | import debugerror 9 | from utils import lstrips, safeunicode 10 | import sys 11 | 12 | import urllib 13 | import traceback 14 | import itertools 15 | import os 16 | import re 17 | import types 18 | from exceptions import SystemExit 19 | 20 | try: 21 | import wsgiref.handlers 22 | except ImportError: 23 | pass # don't break people with old Pythons 24 | 25 | __all__ = [ 26 | "application", "auto_application", 27 | "subdir_application", "subdomain_application", 28 | "loadhook", "unloadhook", 29 | "autodelegate" 30 | ] 31 | 32 | class application: 33 | """ 34 | Application to delegate requests based on path. 35 | 36 | >>> urls = ("/hello", "hello") 37 | >>> app = application(urls, globals()) 38 | >>> class hello: 39 | ... def GET(self): return "hello" 40 | >>> 41 | >>> app.request("/hello").data 42 | 'hello' 43 | """ 44 | def __init__(self, mapping=(), fvars={}, autoreload=None): 45 | if autoreload is None: 46 | autoreload = web.config.get('debug', False) 47 | self.mapping = mapping 48 | self.fvars = fvars 49 | self.processors = [] 50 | 51 | self.add_processor(loadhook(self._load)) 52 | self.add_processor(unloadhook(self._unload)) 53 | 54 | if autoreload: 55 | def main_module_name(): 56 | mod = sys.modules['__main__'] 57 | file = getattr(mod, '__file__', None) # make sure this works even from python interpreter 58 | return file and os.path.splitext(os.path.basename(file))[0] 59 | 60 | def modname(fvars): 61 | """find name of the module name from fvars.""" 62 | file, name = fvars.get('__file__'), fvars.get('__name__') 63 | if file is None or name is None: 64 | return None 65 | 66 | if name == '__main__': 67 | # Since the __main__ module can't be reloaded, the module has 68 | # to be imported using its file name. 69 | name = main_module_name() 70 | return name 71 | 72 | mapping_name = utils.dictfind(fvars, mapping) 73 | module_name = modname(fvars) 74 | 75 | def reload_mapping(): 76 | """loadhook to reload mapping and fvars.""" 77 | mod = __import__(module_name) 78 | mapping = getattr(mod, mapping_name, None) 79 | if mapping: 80 | self.fvars = mod.__dict__ 81 | self.mapping = mapping 82 | 83 | self.add_processor(loadhook(Reloader())) 84 | if mapping_name and module_name: 85 | self.add_processor(loadhook(reload_mapping)) 86 | 87 | # load __main__ module usings its filename, so that it can be reloaded. 88 | if main_module_name() and '__main__' in sys.argv: 89 | try: 90 | __import__(main_module_name()) 91 | except ImportError: 92 | pass 93 | 94 | def _load(self): 95 | web.ctx.app_stack.append(self) 96 | 97 | def _unload(self): 98 | web.ctx.app_stack = web.ctx.app_stack[:-1] 99 | 100 | if web.ctx.app_stack: 101 | # this is a sub-application, revert ctx to earlier state. 102 | oldctx = web.ctx.get('_oldctx') 103 | if oldctx: 104 | web.ctx.home = oldctx.home 105 | web.ctx.homepath = oldctx.homepath 106 | web.ctx.path = oldctx.path 107 | web.ctx.fullpath = oldctx.fullpath 108 | 109 | def _cleanup(self): 110 | #@@@ 111 | # Since the CherryPy Webserver uses thread pool, the thread-local state is never cleared. 112 | # This interferes with the other requests. 113 | # clearing the thread-local storage to avoid that. 114 | # see utils.ThreadedDict for details 115 | import threading 116 | t = threading.currentThread() 117 | if hasattr(t, '_d'): 118 | del t._d 119 | 120 | def add_mapping(self, pattern, classname): 121 | self.mapping += (pattern, classname) 122 | 123 | def add_processor(self, processor): 124 | """ 125 | Adds a processor to the application. 126 | 127 | >>> urls = ("/(.*)", "echo") 128 | >>> app = application(urls, globals()) 129 | >>> class echo: 130 | ... def GET(self, name): return name 131 | ... 132 | >>> 133 | >>> def hello(handler): return "hello, " + handler() 134 | ... 135 | >>> app.add_processor(hello) 136 | >>> app.request("/web.py").data 137 | 'hello, web.py' 138 | """ 139 | self.processors.append(processor) 140 | 141 | def request(self, localpart='/', method='GET', data=None, 142 | host="0.0.0.0:8080", headers=None, https=False, **kw): 143 | """Makes request to this application for the specified path and method. 144 | Response will be a storage object with data, status and headers. 145 | 146 | >>> urls = ("/hello", "hello") 147 | >>> app = application(urls, globals()) 148 | >>> class hello: 149 | ... def GET(self): 150 | ... web.header('Content-Type', 'text/plain') 151 | ... return "hello" 152 | ... 153 | >>> response = app.request("/hello") 154 | >>> response.data 155 | 'hello' 156 | >>> response.status 157 | '200 OK' 158 | >>> response.headers['Content-Type'] 159 | 'text/plain' 160 | 161 | To use https, use https=True. 162 | 163 | >>> urls = ("/redirect", "redirect") 164 | >>> app = application(urls, globals()) 165 | >>> class redirect: 166 | ... def GET(self): raise web.seeother("/foo") 167 | ... 168 | >>> response = app.request("/redirect") 169 | >>> response.headers['Location'] 170 | 'http://0.0.0.0:8080/foo' 171 | >>> response = app.request("/redirect", https=True) 172 | >>> response.headers['Location'] 173 | 'https://0.0.0.0:8080/foo' 174 | 175 | The headers argument specifies HTTP headers as a mapping object 176 | such as a dict. 177 | 178 | >>> urls = ('/ua', 'uaprinter') 179 | >>> class uaprinter: 180 | ... def GET(self): 181 | ... return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT'] 182 | ... 183 | >>> app = application(urls, globals()) 184 | >>> app.request('/ua', headers = { 185 | ... 'User-Agent': 'a small jumping bean/1.0 (compatible)' 186 | ... }).data 187 | 'your user-agent is a small jumping bean/1.0 (compatible)' 188 | 189 | """ 190 | path, maybe_query = urllib.splitquery(localpart) 191 | query = maybe_query or "" 192 | 193 | if 'env' in kw: 194 | env = kw['env'] 195 | else: 196 | env = {} 197 | env = dict(env, HTTP_HOST=host, REQUEST_METHOD=method, PATH_INFO=path, QUERY_STRING=query, HTTPS=str(https)) 198 | headers = headers or {} 199 | 200 | for k, v in headers.items(): 201 | env['HTTP_' + k.upper().replace('-', '_')] = v 202 | 203 | if 'HTTP_CONTENT_LENGTH' in env: 204 | env['CONTENT_LENGTH'] = env.pop('HTTP_CONTENT_LENGTH') 205 | 206 | if 'HTTP_CONTENT_TYPE' in env: 207 | env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE') 208 | 209 | if method in ["POST", "PUT"]: 210 | data = data or '' 211 | import StringIO 212 | if isinstance(data, dict): 213 | q = urllib.urlencode(data) 214 | else: 215 | q = data 216 | env['wsgi.input'] = StringIO.StringIO(q) 217 | if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env: 218 | env['CONTENT_LENGTH'] = len(q) 219 | response = web.storage() 220 | def start_response(status, headers): 221 | response.status = status 222 | response.headers = dict(headers) 223 | response.header_items = headers 224 | response.data = "".join(self.wsgifunc()(env, start_response)) 225 | return response 226 | 227 | def browser(self): 228 | import browser 229 | return browser.AppBrowser(self) 230 | 231 | def handle(self): 232 | fn, args = self._match(self.mapping, web.ctx.path) 233 | return self._delegate(fn, self.fvars, args) 234 | 235 | def handle_with_processors(self): 236 | def process(processors): 237 | try: 238 | if processors: 239 | p, processors = processors[0], processors[1:] 240 | return p(lambda: process(processors)) 241 | else: 242 | return self.handle() 243 | except web.HTTPError: 244 | raise 245 | except (KeyboardInterrupt, SystemExit): 246 | raise 247 | except: 248 | print >> web.debug, traceback.format_exc() 249 | raise self.internalerror() 250 | 251 | # processors must be applied in the resvere order. (??) 252 | return process(self.processors) 253 | 254 | def wsgifunc(self, *middleware): 255 | """Returns a WSGI-compatible function for this application.""" 256 | def peep(iterator): 257 | """Peeps into an iterator by doing an iteration 258 | and returns an equivalent iterator. 259 | """ 260 | # wsgi requires the headers first 261 | # so we need to do an iteration 262 | # and save the result for later 263 | try: 264 | firstchunk = iterator.next() 265 | except StopIteration: 266 | firstchunk = '' 267 | 268 | return itertools.chain([firstchunk], iterator) 269 | 270 | def is_generator(x): return x and hasattr(x, 'next') 271 | 272 | def wsgi(env, start_resp): 273 | self.load(env) 274 | try: 275 | # allow uppercase methods only 276 | if web.ctx.method.upper() != web.ctx.method: 277 | raise web.nomethod() 278 | 279 | result = self.handle_with_processors() 280 | if is_generator(result): 281 | result = peep(result) 282 | else: 283 | result = [result] 284 | except web.HTTPError, e: 285 | result = [e.data] 286 | 287 | result = web.utf8(iter(result)) 288 | 289 | status, headers = web.ctx.status, web.ctx.headers 290 | start_resp(status, headers) 291 | 292 | def cleanup(): 293 | self._cleanup() 294 | yield '' # force this function to be a generator 295 | 296 | return itertools.chain(result, cleanup()) 297 | 298 | for m in middleware: 299 | wsgi = m(wsgi) 300 | 301 | return wsgi 302 | 303 | def run(self, *middleware): 304 | """ 305 | Starts handling requests. If called in a CGI or FastCGI context, it will follow 306 | that protocol. If called from the command line, it will start an HTTP 307 | server on the port named in the first command line argument, or, if there 308 | is no argument, on port 8080. 309 | 310 | `middleware` is a list of WSGI middleware which is applied to the resulting WSGI 311 | function. 312 | """ 313 | return wsgi.runwsgi(self.wsgifunc(*middleware)) 314 | 315 | def cgirun(self, *middleware): 316 | """ 317 | Return a CGI handler. This is mostly useful with Google App Engine. 318 | There you can just do: 319 | 320 | main = app.cgirun() 321 | """ 322 | wsgiapp = self.wsgifunc(*middleware) 323 | 324 | try: 325 | from google.appengine.ext.webapp.util import run_wsgi_app 326 | return run_wsgi_app(wsgiapp) 327 | except ImportError: 328 | # we're not running from within Google App Engine 329 | return wsgiref.handlers.CGIHandler().run(wsgiapp) 330 | 331 | def load(self, env): 332 | """Initializes ctx using env.""" 333 | ctx = web.ctx 334 | ctx.clear() 335 | ctx.status = '200 OK' 336 | ctx.headers = [] 337 | ctx.output = '' 338 | ctx.environ = ctx.env = env 339 | ctx.host = env.get('HTTP_HOST') 340 | 341 | if env.get('wsgi.url_scheme') in ['http', 'https']: 342 | ctx.protocol = env['wsgi.url_scheme'] 343 | elif env.get('HTTPS', '').lower() in ['on', 'true', '1']: 344 | ctx.protocol = 'https' 345 | else: 346 | ctx.protocol = 'http' 347 | ctx.homedomain = ctx.protocol + '://' + env.get('HTTP_HOST', '[unknown]') 348 | ctx.homepath = os.environ.get('REAL_SCRIPT_NAME', env.get('SCRIPT_NAME', '')) 349 | ctx.home = ctx.homedomain + ctx.homepath 350 | #@@ home is changed when the request is handled to a sub-application. 351 | #@@ but the real home is required for doing absolute redirects. 352 | ctx.realhome = ctx.home 353 | ctx.ip = env.get('REMOTE_ADDR') 354 | ctx.method = env.get('REQUEST_METHOD') 355 | ctx.path = env.get('PATH_INFO') 356 | # http://trac.lighttpd.net/trac/ticket/406 requires: 357 | if env.get('SERVER_SOFTWARE', '').startswith('lighttpd/'): 358 | ctx.path = lstrips(env.get('REQUEST_URI').split('?')[0], ctx.homepath) 359 | # Apache and CherryPy webservers unquote the url but lighttpd doesn't. 360 | # unquote explicitly for lighttpd to make ctx.path uniform across all servers. 361 | ctx.path = urllib.unquote(ctx.path) 362 | 363 | if env.get('QUERY_STRING'): 364 | ctx.query = '?' + env.get('QUERY_STRING', '') 365 | else: 366 | ctx.query = '' 367 | 368 | ctx.fullpath = ctx.path + ctx.query 369 | 370 | for k, v in ctx.iteritems(): 371 | if isinstance(v, str): 372 | ctx[k] = safeunicode(v) 373 | 374 | # status must always be str 375 | ctx.status = '200 OK' 376 | 377 | ctx.app_stack = [] 378 | 379 | def _delegate(self, f, fvars, args=[]): 380 | def handle_class(cls): 381 | meth = web.ctx.method 382 | if meth == 'HEAD' and not hasattr(cls, meth): 383 | meth = 'GET' 384 | if not hasattr(cls, meth): 385 | raise web.nomethod(cls) 386 | tocall = getattr(cls(), meth) 387 | return tocall(*args) 388 | 389 | def is_class(o): return isinstance(o, (types.ClassType, type)) 390 | 391 | if f is None: 392 | raise web.notfound() 393 | elif isinstance(f, application): 394 | return f.handle_with_processors() 395 | elif is_class(f): 396 | return handle_class(f) 397 | elif isinstance(f, basestring): 398 | if f.startswith('redirect '): 399 | url = f.split(' ', 1)[1] 400 | if web.ctx.method == "GET": 401 | x = web.ctx.env.get('QUERY_STRING', '') 402 | if x: 403 | url += '?' + x 404 | raise web.redirect(url) 405 | elif '.' in f: 406 | x = f.split('.') 407 | mod, cls = '.'.join(x[:-1]), x[-1] 408 | mod = __import__(mod, globals(), locals(), [""]) 409 | cls = getattr(mod, cls) 410 | else: 411 | cls = fvars[f] 412 | return handle_class(cls) 413 | elif hasattr(f, '__call__'): 414 | return f() 415 | else: 416 | return web.notfound() 417 | 418 | def _match(self, mapping, value): 419 | for pat, what in utils.group(mapping, 2): 420 | if isinstance(what, application): 421 | if value.startswith(pat): 422 | f = lambda: self._delegate_sub_application(pat, what) 423 | return f, None 424 | else: 425 | continue 426 | elif isinstance(what, basestring): 427 | what, result = utils.re_subm('^' + pat + '$', what, value) 428 | else: 429 | result = utils.re_compile('^' + pat + '$').match(value) 430 | 431 | if result: # it's a match 432 | return what, [x for x in result.groups()] 433 | return None, None 434 | 435 | def _delegate_sub_application(self, dir, app): 436 | """Deletes request to sub application `app` rooted at the directory `dir`. 437 | The home, homepath, path and fullpath values in web.ctx are updated to mimic request 438 | to the subapp and are restored after it is handled. 439 | 440 | @@Any issues with when used with yield? 441 | """ 442 | web.ctx._oldctx = web.storage(web.ctx) 443 | web.ctx.home += dir 444 | web.ctx.homepath += dir 445 | web.ctx.path = web.ctx.path[len(dir):] 446 | web.ctx.fullpath = web.ctx.fullpath[len(dir):] 447 | return app.handle_with_processors() 448 | 449 | def get_parent_app(self): 450 | if self in web.ctx.app_stack: 451 | index = web.ctx.app_stack.index(self) 452 | if index > 0: 453 | return web.ctx.app_stack[index-1] 454 | 455 | def notfound(self): 456 | """Returns HTTPError with '404 not found' message""" 457 | parent = self.get_parent_app() 458 | if parent: 459 | return parent.notfound() 460 | else: 461 | return web._NotFound() 462 | 463 | def internalerror(self): 464 | """Returns HTTPError with '500 internal error' message""" 465 | parent = self.get_parent_app() 466 | if parent: 467 | return parent.internalerror() 468 | elif web.config.get('debug'): 469 | import debugerror 470 | return debugerror.debugerror() 471 | else: 472 | return web._InternalError() 473 | 474 | class auto_application(application): 475 | """Application similar to `application` but urls are constructed 476 | automatiacally using metaclass. 477 | 478 | >>> app = auto_application() 479 | >>> class hello(app.page): 480 | ... def GET(self): return "hello, world" 481 | ... 482 | >>> class foo(app.page): 483 | ... path = '/foo/.*' 484 | ... def GET(self): return "foo" 485 | >>> app.request("/hello").data 486 | 'hello, world' 487 | >>> app.request('/foo/bar').data 488 | 'foo' 489 | """ 490 | def __init__(self): 491 | application.__init__(self) 492 | 493 | class metapage(type): 494 | def __init__(klass, name, bases, attrs): 495 | type.__init__(klass, name, bases, attrs) 496 | path = attrs.get('path', '/' + name) 497 | 498 | # path can be specified as None to ignore that class 499 | # typically required to create a abstract base class. 500 | if path is not None: 501 | self.add_mapping(path, klass) 502 | 503 | class page: 504 | path = None 505 | __metaclass__ = metapage 506 | 507 | self.page = page 508 | 509 | # The application class already has the required functionality of subdir_application 510 | subdir_application = application 511 | 512 | class subdomain_application(application): 513 | """ 514 | Application to delegate requests based on the host. 515 | 516 | >>> urls = ("/hello", "hello") 517 | >>> app = application(urls, globals()) 518 | >>> class hello: 519 | ... def GET(self): return "hello" 520 | >>> 521 | >>> mapping = (r"hello\.example\.com", app) 522 | >>> app2 = subdomain_application(mapping) 523 | >>> app2.request("/hello", host="hello.example.com").data 524 | 'hello' 525 | >>> response = app2.request("/hello", host="something.example.com") 526 | >>> response.status 527 | '404 Not Found' 528 | >>> response.data 529 | 'not found' 530 | """ 531 | def handle(self): 532 | host = web.ctx.host.split(':')[0] #strip port 533 | fn, args = self._match(self.mapping, host) 534 | return self._delegate(fn, self.fvars, args) 535 | 536 | def _match(self, mapping, value): 537 | for pat, what in utils.group(mapping, 2): 538 | if isinstance(what, basestring): 539 | what, result = utils.re_subm('^' + pat + '$', what, value) 540 | else: 541 | result = utils.re_compile('^' + pat + '$').match(value) 542 | 543 | if result: # it's a match 544 | return what, [x for x in result.groups()] 545 | return None, None 546 | 547 | def loadhook(h): 548 | """ 549 | Converts a load hook into an application processor. 550 | 551 | >>> app = auto_application() 552 | >>> def f(): "something done before handling request" 553 | ... 554 | >>> app.add_processor(loadhook(f)) 555 | """ 556 | def processor(handler): 557 | h() 558 | return handler() 559 | 560 | return processor 561 | 562 | def unloadhook(h): 563 | """ 564 | Converts an unload hook into an application processor. 565 | 566 | >>> app = auto_application() 567 | >>> def f(): "something done after handling request" 568 | ... 569 | >>> app.add_processor(unloadhook(f)) 570 | """ 571 | def processor(handler): 572 | try: 573 | result = handler() 574 | is_generator = result and hasattr(result, 'next') 575 | except: 576 | # run the hook even when handler raises some exception 577 | h() 578 | raise 579 | 580 | if is_generator: 581 | return wrap(result) 582 | else: 583 | h() 584 | return result 585 | 586 | def wrap(result): 587 | def next(): 588 | try: 589 | return result.next() 590 | except: 591 | # call the hook at the and of iterator 592 | h() 593 | raise 594 | 595 | result = iter(result) 596 | while True: 597 | yield next() 598 | 599 | return processor 600 | 601 | def autodelegate(prefix=''): 602 | """ 603 | Returns a method that takes one argument and calls the method named prefix+arg, 604 | calling `notfound()` if there isn't one. Example: 605 | 606 | urls = ('/prefs/(.*)', 'prefs') 607 | 608 | class prefs: 609 | GET = autodelegate('GET_') 610 | def GET_password(self): pass 611 | def GET_privacy(self): pass 612 | 613 | `GET_password` would get called for `/prefs/password` while `GET_privacy` for 614 | `GET_privacy` gets called for `/prefs/privacy`. 615 | 616 | If a user visits `/prefs/password/change` then `GET_password(self, '/change')` 617 | is called. 618 | """ 619 | def internal(self, arg): 620 | if '/' in arg: 621 | first, rest = arg.split('/', 1) 622 | func = prefix + first 623 | args = ['/' + rest] 624 | else: 625 | func = prefix + arg 626 | args = [] 627 | 628 | if hasattr(self, func): 629 | try: 630 | return getattr(self, func)(*args) 631 | except TypeError: 632 | raise web.notfound() 633 | else: 634 | raise web.notfound() 635 | return internal 636 | 637 | class Reloader: 638 | """Checks to see if any loaded modules have changed on disk and, 639 | if so, reloads them. 640 | """ 641 | def __init__(self): 642 | self.mtimes = {} 643 | 644 | def __call__(self): 645 | for mod in sys.modules.values(): 646 | self.check(mod) 647 | 648 | def check(self, mod): 649 | try: 650 | mtime = os.stat(mod.__file__).st_mtime 651 | except (AttributeError, OSError, IOError): 652 | return 653 | if mod.__file__.endswith('.pyc') and os.path.exists(mod.__file__[:-1]): 654 | mtime = max(os.stat(mod.__file__[:-1]).st_mtime, mtime) 655 | 656 | if mod not in self.mtimes: 657 | self.mtimes[mod] = mtime 658 | elif self.mtimes[mod] < mtime: 659 | try: 660 | reload(mod) 661 | self.mtimes[mod] = mtime 662 | except ImportError: 663 | pass 664 | 665 | if __name__ == "__main__": 666 | import doctest 667 | doctest.testmod() 668 | -------------------------------------------------------------------------------- /kee/zepto.min.js: -------------------------------------------------------------------------------- 1 | /* Zepto v1.0-1-ga3cab6c - polyfill zepto detect event ajax form fx - zeptojs.com/license */ 2 | (function(a){String.prototype.trim===a&&(String.prototype.trim=function(){return this.replace(/^\s+|\s+$/g,"")}),Array.prototype.reduce===a&&(Array.prototype.reduce=function(b){if(this===void 0||this===null)throw new TypeError;var c=Object(this),d=c.length>>>0,e=0,f;if(typeof b!="function")throw new TypeError;if(d==0&&arguments.length==1)throw new TypeError;if(arguments.length>=2)f=arguments[1];else do{if(e in c){f=c[e++];break}if(++e>=d)throw new TypeError}while(!0);while(e0?c.fn.concat.apply([],a):a}function O(a){return a.replace(/::/g,"/").replace(/([A-Z]+)([A-Z][a-z])/g,"$1_$2").replace(/([a-z\d])([A-Z])/g,"$1_$2").replace(/_/g,"-").toLowerCase()}function P(a){return a in j?j[a]:j[a]=new RegExp("(^|\\s)"+a+"(\\s|$)")}function Q(a,b){return typeof b=="number"&&!l[O(a)]?b+"px":b}function R(a){var b,c;return i[a]||(b=h.createElement(a),h.body.appendChild(b),c=k(b,"").getPropertyValue("display"),b.parentNode.removeChild(b),c=="none"&&(c="block"),i[a]=c),i[a]}function S(a){return"children"in a?f.call(a.children):c.map(a.childNodes,function(a){if(a.nodeType==1)return a})}function T(c,d,e){for(b in d)e&&(J(d[b])||K(d[b]))?(J(d[b])&&!J(c[b])&&(c[b]={}),K(d[b])&&!K(c[b])&&(c[b]=[]),T(c[b],d[b],e)):d[b]!==a&&(c[b]=d[b])}function U(b,d){return d===a?c(b):c(b).filter(d)}function V(a,b,c,d){return F(b)?b.call(a,c,d):b}function W(a,b,c){c==null?a.removeAttribute(b):a.setAttribute(b,c)}function X(b,c){var d=b.className,e=d&&d.baseVal!==a;if(c===a)return e?d.baseVal:d;e?d.baseVal=c:b.className=c}function Y(a){var b;try{return a?a=="true"||(a=="false"?!1:a=="null"?null:isNaN(b=Number(a))?/^[\[\{]/.test(a)?c.parseJSON(a):a:b):a}catch(d){return a}}function Z(a,b){b(a);for(var c in a.childNodes)Z(a.childNodes[c],b)}var a,b,c,d,e=[],f=e.slice,g=e.filter,h=window.document,i={},j={},k=h.defaultView.getComputedStyle,l={"column-count":1,columns:1,"font-weight":1,"line-height":1,opacity:1,"z-index":1,zoom:1},m=/^\s*<(\w+|!)[^>]*>/,n=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,o=/^(?:body|html)$/i,p=["val","css","html","text","data","width","height","offset"],q=["after","prepend","before","append"],r=h.createElement("table"),s=h.createElement("tr"),t={tr:h.createElement("tbody"),tbody:r,thead:r,tfoot:r,td:s,th:s,"*":h.createElement("div")},u=/complete|loaded|interactive/,v=/^\.([\w-]+)$/,w=/^#([\w-]*)$/,x=/^[\w-]+$/,y={},z=y.toString,A={},B,C,D=h.createElement("div");return A.matches=function(a,b){if(!a||a.nodeType!==1)return!1;var c=a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.matchesSelector;if(c)return c.call(a,b);var d,e=a.parentNode,f=!e;return f&&(e=D).appendChild(a),d=~A.qsa(e,b).indexOf(a),f&&D.removeChild(a),d},B=function(a){return a.replace(/-+(.)?/g,function(a,b){return b?b.toUpperCase():""})},C=function(a){return g.call(a,function(b,c){return a.indexOf(b)==c})},A.fragment=function(b,d,e){b.replace&&(b=b.replace(n,"<$1>")),d===a&&(d=m.test(b)&&RegExp.$1),d in t||(d="*");var g,h,i=t[d];return i.innerHTML=""+b,h=c.each(f.call(i.childNodes),function(){i.removeChild(this)}),J(e)&&(g=c(h),c.each(e,function(a,b){p.indexOf(a)>-1?g[a](b):g.attr(a,b)})),h},A.Z=function(a,b){return a=a||[],a.__proto__=c.fn,a.selector=b||"",a},A.isZ=function(a){return a instanceof A.Z},A.init=function(b,d){if(!b)return A.Z();if(F(b))return c(h).ready(b);if(A.isZ(b))return b;var e;if(K(b))e=M(b);else if(I(b))e=[J(b)?c.extend({},b):b],b=null;else if(m.test(b))e=A.fragment(b.trim(),RegExp.$1,d),b=null;else{if(d!==a)return c(d).find(b);e=A.qsa(h,b)}return A.Z(e,b)},c=function(a,b){return A.init(a,b)},c.extend=function(a){var b,c=f.call(arguments,1);return typeof a=="boolean"&&(b=a,a=c.shift()),c.forEach(function(c){T(a,c,b)}),a},A.qsa=function(a,b){var c;return H(a)&&w.test(b)?(c=a.getElementById(RegExp.$1))?[c]:[]:a.nodeType!==1&&a.nodeType!==9?[]:f.call(v.test(b)?a.getElementsByClassName(RegExp.$1):x.test(b)?a.getElementsByTagName(b):a.querySelectorAll(b))},c.contains=function(a,b){return a!==b&&a.contains(b)},c.type=E,c.isFunction=F,c.isWindow=G,c.isArray=K,c.isPlainObject=J,c.isEmptyObject=function(a){var b;for(b in a)return!1;return!0},c.inArray=function(a,b,c){return e.indexOf.call(b,a,c)},c.camelCase=B,c.trim=function(a){return a.trim()},c.uuid=0,c.support={},c.expr={},c.map=function(a,b){var c,d=[],e,f;if(L(a))for(e=0;e=0?b:b+this.length]},toArray:function(){return this.get()},size:function(){return this.length},remove:function(){return this.each(function(){this.parentNode!=null&&this.parentNode.removeChild(this)})},each:function(a){return e.every.call(this,function(b,c){return a.call(b,c,b)!==!1}),this},filter:function(a){return F(a)?this.not(this.not(a)):c(g.call(this,function(b){return A.matches(b,a)}))},add:function(a,b){return c(C(this.concat(c(a,b))))},is:function(a){return this.length>0&&A.matches(this[0],a)},not:function(b){var d=[];if(F(b)&&b.call!==a)this.each(function(a){b.call(this,a)||d.push(this)});else{var e=typeof b=="string"?this.filter(b):L(b)&&F(b.item)?f.call(b):c(b);this.forEach(function(a){e.indexOf(a)<0&&d.push(a)})}return c(d)},has:function(a){return this.filter(function(){return I(a)?c.contains(this,a):c(this).find(a).size()})},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){var a=this[0];return a&&!I(a)?a:c(a)},last:function(){var a=this[this.length-1];return a&&!I(a)?a:c(a)},find:function(a){var b,d=this;return typeof a=="object"?b=c(a).filter(function(){var a=this;return e.some.call(d,function(b){return c.contains(b,a)})}):this.length==1?b=c(A.qsa(this[0],a)):b=this.map(function(){return A.qsa(this,a)}),b},closest:function(a,b){var d=this[0],e=!1;typeof a=="object"&&(e=c(a));while(d&&!(e?e.indexOf(d)>=0:A.matches(d,a)))d=d!==b&&!H(d)&&d.parentNode;return c(d)},parents:function(a){var b=[],d=this;while(d.length>0)d=c.map(d,function(a){if((a=a.parentNode)&&!H(a)&&b.indexOf(a)<0)return b.push(a),a});return U(b,a)},parent:function(a){return U(C(this.pluck("parentNode")),a)},children:function(a){return U(this.map(function(){return S(this)}),a)},contents:function(){return this.map(function(){return f.call(this.childNodes)})},siblings:function(a){return U(this.map(function(a,b){return g.call(S(b.parentNode),function(a){return a!==b})}),a)},empty:function(){return this.each(function(){this.innerHTML=""})},pluck:function(a){return c.map(this,function(b){return b[a]})},show:function(){return this.each(function(){this.style.display=="none"&&(this.style.display=null),k(this,"").getPropertyValue("display")=="none"&&(this.style.display=R(this.nodeName))})},replaceWith:function(a){return this.before(a).remove()},wrap:function(a){var b=F(a);if(this[0]&&!b)var d=c(a).get(0),e=d.parentNode||this.length>1;return this.each(function(f){c(this).wrapAll(b?a.call(this,f):e?d.cloneNode(!0):d)})},wrapAll:function(a){if(this[0]){c(this[0]).before(a=c(a));var b;while((b=a.children()).length)a=b.first();c(a).append(this)}return this},wrapInner:function(a){var b=F(a);return this.each(function(d){var e=c(this),f=e.contents(),g=b?a.call(this,d):a;f.length?f.wrapAll(g):e.append(g)})},unwrap:function(){return this.parent().each(function(){c(this).replaceWith(c(this).children())}),this},clone:function(){return this.map(function(){return this.cloneNode(!0)})},hide:function(){return this.css("display","none")},toggle:function(b){return this.each(function(){var d=c(this);(b===a?d.css("display")=="none":b)?d.show():d.hide()})},prev:function(a){return c(this.pluck("previousElementSibling")).filter(a||"*")},next:function(a){return c(this.pluck("nextElementSibling")).filter(a||"*")},html:function(b){return b===a?this.length>0?this[0].innerHTML:null:this.each(function(a){var d=this.innerHTML;c(this).empty().append(V(this,b,a,d))})},text:function(b){return b===a?this.length>0?this[0].textContent:null:this.each(function(){this.textContent=b})},attr:function(c,d){var e;return typeof c=="string"&&d===a?this.length==0||this[0].nodeType!==1?a:c=="value"&&this[0].nodeName=="INPUT"?this.val():!(e=this[0].getAttribute(c))&&c in this[0]?this[0][c]:e:this.each(function(a){if(this.nodeType!==1)return;if(I(c))for(b in c)W(this,b,c[b]);else W(this,c,V(this,d,a,this.getAttribute(c)))})},removeAttr:function(a){return this.each(function(){this.nodeType===1&&W(this,a)})},prop:function(b,c){return c===a?this[0]&&this[0][b]:this.each(function(a){this[b]=V(this,c,a,this[b])})},data:function(b,c){var d=this.attr("data-"+O(b),c);return d!==null?Y(d):a},val:function(b){return b===a?this[0]&&(this[0].multiple?c(this[0]).find("option").filter(function(a){return this.selected}).pluck("value"):this[0].value):this.each(function(a){this.value=V(this,b,a,this.value)})},offset:function(a){if(a)return this.each(function(b){var d=c(this),e=V(this,a,b,d.offset()),f=d.offsetParent().offset(),g={top:e.top-f.top,left:e.left-f.left};d.css("position")=="static"&&(g.position="relative"),d.css(g)});if(this.length==0)return null;var b=this[0].getBoundingClientRect();return{left:b.left+window.pageXOffset,top:b.top+window.pageYOffset,width:Math.round(b.width),height:Math.round(b.height)}},css:function(a,c){if(arguments.length<2&&typeof a=="string")return this[0]&&(this[0].style[B(a)]||k(this[0],"").getPropertyValue(a));var d="";if(E(a)=="string")!c&&c!==0?this.each(function(){this.style.removeProperty(O(a))}):d=O(a)+":"+Q(a,c);else for(b in a)!a[b]&&a[b]!==0?this.each(function(){this.style.removeProperty(O(b))}):d+=O(b)+":"+Q(b,a[b])+";";return this.each(function(){this.style.cssText+=";"+d})},index:function(a){return a?this.indexOf(c(a)[0]):this.parent().children().indexOf(this[0])},hasClass:function(a){return e.some.call(this,function(a){return this.test(X(a))},P(a))},addClass:function(a){return this.each(function(b){d=[];var e=X(this),f=V(this,a,b,e);f.split(/\s+/g).forEach(function(a){c(this).hasClass(a)||d.push(a)},this),d.length&&X(this,e+(e?" ":"")+d.join(" "))})},removeClass:function(b){return this.each(function(c){if(b===a)return X(this,"");d=X(this),V(this,b,c,d).split(/\s+/g).forEach(function(a){d=d.replace(P(a)," ")}),X(this,d.trim())})},toggleClass:function(b,d){return this.each(function(e){var f=c(this),g=V(this,b,e,X(this));g.split(/\s+/g).forEach(function(b){(d===a?!f.hasClass(b):d)?f.addClass(b):f.removeClass(b)})})},scrollTop:function(){if(!this.length)return;return"scrollTop"in this[0]?this[0].scrollTop:this[0].scrollY},position:function(){if(!this.length)return;var a=this[0],b=this.offsetParent(),d=this.offset(),e=o.test(b[0].nodeName)?{top:0,left:0}:b.offset();return d.top-=parseFloat(c(a).css("margin-top"))||0,d.left-=parseFloat(c(a).css("margin-left"))||0,e.top+=parseFloat(c(b[0]).css("border-top-width"))||0,e.left+=parseFloat(c(b[0]).css("border-left-width"))||0,{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||h.body;while(a&&!o.test(a.nodeName)&&c(a).css("position")=="static")a=a.offsetParent;return a})}},c.fn.detach=c.fn.remove,["width","height"].forEach(function(b){c.fn[b]=function(d){var e,f=this[0],g=b.replace(/./,function(a){return a[0].toUpperCase()});return d===a?G(f)?f["inner"+g]:H(f)?f.documentElement["offset"+g]:(e=this.offset())&&e[b]:this.each(function(a){f=c(this),f.css(b,V(this,d,a,f[b]()))})}}),q.forEach(function(a,b){var d=b%2;c.fn[a]=function(){var a,e=c.map(arguments,function(b){return a=E(b),a=="object"||a=="array"||b==null?b:A.fragment(b)}),f,g=this.length>1;return e.length<1?this:this.each(function(a,h){f=d?h:h.parentNode,h=b==0?h.nextSibling:b==1?h.firstChild:b==2?h:null,e.forEach(function(a){if(g)a=a.cloneNode(!0);else if(!f)return c(a).remove();Z(f.insertBefore(a,h),function(a){a.nodeName!=null&&a.nodeName.toUpperCase()==="SCRIPT"&&(!a.type||a.type==="text/javascript")&&!a.src&&window.eval.call(window,a.innerHTML)})})})},c.fn[d?a+"To":"insert"+(b?"Before":"After")]=function(b){return c(b)[a](this),this}}),A.Z.prototype=c.fn,A.uniq=C,A.deserializeValue=Y,c.zepto=A,c}();window.Zepto=Zepto,"$"in window||(window.$=Zepto),function(a){function b(a){var b=this.os={},c=this.browser={},d=a.match(/WebKit\/([\d.]+)/),e=a.match(/(Android)\s+([\d.]+)/),f=a.match(/(iPad).*OS\s([\d_]+)/),g=!f&&a.match(/(iPhone\sOS)\s([\d_]+)/),h=a.match(/(webOS|hpwOS)[\s\/]([\d.]+)/),i=h&&a.match(/TouchPad/),j=a.match(/Kindle\/([\d.]+)/),k=a.match(/Silk\/([\d._]+)/),l=a.match(/(BlackBerry).*Version\/([\d.]+)/),m=a.match(/(BB10).*Version\/([\d.]+)/),n=a.match(/(RIM\sTablet\sOS)\s([\d.]+)/),o=a.match(/PlayBook/),p=a.match(/Chrome\/([\d.]+)/)||a.match(/CriOS\/([\d.]+)/),q=a.match(/Firefox\/([\d.]+)/);if(c.webkit=!!d)c.version=d[1];e&&(b.android=!0,b.version=e[2]),g&&(b.ios=b.iphone=!0,b.version=g[2].replace(/_/g,".")),f&&(b.ios=b.ipad=!0,b.version=f[2].replace(/_/g,".")),h&&(b.webos=!0,b.version=h[2]),i&&(b.touchpad=!0),l&&(b.blackberry=!0,b.version=l[2]),m&&(b.bb10=!0,b.version=m[2]),n&&(b.rimtabletos=!0,b.version=n[2]),o&&(c.playbook=!0),j&&(b.kindle=!0,b.version=j[1]),k&&(c.silk=!0,c.version=k[1]),!k&&b.android&&a.match(/Kindle Fire/)&&(c.silk=!0),p&&(c.chrome=!0,c.version=p[1]),q&&(c.firefox=!0,c.version=q[1]),b.tablet=!!(f||o||e&&!a.match(/Mobile/)||q&&a.match(/Tablet/)),b.phone=!b.tablet&&!!(e||g||h||l||m||p&&a.match(/Android/)||p&&a.match(/CriOS\/([\d.]+)/)||q&&a.match(/Mobile/))}b.call(a,navigator.userAgent),a.__detect=b}(Zepto),function(a){function g(a){return a._zid||(a._zid=d++)}function h(a,b,d,e){b=i(b);if(b.ns)var f=j(b.ns);return(c[g(a)]||[]).filter(function(a){return a&&(!b.e||a.e==b.e)&&(!b.ns||f.test(a.ns))&&(!d||g(a.fn)===g(d))&&(!e||a.sel==e)})}function i(a){var b=(""+a).split(".");return{e:b[0],ns:b.slice(1).sort().join(" ")}}function j(a){return new RegExp("(?:^| )"+a.replace(" "," .* ?")+"(?: |$)")}function k(b,c,d){a.type(b)!="string"?a.each(b,d):b.split(/\s/).forEach(function(a){d(a,c)})}function l(a,b){return a.del&&(a.e=="focus"||a.e=="blur")||!!b}function m(a){return f[a]||a}function n(b,d,e,h,j,n){var o=g(b),p=c[o]||(c[o]=[]);k(d,e,function(c,d){var e=i(c);e.fn=d,e.sel=h,e.e in f&&(d=function(b){var c=b.relatedTarget;if(!c||c!==this&&!a.contains(this,c))return e.fn.apply(this,arguments)}),e.del=j&&j(d,c);var g=e.del||d;e.proxy=function(a){var c=g.apply(b,[a].concat(a.data));return c===!1&&(a.preventDefault(),a.stopPropagation()),c},e.i=p.length,p.push(e),b.addEventListener(m(e.e),e.proxy,l(e,n))})}function o(a,b,d,e,f){var i=g(a);k(b||"",d,function(b,d){h(a,b,d,e).forEach(function(b){delete c[i][b.i],a.removeEventListener(m(b.e),b.proxy,l(b,f))})})}function t(b){var c,d={originalEvent:b};for(c in b)!r.test(c)&&b[c]!==undefined&&(d[c]=b[c]);return a.each(s,function(a,c){d[a]=function(){return this[c]=p,b[a].apply(b,arguments)},d[c]=q}),d}function u(a){if(!("defaultPrevented"in a)){a.defaultPrevented=!1;var b=a.preventDefault;a.preventDefault=function(){this.defaultPrevented=!0,b.call(this)}}}var b=a.zepto.qsa,c={},d=1,e={},f={mouseenter:"mouseover",mouseleave:"mouseout"};e.click=e.mousedown=e.mouseup=e.mousemove="MouseEvents",a.event={add:n,remove:o},a.proxy=function(b,c){if(a.isFunction(b)){var d=function(){return b.apply(c,arguments)};return d._zid=g(b),d}if(typeof c=="string")return a.proxy(b[c],b);throw new TypeError("expected function")},a.fn.bind=function(a,b){return this.each(function(){n(this,a,b)})},a.fn.unbind=function(a,b){return this.each(function(){o(this,a,b)})},a.fn.one=function(a,b){return this.each(function(c,d){n(this,a,b,null,function(a,b){return function(){var c=a.apply(d,arguments);return o(d,b,a),c}})})};var p=function(){return!0},q=function(){return!1},r=/^([A-Z]|layer[XY]$)/,s={preventDefault:"isDefaultPrevented",stopImmediatePropagation:"isImmediatePropagationStopped",stopPropagation:"isPropagationStopped"};a.fn.delegate=function(b,c,d){return this.each(function(e,f){n(f,c,d,b,function(c){return function(d){var e,g=a(d.target).closest(b,f).get(0);if(g)return e=a.extend(t(d),{currentTarget:g,liveFired:f}),c.apply(g,[e].concat([].slice.call(arguments,1)))}})})},a.fn.undelegate=function(a,b,c){return this.each(function(){o(this,b,c,a)})},a.fn.live=function(b,c){return a(document.body).delegate(this.selector,b,c),this},a.fn.die=function(b,c){return a(document.body).undelegate(this.selector,b,c),this},a.fn.on=function(b,c,d){return!c||a.isFunction(c)?this.bind(b,c||d):this.delegate(c,b,d)},a.fn.off=function(b,c,d){return!c||a.isFunction(c)?this.unbind(b,c||d):this.undelegate(c,b,d)},a.fn.trigger=function(b,c){if(typeof b=="string"||a.isPlainObject(b))b=a.Event(b);return u(b),b.data=c,this.each(function(){"dispatchEvent"in this&&this.dispatchEvent(b)})},a.fn.triggerHandler=function(b,c){var d,e;return this.each(function(f,g){d=t(typeof b=="string"?a.Event(b):b),d.data=c,d.target=g,a.each(h(g,b.type||b),function(a,b){e=b.proxy(d);if(d.isImmediatePropagationStopped())return!1})}),e},"focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select keydown keypress keyup error".split(" ").forEach(function(b){a.fn[b]=function(a){return a?this.bind(b,a):this.trigger(b)}}),["focus","blur"].forEach(function(b){a.fn[b]=function(a){return a?this.bind(b,a):this.each(function(){try{this[b]()}catch(a){}}),this}}),a.Event=function(a,b){typeof a!="string"&&(b=a,a=b.type);var c=document.createEvent(e[a]||"Events"),d=!0;if(b)for(var f in b)f=="bubbles"?d=!!b[f]:c[f]=b[f];return c.initEvent(a,d,!0,null,null,null,null,null,null,null,null,null,null,null,null),c.isDefaultPrevented=function(){return this.defaultPrevented},c}}(Zepto),function($){function triggerAndReturn(a,b,c){var d=$.Event(b);return $(a).trigger(d,c),!d.defaultPrevented}function triggerGlobal(a,b,c,d){if(a.global)return triggerAndReturn(b||document,c,d)}function ajaxStart(a){a.global&&$.active++===0&&triggerGlobal(a,null,"ajaxStart")}function ajaxStop(a){a.global&&!--$.active&&triggerGlobal(a,null,"ajaxStop")}function ajaxBeforeSend(a,b){var c=b.context;if(b.beforeSend.call(c,a,b)===!1||triggerGlobal(b,c,"ajaxBeforeSend",[a,b])===!1)return!1;triggerGlobal(b,c,"ajaxSend",[a,b])}function ajaxSuccess(a,b,c){var d=c.context,e="success";c.success.call(d,a,e,b),triggerGlobal(c,d,"ajaxSuccess",[b,c,a]),ajaxComplete(e,b,c)}function ajaxError(a,b,c,d){var e=d.context;d.error.call(e,c,b,a),triggerGlobal(d,e,"ajaxError",[c,d,a]),ajaxComplete(b,c,d)}function ajaxComplete(a,b,c){var d=c.context;c.complete.call(d,b,a),triggerGlobal(c,d,"ajaxComplete",[b,c]),ajaxStop(c)}function empty(){}function mimeToDataType(a){return a&&(a=a.split(";",2)[0]),a&&(a==htmlType?"html":a==jsonType?"json":scriptTypeRE.test(a)?"script":xmlTypeRE.test(a)&&"xml")||"text"}function appendQuery(a,b){return(a+"&"+b).replace(/[&?]{1,2}/,"?")}function serializeData(a){a.processData&&a.data&&$.type(a.data)!="string"&&(a.data=$.param(a.data,a.traditional)),a.data&&(!a.type||a.type.toUpperCase()=="GET")&&(a.url=appendQuery(a.url,a.data))}function parseArguments(a,b,c,d){var e=!$.isFunction(b);return{url:a,data:e?b:undefined,success:e?$.isFunction(c)?c:undefined:b,dataType:e?d||c:c}}function serialize(a,b,c,d){var e,f=$.isArray(b);$.each(b,function(b,g){e=$.type(g),d&&(b=c?d:d+"["+(f?"":b)+"]"),!d&&f?a.add(g.name,g.value):e=="array"||!c&&e=="object"?serialize(a,g,c,b):a.add(b,g)})}var jsonpID=0,document=window.document,key,name,rscript=/)<[^<]*)*<\/script>/gi,scriptTypeRE=/^(?:text|application)\/javascript/i,xmlTypeRE=/^(?:text|application)\/xml/i,jsonType="application/json",htmlType="text/html",blankRE=/^\s*$/;$.active=0,$.ajaxJSONP=function(a){if("type"in a){var b="jsonp"+ ++jsonpID,c=document.createElement("script"),d=function(){clearTimeout(g),$(c).remove(),delete window[b]},e=function(c){d();if(!c||c=="timeout")window[b]=empty;ajaxError(null,c||"abort",f,a)},f={abort:e},g;return ajaxBeforeSend(f,a)===!1?(e("abort"),!1):(window[b]=function(b){d(),ajaxSuccess(b,f,a)},c.onerror=function(){e("error")},c.src=a.url.replace(/=\?/,"="+b),$("head").append(c),a.timeout>0&&(g=setTimeout(function(){e("timeout")},a.timeout)),f)}return $.ajax(a)},$.ajaxSettings={type:"GET",beforeSend:empty,success:empty,error:empty,complete:empty,context:null,global:!0,xhr:function(){return new window.XMLHttpRequest},accepts:{script:"text/javascript, application/javascript",json:jsonType,xml:"application/xml, text/xml",html:htmlType,text:"text/plain"},crossDomain:!1,timeout:0,processData:!0,cache:!0},$.ajax=function(options){var settings=$.extend({},options||{});for(key in $.ajaxSettings)settings[key]===undefined&&(settings[key]=$.ajaxSettings[key]);ajaxStart(settings),settings.crossDomain||(settings.crossDomain=/^([\w-]+:)?\/\/([^\/]+)/.test(settings.url)&&RegExp.$2!=window.location.host),settings.url||(settings.url=window.location.toString()),serializeData(settings),settings.cache===!1&&(settings.url=appendQuery(settings.url,"_="+Date.now()));var dataType=settings.dataType,hasPlaceholder=/=\?/.test(settings.url);if(dataType=="jsonp"||hasPlaceholder)return hasPlaceholder||(settings.url=appendQuery(settings.url,"callback=?")),$.ajaxJSONP(settings);var mime=settings.accepts[dataType],baseHeaders={},protocol=/^([\w-]+:)\/\//.test(settings.url)?RegExp.$1:window.location.protocol,xhr=settings.xhr(),abortTimeout;settings.crossDomain||(baseHeaders["X-Requested-With"]="XMLHttpRequest"),mime&&(baseHeaders.Accept=mime,mime.indexOf(",")>-1&&(mime=mime.split(",",2)[0]),xhr.overrideMimeType&&xhr.overrideMimeType(mime));if(settings.contentType||settings.contentType!==!1&&settings.data&&settings.type.toUpperCase()!="GET")baseHeaders["Content-Type"]=settings.contentType||"application/x-www-form-urlencoded";settings.headers=$.extend(baseHeaders,settings.headers||{}),xhr.onreadystatechange=function(){if(xhr.readyState==4){xhr.onreadystatechange=empty,clearTimeout(abortTimeout);var result,error=!1;if(xhr.status>=200&&xhr.status<300||xhr.status==304||xhr.status==0&&protocol=="file:"){dataType=dataType||mimeToDataType(xhr.getResponseHeader("content-type")),result=xhr.responseText;try{dataType=="script"?(1,eval)(result):dataType=="xml"?result=xhr.responseXML:dataType=="json"&&(result=blankRE.test(result)?null:$.parseJSON(result))}catch(e){error=e}error?ajaxError(error,"parsererror",xhr,settings):ajaxSuccess(result,xhr,settings)}else ajaxError(null,xhr.status?"error":"abort",xhr,settings)}};var async="async"in settings?settings.async:!0;xhr.open(settings.type,settings.url,async);for(name in settings.headers)xhr.setRequestHeader(name,settings.headers[name]);return ajaxBeforeSend(xhr,settings)===!1?(xhr.abort(),!1):(settings.timeout>0&&(abortTimeout=setTimeout(function(){xhr.onreadystatechange=empty,xhr.abort(),ajaxError(null,"timeout",xhr,settings)},settings.timeout)),xhr.send(settings.data?settings.data:null),xhr)},$.get=function(a,b,c,d){return $.ajax(parseArguments.apply(null,arguments))},$.post=function(a,b,c,d){var e=parseArguments.apply(null,arguments);return e.type="POST",$.ajax(e)},$.getJSON=function(a,b,c){var d=parseArguments.apply(null,arguments);return d.dataType="json",$.ajax(d)},$.fn.load=function(a,b,c){if(!this.length)return this;var d=this,e=a.split(/\s/),f,g=parseArguments(a,b,c),h=g.success;return e.length>1&&(g.url=e[0],f=e[1]),g.success=function(a){d.html(f?$("
").html(a.replace(rscript,"")).find(f):a),h&&h.apply(d,arguments)},$.ajax(g),this};var escape=encodeURIComponent;$.param=function(a,b){var c=[];return c.add=function(a,b){this.push(escape(a)+"="+escape(b))},serialize(c,a,b),c.join("&").replace(/%20/g,"+")}}(Zepto),function(a){a.fn.serializeArray=function(){var b=[],c;return a(Array.prototype.slice.call(this.get(0).elements)).each(function(){c=a(this);var d=c.attr("type");this.nodeName.toLowerCase()!="fieldset"&&!this.disabled&&d!="submit"&&d!="reset"&&d!="button"&&(d!="radio"&&d!="checkbox"||this.checked)&&b.push({name:c.attr("name"),value:c.val()})}),b},a.fn.serialize=function(){var a=[];return this.serializeArray().forEach(function(b){a.push(encodeURIComponent(b.name)+"="+encodeURIComponent(b.value))}),a.join("&")},a.fn.submit=function(b){if(b)this.bind("submit",b);else if(this.length){var c=a.Event("submit");this.eq(0).trigger(c),c.defaultPrevented||this.get(0).submit()}return this}}(Zepto),function(a,b){function s(a){return t(a.replace(/([a-z])([A-Z])/,"$1-$2"))}function t(a){return a.toLowerCase()}function u(a){return d?d+a:t(a)}var c="",d,e,f,g={Webkit:"webkit",Moz:"",O:"o",ms:"MS"},h=window.document,i=h.createElement("div"),j=/^((translate|rotate|scale)(X|Y|Z|3d)?|matrix(3d)?|perspective|skew(X|Y)?)$/i,k,l,m,n,o,p,q,r={};a.each(g,function(a,e){if(i.style[a+"TransitionProperty"]!==b)return c="-"+t(a)+"-",d=e,!1}),k=c+"transform",r[l=c+"transition-property"]=r[m=c+"transition-duration"]=r[n=c+"transition-timing-function"]=r[o=c+"animation-name"]=r[p=c+"animation-duration"]=r[q=c+"animation-timing-function"]="",a.fx={off:d===b&&i.style.transitionProperty===b,speeds:{_default:400,fast:200,slow:600},cssPrefix:c,transitionEnd:u("TransitionEnd"),animationEnd:u("AnimationEnd")},a.fn.animate=function(b,c,d,e){return a.isPlainObject(c)&&(d=c.easing,e=c.complete,c=c.duration),c&&(c=(typeof c=="number"?c:a.fx.speeds[c]||a.fx.speeds._default)/1e3),this.anim(b,c,d,e)},a.fn.anim=function(c,d,e,f){var g,h={},i,t="",u=this,v,w=a.fx.transitionEnd;d===b&&(d=.4),a.fx.off&&(d=0);if(typeof c=="string")h[o]=c,h[p]=d+"s",h[q]=e||"linear",w=a.fx.animationEnd;else{i=[];for(g in c)j.test(g)?t+=g+"("+c[g]+") ":(h[g]=c[g],i.push(s(g)));t&&(h[k]=t,i.push(k)),d>0&&typeof c=="object"&&(h[l]=i.join(", "),h[m]=d+"s",h[n]=e||"linear")}return v=function(b){if(typeof b!="undefined"){if(b.target!==b.currentTarget)return;a(b.target).unbind(w,v)}a(this).css(r),f&&f.call(this)},d>0&&this.bind(w,v),this.size()&&this.get(0).clientLeft,this.css(h),d<=0&&setTimeout(function(){u.each(function(){v.call(this)})},0),this},i=null}(Zepto) -------------------------------------------------------------------------------- /web/utils.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | General Utilities 4 | (part of web.py) 5 | """ 6 | 7 | __all__ = [ 8 | "Storage", "storage", "storify", 9 | "iters", 10 | "rstrips", "lstrips", "strips", 11 | "safeunicode", "safestr", "utf8", 12 | "TimeoutError", "timelimit", 13 | "Memoize", "memoize", 14 | "re_compile", "re_subm", 15 | "group", "uniq", "iterview", 16 | "IterBetter", "iterbetter", 17 | "dictreverse", "dictfind", "dictfindall", "dictincr", "dictadd", 18 | "listget", "intget", "datestr", 19 | "numify", "denumify", "commify", "dateify", 20 | "nthstr", 21 | "CaptureStdout", "capturestdout", "Profile", "profile", 22 | "tryall", 23 | "ThreadedDict", "threadeddict", 24 | "autoassign", 25 | "to36", 26 | "safemarkdown", 27 | "sendmail" 28 | ] 29 | 30 | import re, sys, time, threading, itertools 31 | 32 | try: 33 | import subprocess 34 | except ImportError: 35 | subprocess = None 36 | 37 | try: import datetime 38 | except ImportError: pass 39 | 40 | try: set 41 | except NameError: 42 | from sets import Set as set 43 | 44 | class Storage(dict): 45 | """ 46 | A Storage object is like a dictionary except `obj.foo` can be used 47 | in addition to `obj['foo']`. 48 | 49 | >>> o = storage(a=1) 50 | >>> o.a 51 | 1 52 | >>> o['a'] 53 | 1 54 | >>> o.a = 2 55 | >>> o['a'] 56 | 2 57 | >>> del o.a 58 | >>> o.a 59 | Traceback (most recent call last): 60 | ... 61 | AttributeError: 'a' 62 | 63 | """ 64 | def __getattr__(self, key): 65 | try: 66 | return self[key] 67 | except KeyError, k: 68 | raise AttributeError, k 69 | 70 | def __setattr__(self, key, value): 71 | self[key] = value 72 | 73 | def __delattr__(self, key): 74 | try: 75 | del self[key] 76 | except KeyError, k: 77 | raise AttributeError, k 78 | 79 | def __repr__(self): 80 | return '' 81 | 82 | storage = Storage 83 | 84 | def storify(mapping, *requireds, **defaults): 85 | """ 86 | Creates a `storage` object from dictionary `mapping`, raising `KeyError` if 87 | d doesn't have all of the keys in `requireds` and using the default 88 | values for keys found in `defaults`. 89 | 90 | For example, `storify({'a':1, 'c':3}, b=2, c=0)` will return the equivalent of 91 | `storage({'a':1, 'b':2, 'c':3})`. 92 | 93 | If a `storify` value is a list (e.g. multiple values in a form submission), 94 | `storify` returns the last element of the list, unless the key appears in 95 | `defaults` as a list. Thus: 96 | 97 | >>> storify({'a':[1, 2]}).a 98 | 2 99 | >>> storify({'a':[1, 2]}, a=[]).a 100 | [1, 2] 101 | >>> storify({'a':1}, a=[]).a 102 | [1] 103 | >>> storify({}, a=[]).a 104 | [] 105 | 106 | Similarly, if the value has a `value` attribute, `storify will return _its_ 107 | value, unless the key appears in `defaults` as a dictionary. 108 | 109 | >>> storify({'a':storage(value=1)}).a 110 | 1 111 | >>> storify({'a':storage(value=1)}, a={}).a 112 | 113 | >>> storify({}, a={}).a 114 | {} 115 | 116 | Optionally, keyword parameter `_unicode` can be passed to convert all values to unicode. 117 | 118 | >>> storify({'x': 'a'}, _unicode=True) 119 | 120 | >>> storify({'x': storage(value='a')}, x={}, _unicode=True) 121 | }> 122 | >>> storify({'x': storage(value='a')}, _unicode=True) 123 | 124 | """ 125 | _unicode = defaults.pop('_unicode', False) 126 | def unicodify(s): 127 | if _unicode and isinstance(s, str): return safeunicode(s) 128 | else: return s 129 | 130 | def getvalue(x): 131 | if hasattr(x, 'file') and hasattr(x, 'value'): 132 | return x.value 133 | elif hasattr(x, 'value'): 134 | return unicodify(x.value) 135 | else: 136 | return unicodify(x) 137 | 138 | stor = Storage() 139 | for key in requireds + tuple(mapping.keys()): 140 | value = mapping[key] 141 | if isinstance(value, list): 142 | if isinstance(defaults.get(key), list): 143 | value = [getvalue(x) for x in value] 144 | else: 145 | value = value[-1] 146 | if not isinstance(defaults.get(key), dict): 147 | value = getvalue(value) 148 | if isinstance(defaults.get(key), list) and not isinstance(value, list): 149 | value = [value] 150 | setattr(stor, key, value) 151 | 152 | for (key, value) in defaults.iteritems(): 153 | result = value 154 | if hasattr(stor, key): 155 | result = stor[key] 156 | if value == () and not isinstance(result, tuple): 157 | result = (result,) 158 | setattr(stor, key, result) 159 | 160 | return stor 161 | 162 | iters = [list, tuple] 163 | import __builtin__ 164 | if hasattr(__builtin__, 'set'): 165 | iters.append(set) 166 | if hasattr(__builtin__, 'frozenset'): 167 | iters.append(set) 168 | if sys.version_info < (2,6): # sets module deprecated in 2.6 169 | try: 170 | from sets import Set 171 | iters.append(Set) 172 | except ImportError: 173 | pass 174 | 175 | class _hack(tuple): pass 176 | iters = _hack(iters) 177 | iters.__doc__ = """ 178 | A list of iterable items (like lists, but not strings). Includes whichever 179 | of lists, tuples, sets, and Sets are available in this version of Python. 180 | """ 181 | 182 | def _strips(direction, text, remove): 183 | if direction == 'l': 184 | if text.startswith(remove): 185 | return text[len(remove):] 186 | elif direction == 'r': 187 | if text.endswith(remove): 188 | return text[:-len(remove)] 189 | else: 190 | raise ValueError, "Direction needs to be r or l." 191 | return text 192 | 193 | def rstrips(text, remove): 194 | """ 195 | removes the string `remove` from the right of `text` 196 | 197 | >>> rstrips("foobar", "bar") 198 | 'foo' 199 | 200 | """ 201 | return _strips('r', text, remove) 202 | 203 | def lstrips(text, remove): 204 | """ 205 | removes the string `remove` from the left of `text` 206 | 207 | >>> lstrips("foobar", "foo") 208 | 'bar' 209 | 210 | """ 211 | return _strips('l', text, remove) 212 | 213 | def strips(text, remove): 214 | """ 215 | removes the string `remove` from the both sides of `text` 216 | 217 | >>> strips("foobarfoo", "foo") 218 | 'bar' 219 | 220 | """ 221 | return rstrips(lstrips(text, remove), remove) 222 | 223 | def safeunicode(obj, encoding='utf-8'): 224 | r""" 225 | Converts any given object to unicode string. 226 | 227 | >>> safeunicode('hello') 228 | u'hello' 229 | >>> safeunicode(2) 230 | u'2' 231 | >>> safeunicode('\xe1\x88\xb4') 232 | u'\u1234' 233 | """ 234 | if isinstance(obj, unicode): 235 | return obj 236 | elif isinstance(obj, str): 237 | return obj.decode(encoding) 238 | else: 239 | if hasattr(obj, '__unicode__'): 240 | return unicode(obj) 241 | else: 242 | return str(obj).decode(encoding) 243 | 244 | def safestr(obj, encoding='utf-8'): 245 | r""" 246 | Converts any given object to utf-8 encoded string. 247 | 248 | >>> safestr('hello') 249 | 'hello' 250 | >>> safestr(u'\u1234') 251 | '\xe1\x88\xb4' 252 | >>> safestr(2) 253 | '2' 254 | """ 255 | if isinstance(obj, unicode): 256 | return obj.encode('utf-8') 257 | elif isinstance(obj, str): 258 | return obj 259 | elif hasattr(obj, 'next') and hasattr(obj, '__iter__'): # iterator 260 | return itertools.imap(safestr, obj) 261 | else: 262 | return str(obj) 263 | 264 | # for backward-compatibility 265 | utf8 = safestr 266 | 267 | class TimeoutError(Exception): pass 268 | def timelimit(timeout): 269 | """ 270 | A decorator to limit a function to `timeout` seconds, raising `TimeoutError` 271 | if it takes longer. 272 | 273 | >>> import time 274 | >>> def meaningoflife(): 275 | ... time.sleep(.2) 276 | ... return 42 277 | >>> 278 | >>> timelimit(.1)(meaningoflife)() 279 | Traceback (most recent call last): 280 | ... 281 | TimeoutError: took too long 282 | >>> timelimit(1)(meaningoflife)() 283 | 42 284 | 285 | _Caveat:_ The function isn't stopped after `timeout` seconds but continues 286 | executing in a separate thread. (There seems to be no way to kill a thread.) 287 | 288 | inspired by 289 | """ 290 | def _1(function): 291 | def _2(*args, **kw): 292 | class Dispatch(threading.Thread): 293 | def __init__(self): 294 | threading.Thread.__init__(self) 295 | self.result = None 296 | self.error = None 297 | 298 | self.setDaemon(True) 299 | self.start() 300 | 301 | def run(self): 302 | try: 303 | self.result = function(*args, **kw) 304 | except: 305 | self.error = sys.exc_info() 306 | 307 | c = Dispatch() 308 | c.join(timeout) 309 | if c.isAlive(): 310 | raise TimeoutError, 'took too long' 311 | if c.error: 312 | raise c.error[0], c.error[1] 313 | return c.result 314 | return _2 315 | return _1 316 | 317 | class Memoize: 318 | """ 319 | 'Memoizes' a function, caching its return values for each input. 320 | If `expires` is specified, values are recalculated after `expires` seconds. 321 | If `background` is specified, values are recalculated in a separate thread. 322 | 323 | >>> calls = 0 324 | >>> def howmanytimeshaveibeencalled(): 325 | ... global calls 326 | ... calls += 1 327 | ... return calls 328 | >>> fastcalls = memoize(howmanytimeshaveibeencalled) 329 | >>> howmanytimeshaveibeencalled() 330 | 1 331 | >>> howmanytimeshaveibeencalled() 332 | 2 333 | >>> fastcalls() 334 | 3 335 | >>> fastcalls() 336 | 3 337 | >>> import time 338 | >>> fastcalls = memoize(howmanytimeshaveibeencalled, .1, background=False) 339 | >>> fastcalls() 340 | 4 341 | >>> fastcalls() 342 | 4 343 | >>> time.sleep(.2) 344 | >>> fastcalls() 345 | 5 346 | >>> def slowfunc(): 347 | ... time.sleep(.1) 348 | ... return howmanytimeshaveibeencalled() 349 | >>> fastcalls = memoize(slowfunc, .2, background=True) 350 | >>> fastcalls() 351 | 6 352 | >>> timelimit(.05)(fastcalls)() 353 | 6 354 | >>> time.sleep(.2) 355 | >>> timelimit(.05)(fastcalls)() 356 | 6 357 | >>> timelimit(.05)(fastcalls)() 358 | 6 359 | >>> time.sleep(.2) 360 | >>> timelimit(.05)(fastcalls)() 361 | 7 362 | >>> fastcalls = memoize(slowfunc, None, background=True) 363 | >>> threading.Thread(target=fastcalls).start() 364 | >>> time.sleep(.01) 365 | >>> fastcalls() 366 | 9 367 | """ 368 | def __init__(self, func, expires=None, background=True): 369 | self.func = func 370 | self.cache = {} 371 | self.expires = expires 372 | self.background = background 373 | self.running = {} 374 | 375 | def __call__(self, *args, **keywords): 376 | key = (args, tuple(keywords.items())) 377 | if not self.running.get(key): 378 | self.running[key] = threading.Lock() 379 | def update(block=False): 380 | if self.running[key].acquire(block): 381 | try: 382 | self.cache[key] = (self.func(*args, **keywords), time.time()) 383 | finally: 384 | self.running[key].release() 385 | 386 | if key not in self.cache: 387 | update(block=True) 388 | elif self.expires and (time.time() - self.cache[key][1]) > self.expires: 389 | if self.background: 390 | threading.Thread(target=update).start() 391 | else: 392 | update() 393 | return self.cache[key][0] 394 | 395 | memoize = Memoize 396 | 397 | re_compile = memoize(re.compile) #@@ threadsafe? 398 | re_compile.__doc__ = """ 399 | A memoized version of re.compile. 400 | """ 401 | 402 | class _re_subm_proxy: 403 | def __init__(self): 404 | self.match = None 405 | def __call__(self, match): 406 | self.match = match 407 | return '' 408 | 409 | def re_subm(pat, repl, string): 410 | """ 411 | Like re.sub, but returns the replacement _and_ the match object. 412 | 413 | >>> t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball') 414 | >>> t 415 | 'foooooolish' 416 | >>> m.groups() 417 | ('oooooo',) 418 | """ 419 | compiled_pat = re_compile(pat) 420 | proxy = _re_subm_proxy() 421 | compiled_pat.sub(proxy.__call__, string) 422 | return compiled_pat.sub(repl, string), proxy.match 423 | 424 | def group(seq, size): 425 | """ 426 | Returns an iterator over a series of lists of length size from iterable. 427 | 428 | >>> list(group([1,2,3,4], 2)) 429 | [[1, 2], [3, 4]] 430 | >>> list(group([1,2,3,4,5], 2)) 431 | [[1, 2], [3, 4], [5]] 432 | """ 433 | def take(seq, n): 434 | for i in xrange(n): 435 | yield seq.next() 436 | 437 | if not hasattr(seq, 'next'): 438 | seq = iter(seq) 439 | while True: 440 | x = list(take(seq, size)) 441 | if x: 442 | yield x 443 | else: 444 | break 445 | 446 | def uniq(seq): 447 | """ 448 | Removes duplicate elements from a list. 449 | 450 | >>> uniq([1,2,3,1,4,5,6]) 451 | [1, 2, 3, 4, 5, 6] 452 | """ 453 | seen = set() 454 | result = [] 455 | for item in seq: 456 | if item in seen: continue 457 | seen.add(item) 458 | result.append(item) 459 | return result 460 | 461 | def iterview(x): 462 | """ 463 | Takes an iterable `x` and returns an iterator over it 464 | which prints its progress to stderr as it iterates through. 465 | """ 466 | WIDTH = 70 467 | 468 | def plainformat(n, lenx): 469 | return '%5.1f%% (%*d/%d)' % ((float(n)/lenx)*100, len(str(lenx)), n, lenx) 470 | 471 | def bars(size, n, lenx): 472 | val = int((float(n)*size)/lenx + 0.5) 473 | if size - val: 474 | spacing = ">" + (" "*(size-val))[1:] 475 | else: 476 | spacing = "" 477 | return "[%s%s]" % ("="*val, spacing) 478 | 479 | def eta(elapsed, n, lenx): 480 | if n == 0: 481 | return '--:--:--' 482 | if n == lenx: 483 | secs = int(elapsed) 484 | else: 485 | secs = int((elapsed/n) * (lenx-n)) 486 | mins, secs = divmod(secs, 60) 487 | hrs, mins = divmod(mins, 60) 488 | 489 | return '%02d:%02d:%02d' % (hrs, mins, secs) 490 | 491 | def format(starttime, n, lenx): 492 | out = plainformat(n, lenx) + ' ' 493 | if n == lenx: 494 | end = ' ' 495 | else: 496 | end = ' ETA ' 497 | end += eta(time.time() - starttime, n, lenx) 498 | out += bars(WIDTH - len(out) - len(end), n, lenx) 499 | out += end 500 | return out 501 | 502 | starttime = time.time() 503 | lenx = len(x) 504 | for n, y in enumerate(x): 505 | sys.stderr.write('\r' + format(starttime, n, lenx)) 506 | yield y 507 | sys.stderr.write('\r' + format(starttime, n+1, lenx) + '\n') 508 | 509 | class IterBetter: 510 | """ 511 | Returns an object that can be used as an iterator 512 | but can also be used via __getitem__ (although it 513 | cannot go backwards -- that is, you cannot request 514 | `iterbetter[0]` after requesting `iterbetter[1]`). 515 | 516 | >>> import itertools 517 | >>> c = iterbetter(itertools.count()) 518 | >>> c[1] 519 | 1 520 | >>> c[5] 521 | 5 522 | >>> c[3] 523 | Traceback (most recent call last): 524 | ... 525 | IndexError: already passed 3 526 | """ 527 | def __init__(self, iterator): 528 | self.i, self.c = iterator, 0 529 | def __iter__(self): 530 | while 1: 531 | yield self.i.next() 532 | self.c += 1 533 | def __getitem__(self, i): 534 | #todo: slices 535 | if i < self.c: 536 | raise IndexError, "already passed "+str(i) 537 | try: 538 | while i > self.c: 539 | self.i.next() 540 | self.c += 1 541 | # now self.c == i 542 | self.c += 1 543 | return self.i.next() 544 | except StopIteration: 545 | raise IndexError, str(i) 546 | 547 | def __nonzero__(self): 548 | return len(self) != 0 549 | 550 | iterbetter = IterBetter 551 | 552 | def dictreverse(mapping): 553 | """ 554 | Returns a new dictionary with keys and values swapped. 555 | 556 | >>> dictreverse({1: 2, 3: 4}) 557 | {2: 1, 4: 3} 558 | """ 559 | return dict([(value, key) for (key, value) in mapping.iteritems()]) 560 | 561 | def dictfind(dictionary, element): 562 | """ 563 | Returns a key whose value in `dictionary` is `element` 564 | or, if none exists, None. 565 | 566 | >>> d = {1:2, 3:4} 567 | >>> dictfind(d, 4) 568 | 3 569 | >>> dictfind(d, 5) 570 | """ 571 | for (key, value) in dictionary.iteritems(): 572 | if element is value: 573 | return key 574 | 575 | def dictfindall(dictionary, element): 576 | """ 577 | Returns the keys whose values in `dictionary` are `element` 578 | or, if none exists, []. 579 | 580 | >>> d = {1:4, 3:4} 581 | >>> dictfindall(d, 4) 582 | [1, 3] 583 | >>> dictfindall(d, 5) 584 | [] 585 | """ 586 | res = [] 587 | for (key, value) in dictionary.iteritems(): 588 | if element is value: 589 | res.append(key) 590 | return res 591 | 592 | def dictincr(dictionary, element): 593 | """ 594 | Increments `element` in `dictionary`, 595 | setting it to one if it doesn't exist. 596 | 597 | >>> d = {1:2, 3:4} 598 | >>> dictincr(d, 1) 599 | 3 600 | >>> d[1] 601 | 3 602 | >>> dictincr(d, 5) 603 | 1 604 | >>> d[5] 605 | 1 606 | """ 607 | dictionary.setdefault(element, 0) 608 | dictionary[element] += 1 609 | return dictionary[element] 610 | 611 | def dictadd(*dicts): 612 | """ 613 | Returns a dictionary consisting of the keys in the argument dictionaries. 614 | If they share a key, the value from the last argument is used. 615 | 616 | >>> dictadd({1: 0, 2: 0}, {2: 1, 3: 1}) 617 | {1: 0, 2: 1, 3: 1} 618 | """ 619 | result = {} 620 | for dct in dicts: 621 | result.update(dct) 622 | return result 623 | 624 | def listget(lst, ind, default=None): 625 | """ 626 | Returns `lst[ind]` if it exists, `default` otherwise. 627 | 628 | >>> listget(['a'], 0) 629 | 'a' 630 | >>> listget(['a'], 1) 631 | >>> listget(['a'], 1, 'b') 632 | 'b' 633 | """ 634 | if len(lst)-1 < ind: 635 | return default 636 | return lst[ind] 637 | 638 | def intget(integer, default=None): 639 | """ 640 | Returns `integer` as an int or `default` if it can't. 641 | 642 | >>> intget('3') 643 | 3 644 | >>> intget('3a') 645 | >>> intget('3a', 0) 646 | 0 647 | """ 648 | try: 649 | return int(integer) 650 | except (TypeError, ValueError): 651 | return default 652 | 653 | def datestr(then, now=None): 654 | """ 655 | Converts a (UTC) datetime object to a nice string representation. 656 | 657 | >>> from datetime import datetime, timedelta 658 | >>> d = datetime(1970, 5, 1) 659 | >>> datestr(d, now=d) 660 | '0 microseconds ago' 661 | >>> for t, v in { 662 | ... timedelta(microseconds=1): '1 microsecond ago', 663 | ... timedelta(microseconds=2): '2 microseconds ago', 664 | ... -timedelta(microseconds=1): '1 microsecond from now', 665 | ... -timedelta(microseconds=2): '2 microseconds from now', 666 | ... timedelta(microseconds=2000): '2 milliseconds ago', 667 | ... timedelta(seconds=2): '2 seconds ago', 668 | ... timedelta(seconds=2*60): '2 minutes ago', 669 | ... timedelta(seconds=2*60*60): '2 hours ago', 670 | ... timedelta(days=2): '2 days ago', 671 | ... }.iteritems(): 672 | ... assert datestr(d, now=d+t) == v 673 | >>> datestr(datetime(1970, 1, 1), now=d) 674 | 'January 1' 675 | >>> datestr(datetime(1969, 1, 1), now=d) 676 | 'January 1, 1969' 677 | >>> datestr(datetime(1970, 6, 1), now=d) 678 | 'June 1, 1970' 679 | >>> datestr(None) 680 | '' 681 | """ 682 | def agohence(n, what, divisor=None): 683 | if divisor: n = n // divisor 684 | 685 | out = str(abs(n)) + ' ' + what # '2 day' 686 | if abs(n) != 1: out += 's' # '2 days' 687 | out += ' ' # '2 days ' 688 | if n < 0: 689 | out += 'from now' 690 | else: 691 | out += 'ago' 692 | return out # '2 days ago' 693 | 694 | oneday = 24 * 60 * 60 695 | 696 | if not then: return "" 697 | if not now: now = datetime.datetime.utcnow() 698 | if type(now).__name__ == "DateTime": 699 | now = datetime.datetime.fromtimestamp(now) 700 | if type(then).__name__ == "DateTime": 701 | then = datetime.datetime.fromtimestamp(then) 702 | elif type(then).__name__ == "date": 703 | then = datetime.datetime(then.year, then.month, then.day) 704 | 705 | delta = now - then 706 | deltaseconds = int(delta.days * oneday + delta.seconds + delta.microseconds * 1e-06) 707 | deltadays = abs(deltaseconds) // oneday 708 | if deltaseconds < 0: deltadays *= -1 # fix for oddity of floor 709 | 710 | if deltadays: 711 | if abs(deltadays) < 4: 712 | return agohence(deltadays, 'day') 713 | 714 | out = then.strftime('%B %e') # e.g. 'June 13' 715 | if then.year != now.year or deltadays < 0: 716 | out += ', %s' % then.year 717 | return out 718 | 719 | if int(deltaseconds): 720 | if abs(deltaseconds) > (60 * 60): 721 | return agohence(deltaseconds, 'hour', 60 * 60) 722 | elif abs(deltaseconds) > 60: 723 | return agohence(deltaseconds, 'minute', 60) 724 | else: 725 | return agohence(deltaseconds, 'second') 726 | 727 | deltamicroseconds = delta.microseconds 728 | if delta.days: deltamicroseconds = int(delta.microseconds - 1e6) # datetime oddity 729 | if abs(deltamicroseconds) > 1000: 730 | return agohence(deltamicroseconds, 'millisecond', 1000) 731 | 732 | return agohence(deltamicroseconds, 'microsecond') 733 | 734 | def numify(string): 735 | """ 736 | Removes all non-digit characters from `string`. 737 | 738 | >>> numify('800-555-1212') 739 | '8005551212' 740 | >>> numify('800.555.1212') 741 | '8005551212' 742 | 743 | """ 744 | return ''.join([c for c in str(string) if c.isdigit()]) 745 | 746 | def denumify(string, pattern): 747 | """ 748 | Formats `string` according to `pattern`, where the letter X gets replaced 749 | by characters from `string`. 750 | 751 | >>> denumify("8005551212", "(XXX) XXX-XXXX") 752 | '(800) 555-1212' 753 | 754 | """ 755 | out = [] 756 | for c in pattern: 757 | if c == "X": 758 | out.append(string[0]) 759 | string = string[1:] 760 | else: 761 | out.append(c) 762 | return ''.join(out) 763 | 764 | def commify(n): 765 | """ 766 | Add commas to an integer `n`. 767 | 768 | >>> commify(1) 769 | '1' 770 | >>> commify(123) 771 | '123' 772 | >>> commify(1234) 773 | '1,234' 774 | >>> commify(1234567890) 775 | '1,234,567,890' 776 | >>> commify(123.0) 777 | '123.0' 778 | >>> commify(1234.5) 779 | '1,234.5' 780 | >>> commify(1234.56789) 781 | '1,234.56789' 782 | >>> commify('%.2f' % 1234.5) 783 | '1,234.50' 784 | >>> commify(None) 785 | >>> 786 | 787 | """ 788 | if n is None: return None 789 | n = str(n) 790 | if '.' in n: 791 | dollars, cents = n.split('.') 792 | else: 793 | dollars, cents = n, None 794 | 795 | r = [] 796 | for i, c in enumerate(str(dollars)[::-1]): 797 | if i and (not (i % 3)): 798 | r.insert(0, ',') 799 | r.insert(0, c) 800 | out = ''.join(r) 801 | if cents: 802 | out += '.' + cents 803 | return out 804 | 805 | def dateify(datestring): 806 | """ 807 | Formats a numified `datestring` properly. 808 | """ 809 | return denumify(datestring, "XXXX-XX-XX XX:XX:XX") 810 | 811 | 812 | def nthstr(n): 813 | """ 814 | Formats an ordinal. 815 | Doesn't handle negative numbers. 816 | 817 | >>> nthstr(1) 818 | '1st' 819 | >>> nthstr(0) 820 | '0th' 821 | >>> [nthstr(x) for x in [2, 3, 4, 5, 10, 11, 12, 13, 14, 15]] 822 | ['2nd', '3rd', '4th', '5th', '10th', '11th', '12th', '13th', '14th', '15th'] 823 | >>> [nthstr(x) for x in [91, 92, 93, 94, 99, 100, 101, 102]] 824 | ['91st', '92nd', '93rd', '94th', '99th', '100th', '101st', '102nd'] 825 | >>> [nthstr(x) for x in [111, 112, 113, 114, 115]] 826 | ['111th', '112th', '113th', '114th', '115th'] 827 | 828 | """ 829 | 830 | assert n >= 0 831 | if n % 100 in [11, 12, 13]: return '%sth' % n 832 | return {1: '%sst', 2: '%snd', 3: '%srd'}.get(n % 10, '%sth') % n 833 | 834 | def cond(predicate, consequence, alternative=None): 835 | """ 836 | Function replacement for if-else to use in expressions. 837 | 838 | >>> x = 2 839 | >>> cond(x % 2 == 0, "even", "odd") 840 | 'even' 841 | >>> cond(x % 2 == 0, "even", "odd") + '_row' 842 | 'even_row' 843 | """ 844 | if predicate: 845 | return consequence 846 | else: 847 | return alternative 848 | 849 | class CaptureStdout: 850 | """ 851 | Captures everything `func` prints to stdout and returns it instead. 852 | 853 | >>> def idiot(): 854 | ... print "foo" 855 | >>> capturestdout(idiot)() 856 | 'foo\\n' 857 | 858 | **WARNING:** Not threadsafe! 859 | """ 860 | def __init__(self, func): 861 | self.func = func 862 | def __call__(self, *args, **keywords): 863 | from cStringIO import StringIO 864 | # Not threadsafe! 865 | out = StringIO() 866 | oldstdout = sys.stdout 867 | sys.stdout = out 868 | try: 869 | self.func(*args, **keywords) 870 | finally: 871 | sys.stdout = oldstdout 872 | return out.getvalue() 873 | 874 | capturestdout = CaptureStdout 875 | 876 | class Profile: 877 | """ 878 | Profiles `func` and returns a tuple containing its output 879 | and a string with human-readable profiling information. 880 | 881 | >>> import time 882 | >>> out, inf = profile(time.sleep)(.001) 883 | >>> out 884 | >>> inf[:10].strip() 885 | 'took 0.0' 886 | """ 887 | def __init__(self, func): 888 | self.func = func 889 | def __call__(self, *args): ##, **kw): kw unused 890 | import hotshot, hotshot.stats, os, tempfile ##, time already imported 891 | f, filename = tempfile.mkstemp() 892 | os.close(f) 893 | 894 | prof = hotshot.Profile(filename) 895 | 896 | stime = time.time() 897 | result = prof.runcall(self.func, *args) 898 | stime = time.time() - stime 899 | prof.close() 900 | 901 | import cStringIO 902 | out = cStringIO.StringIO() 903 | stats = hotshot.stats.load(filename) 904 | stats.stream = out 905 | stats.strip_dirs() 906 | stats.sort_stats('time', 'calls') 907 | stats.print_stats(40) 908 | stats.print_callers() 909 | 910 | x = '\n\ntook '+ str(stime) + ' seconds\n' 911 | x += out.getvalue() 912 | 913 | # remove the tempfile 914 | try: 915 | os.remove(filename) 916 | except IOError: 917 | pass 918 | 919 | return result, x 920 | 921 | profile = Profile 922 | 923 | 924 | import traceback 925 | # hack for compatibility with Python 2.3: 926 | if not hasattr(traceback, 'format_exc'): 927 | from cStringIO import StringIO 928 | def format_exc(limit=None): 929 | strbuf = StringIO() 930 | traceback.print_exc(limit, strbuf) 931 | return strbuf.getvalue() 932 | traceback.format_exc = format_exc 933 | 934 | def tryall(context, prefix=None): 935 | """ 936 | Tries a series of functions and prints their results. 937 | `context` is a dictionary mapping names to values; 938 | the value will only be tried if it's callable. 939 | 940 | >>> tryall(dict(j=lambda: True)) 941 | j: True 942 | ---------------------------------------- 943 | results: 944 | True: 1 945 | 946 | For example, you might have a file `test/stuff.py` 947 | with a series of functions testing various things in it. 948 | At the bottom, have a line: 949 | 950 | if __name__ == "__main__": tryall(globals()) 951 | 952 | Then you can run `python test/stuff.py` and get the results of 953 | all the tests. 954 | """ 955 | context = context.copy() # vars() would update 956 | results = {} 957 | for (key, value) in context.iteritems(): 958 | if not hasattr(value, '__call__'): 959 | continue 960 | if prefix and not key.startswith(prefix): 961 | continue 962 | print key + ':', 963 | try: 964 | r = value() 965 | dictincr(results, r) 966 | print r 967 | except: 968 | print 'ERROR' 969 | dictincr(results, 'ERROR') 970 | print ' ' + '\n '.join(traceback.format_exc().split('\n')) 971 | 972 | print '-'*40 973 | print 'results:' 974 | for (key, value) in results.iteritems(): 975 | print ' '*2, str(key)+':', value 976 | 977 | class ThreadedDict: 978 | """ 979 | Thread local storage. 980 | 981 | >>> d = ThreadedDict() 982 | >>> d.x = 1 983 | >>> d.x 984 | 1 985 | >>> import threading 986 | >>> def f(): d.x = 2 987 | ... 988 | >>> t = threading.Thread(target=f) 989 | >>> t.start() 990 | >>> t.join() 991 | >>> d.x 992 | 1 993 | """ 994 | def __getattr__(self, key): 995 | return getattr(self._getd(), key) 996 | 997 | def __setattr__(self, key, value): 998 | return setattr(self._getd(), key, value) 999 | 1000 | def __delattr__(self, key): 1001 | return delattr(self._getd(), key) 1002 | 1003 | def __hash__(self): 1004 | return id(self) 1005 | 1006 | def _getd(self): 1007 | t = threading.currentThread() 1008 | if not hasattr(t, '_d'): 1009 | # using __dict__ of thread as thread local storage 1010 | t._d = {} 1011 | 1012 | # there could be multiple instances of ThreadedDict. 1013 | # use self as key 1014 | if self not in t._d: 1015 | t._d[self] = storage() 1016 | return t._d[self] 1017 | 1018 | threadeddict = ThreadedDict 1019 | 1020 | def autoassign(self, locals): 1021 | """ 1022 | Automatically assigns local variables to `self`. 1023 | 1024 | >>> self = storage() 1025 | >>> autoassign(self, dict(a=1, b=2)) 1026 | >>> self 1027 | 1028 | 1029 | Generally used in `__init__` methods, as in: 1030 | 1031 | def __init__(self, foo, bar, baz=1): autoassign(self, locals()) 1032 | """ 1033 | for (key, value) in locals.iteritems(): 1034 | if key == 'self': 1035 | continue 1036 | setattr(self, key, value) 1037 | 1038 | def to36(q): 1039 | """ 1040 | Converts an integer to base 36 (a useful scheme for human-sayable IDs). 1041 | 1042 | >>> to36(35) 1043 | 'z' 1044 | >>> to36(119292) 1045 | '2k1o' 1046 | >>> int(to36(939387374), 36) 1047 | 939387374 1048 | >>> to36(0) 1049 | '0' 1050 | >>> to36(-393) 1051 | Traceback (most recent call last): 1052 | ... 1053 | ValueError: must supply a positive integer 1054 | 1055 | """ 1056 | if q < 0: raise ValueError, "must supply a positive integer" 1057 | letters = "0123456789abcdefghijklmnopqrstuvwxyz" 1058 | converted = [] 1059 | while q != 0: 1060 | q, r = divmod(q, 36) 1061 | converted.insert(0, letters[r]) 1062 | return "".join(converted) or '0' 1063 | 1064 | 1065 | r_url = re_compile('(?', text) 1079 | text = markdown(text) 1080 | return text 1081 | 1082 | def sendmail(from_address, to_address, subject, message, headers=None, **kw): 1083 | """ 1084 | Sends the email message `message` with mail and envelope headers 1085 | for from `from_address_` to `to_address` with `subject`. 1086 | Additional email headers can be specified with the dictionary 1087 | `headers. 1088 | 1089 | If `web.config.smtp_server` is set, it will send the message 1090 | to that SMTP server. Otherwise it will look for 1091 | `/usr/sbin/sendmail`, the typical location for the sendmail-style 1092 | binary. To use sendmail from a different path, set `web.config.sendmail_path`. 1093 | """ 1094 | try: 1095 | import webapi 1096 | except ImportError: 1097 | webapi = Storage(config=Storage()) 1098 | 1099 | if headers is None: headers = {} 1100 | 1101 | cc = kw.get('cc', []) 1102 | bcc = kw.get('bcc', []) 1103 | 1104 | def listify(x): 1105 | if not isinstance(x, list): 1106 | return [safestr(x)] 1107 | else: 1108 | return [safestr(a) for a in x] 1109 | 1110 | from_address = safestr(from_address) 1111 | 1112 | to_address = listify(to_address) 1113 | cc = listify(cc) 1114 | bcc = listify(bcc) 1115 | 1116 | recipients = to_address + cc + bcc 1117 | 1118 | headers = dictadd({ 1119 | 'MIME-Version': '1.0', 1120 | 'Content-Type': 'text/plain; charset=UTF-8', 1121 | 'Content-Disposition': 'inline', 1122 | 'From': from_address, 1123 | 'To': ", ".join(to_address), 1124 | 'Subject': subject 1125 | }, headers) 1126 | 1127 | if cc: 1128 | headers['Cc'] = ", ".join(cc) 1129 | 1130 | import email.Utils 1131 | from_address = email.Utils.parseaddr(from_address)[1] 1132 | recipients = [email.Utils.parseaddr(r)[1] for r in recipients] 1133 | message = ('\n'.join([safestr('%s: %s' % x) for x in headers.iteritems()]) 1134 | + "\n\n" + safestr(message)) 1135 | 1136 | if webapi.config.get('smtp_server'): 1137 | server = webapi.config.get('smtp_server') 1138 | port = webapi.config.get('smtp_port', 0) 1139 | username = webapi.config.get('smtp_username') 1140 | password = webapi.config.get('smtp_password') 1141 | debug_level = webapi.config.get('smtp_debuglevel', None) 1142 | starttls = webapi.config.get('smtp_starttls', False) 1143 | 1144 | import smtplib 1145 | smtpserver = smtplib.SMTP(server, port) 1146 | 1147 | if debug_level: 1148 | smtpserver.set_debuglevel(debug_level) 1149 | 1150 | if starttls: 1151 | smtpserver.ehlo() 1152 | smtpserver.starttls() 1153 | smtpserver.ehlo() 1154 | 1155 | if username and password: 1156 | smtpserver.login(username, password) 1157 | 1158 | smtpserver.sendmail(from_address, recipients, message) 1159 | smtpserver.quit() 1160 | else: 1161 | sendmail = webapi.config.get('sendmail_path', '/usr/sbin/sendmail') 1162 | 1163 | assert not from_address.startswith('-'), 'security' 1164 | for r in recipients: 1165 | assert not r.startswith('-'), 'security' 1166 | 1167 | cmd = [sendmail, '-f', from_address] + recipients 1168 | 1169 | if subprocess: 1170 | p = subprocess.Popen(cmd, stdin=subprocess.PIPE) 1171 | p.stdin.write(message) 1172 | p.stdin.close() 1173 | p.wait() 1174 | else: 1175 | import os 1176 | i, o = os.popen2(cmd) 1177 | i.write(message) 1178 | i.close() 1179 | o.close() 1180 | del i, o 1181 | 1182 | if __name__ == "__main__": 1183 | import doctest 1184 | doctest.testmod() 1185 | --------------------------------------------------------------------------------