├── .gitignore
├── Makefile
├── README.md
├── app-controller
├── conf
└── application.conf
├── lib
├── bottle.py
└── bottledbwrap.py
├── model.py
├── simple-example
├── tests
├── Makefile
├── test_model.py
└── test_web.py
├── views
├── index.html
├── layout.html
├── user-new.html
├── user.html
└── users.html
└── website.py
/.gitignore:
--------------------------------------------------------------------------------
1 | vim-session
2 | *.pyc
3 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | test:
2 | bash -c 'cd tests && exec make'
3 |
4 | clean:
5 | find . -type f -name \*.pyc -exec rm '{}' ';'
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Python Bottle Skeleton
2 | ======================
3 |
4 | This is a skeleton web application using the bottle framework.
5 |
6 | While bottle is an extremely simple framework, there are many things that you
7 | probably will need to figure out if developing even fairly simple websites
8 | using it. This provides many examples to get you started quickly with bottle.
9 |
10 | It includes examples of things you need to create a web application
11 | using bottle:
12 |
13 | * Form processing.
14 |
15 | * SQLAlchemy for Database.
16 |
17 | * Testing.
18 |
19 | * WSGI interface to Apache and standalone test server.
20 |
21 | * Templating including standard site page template.
22 |
23 | It does **not** include user session handling, as that varies widely by
24 | application.
25 |
26 | Quickstart Bottle Tutorial
27 | --------------------------
28 |
29 | If you just want to understand Bottle, this is probably a pretty good
30 | choice. It is as simple as possible, while also covering **all** the
31 | things you are likely need to build a (fairly) complex Bottle-powered site.
32 |
33 | To use this as a tutorial:
34 |
35 | * View and run `simple-example`. This is a small program with a single
36 | dynamic page using the date and time via a template.
37 |
38 | * Optional, requires SQLAlchemy and wtforms to be installed:
39 |
40 | * Install python-sqlalchemy python-sqlalchemy-ext python-wtforms
41 |
42 | * Run: `DBCREDENTIALSTR=sqlite:///:memory: PYTHONPATH=lib python app-controller test-server`
43 |
44 | * In your browser go to http://127.0.0.1:8080/ and click around the site
45 | to understand what it does.
46 |
47 | * Look at the code in `website.py`, particularly the sections marked by
48 | "XXX" .
49 |
50 | * Look at the tests in `tests/test_web.py` to see how tests can be
51 | performed against the web application.
52 |
53 | * Review `model.py` to see how to define a database with "SQLAlchemy
54 | declarative". Particularly review the sections marked with "XXX".
55 |
56 | * Look at the tests in `tests/test_model.py` for examples of testing the
57 | database model.
58 |
59 | * Review `app-controller` if you want to understand how the site
60 | integrates with WSGI servers, CGI servers, or the standalone test
61 | server.
62 |
63 | Quickstart Building Your Own Site
64 | ---------------------------------
65 |
66 | If you would like to use this skeleton to create a website, which is why I
67 | initially created this git repository:
68 |
69 | * If you are going to be using a database, you will need the following files:
70 |
71 | * `model.py` is the database model. Copy it and modify the sections
72 | marked with "XXX".
73 |
74 | * `tests/test_model.py` has tests of the model, you will need to
75 | modify it to test your model, then you can run it to make sure your
76 | model is as you expect.
77 |
78 | * If you are **not** going to be using a database:
79 |
80 | * Delete `model.py`, `tests/test_model.py`, and `lib/bottledbwrap.py`.
81 |
82 | * In `website.py` remove the "dbwrap" line.
83 |
84 | * Modify `website.py` to customize your routes, page processing code,
85 | and form validation and processing.
86 |
87 | * Modify and create HTML template files in the `views` subdirectory.
88 |
89 | * Run `make` to test the code.
90 |
91 | * Run: `DBCREDENTIALSTR=sqlite:///:memory: PYTHONPATH=lib python app-controller test-server`
92 |
93 | * Go to http://127.0.0.1:8080/ to test your application.
94 |
95 | * Deploy it using either the CGI or WSGI methods (discussed below).
96 |
97 | Setting Up Apache
98 | -----------------
99 |
100 | * Install mod\_wsgi and Apache (Ubuntu:`sudo apt-get install
101 | libapache2-mod-wsgi`).
102 |
103 | * Rename "conf/application.conf", update places where it has "XXX", and put
104 | it on your Apache config directory ("/etc/apache2/sites-enabled",
105 | "/etc/httpd/conf.d").
106 |
107 | To run tests:
108 |
109 | * Run "make" in the top-level directory.
110 |
111 | Structure of Project
112 | --------------------
113 |
114 | There are the following components to the project:
115 |
116 | * `simple-example`: This is the simplest example, a small stand-alone
117 | program.
118 |
119 | * `app-controller`: This is a both a WSGI app and a shell script. It can
120 | be used as your WSGI application, but it also can be run with:
121 |
122 | * "test-server" to start a stand-alone server.
123 |
124 | * "initdb" to load your base database schema,
125 |
126 | * "load-test-data" to to populate any test data you have defined in your
127 | model.
128 |
129 | * `model.py`: Your database model definition, via SQLAlchemy.
130 |
131 | * `website.py`: The website controller code. This is where most of the site
132 | programming happens, this includes the URL routing, form processing, and
133 | other code for the site.
134 |
135 | * `static`: Any files put in here are served as static content, such as your
136 | favicon. You need to define routes to these static files in `website.py`
137 |
138 | * `views`: These are your website pages. There is a site template in
139 | `layout.html`, and an example of form processing in `user-new.html`.
140 |
141 | * `lib`: Library files used by the site.
142 |
143 | * `bottle.py`: This is a version of bottle that I used to develop this
144 | skeleton, mostly as a "batteries included" move. You may want to pick
145 | up a newer copy, but this skeleton is tested against the version here.
146 |
147 | * `bottledbwrap.py`: See below in the "About bottledbwrap" section.
148 |
149 | * `tests`: Tests of the web application code, hitting it through the
150 | routed URLs in "website.py". Other code tests can be done in the normal
151 | way, see [my python-unittest-skeleton
152 | project](https://github.com/linsomniac/python-unittest-skeleton) for more
153 | code testing examples.
154 |
155 | * `conf/application.conf`: An Apache config file for serving the
156 | application.
157 |
158 | About bottledbwrap
159 | ------------------
160 |
161 | This is a library I've developed over the years for some of my database
162 | applications. It's mostly a thin layer on top of SQLAlchemy that can
163 | pull database credentials from one of several files, or some environment
164 | variables.
165 |
166 | Largely it's about storing database credentials outside of your project, so
167 | they don't get checked in to your version control or published on github.
168 |
169 | You can provide database credentials in the following ways:
170 |
171 | * In a file called "dbcredentials" in the current directory or the "conf"
172 | sub-directory.
173 |
174 | * In the file specified by the environment variable "DBCREDENTIALS".
175 |
176 | * Directly in the environment variable "DBCREDENTIALSSTR".
177 |
178 | License
179 | -------
180 |
181 | This code, with the exception of `lib/bottle.py` is entirely in the public
182 | domain.
183 |
184 | `lib/bottle.py` is a convenience copy of the Bottle library. Details on
185 | its license are available at: http://bottlepy.org/docs/dev/
186 |
--------------------------------------------------------------------------------
/app-controller:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # This is the main application controller and management script.
4 | # This can be used to interface the application to a WSGI server, CGI, a
5 | # stand-alone server on 127.0.0.1:8080, or to create the database structure
6 | # and load test data.
7 | #
8 | # NOTE: This file can probably be used without modification.
9 | #
10 | # See the README.md for more information
11 | #
12 | # Written by Sean Reifschneider , 2013
13 | #
14 | # Part of the python-bottle-skeleton project at:
15 | #
16 | # https://github.com/linsomniac/python-bottle-skeleton
17 | #
18 | # I hereby place this work, python-bottle-wrapper, into the public domain.
19 |
20 | import sys
21 | import os
22 | import website
23 |
24 |
25 | if __name__.startswith('_mod_wsgi_'):
26 | os.chdir(os.path.dirname(__file__))
27 | sys.path.append('lib')
28 | sys.path.append('.')
29 |
30 | application = website.build_application()
31 |
32 | if __name__ == '__main__':
33 | sys.path.append('lib')
34 |
35 | import model
36 | import bottle
37 |
38 | if os.environ.get('GATEWAY_INTERFACE'):
39 | # Called from CGI
40 | app = website.build_application()
41 | bottle.run(app, server=bottle.CGIServer)
42 | sys.exit(0)
43 |
44 | if 'test-server' in sys.argv[1:]:
45 | 'Run stand-alone test server'
46 | sys.path.append('tests')
47 |
48 | if os.environ.get('DBCREDENTIALSTR') == 'sqlite:///:memory:':
49 | model.initdb()
50 | model.create_sample_data()
51 | app = website.build_application()
52 | bottle.debug(True)
53 | bottle.run(app, reloader=True, host='127.0.0.1', port=8080)
54 | sys.exit(0)
55 |
56 | if 'initdb' in sys.argv[1:]:
57 | 'Run database initialization'
58 | model.initdb()
59 | sys.exit(0)
60 |
61 | if 'load-test-data' in sys.argv[1:]:
62 | 'Load test data-set'
63 | model.create_sample_data()
64 | sys.exit(0)
65 |
--------------------------------------------------------------------------------
/conf/application.conf:
--------------------------------------------------------------------------------
1 |
2 | ServerName XXX.example.com
3 |
4 | WSGIDaemonProcess XXX_APP_NAME user=www-data group=www-data processes=4 threads=1
5 | WSGIScriptAlias / /XXX/path/to/python-bottle-skeleton/app-controller
6 |
7 |
8 | WSGIProcessGroup XXX_APP_NAME
9 | WSGIApplicationGroup %{GLOBAL}
10 | Order deny,allow
11 | Allow from all
12 |
13 | # If you want HTTP Basic Auth
14 | # AuthName "XXX_APP_NAME Realm"
15 | # AuthType Basic
16 | # AuthUserFile /XXX/path/to/python-bottle-skeleton/htpasswd
17 | # require valid-user
18 |
19 |
20 |
--------------------------------------------------------------------------------
/lib/bottle.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 | """
4 | Bottle is a fast and simple micro-framework for small web applications. It
5 | offers request dispatching (Routes) with url parameter support, templates,
6 | a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and
7 | template engines - all in a single file and with no dependencies other than the
8 | Python Standard Library.
9 |
10 | Homepage and documentation: http://bottlepy.org/
11 |
12 | Copyright (c) 2012, Marcel Hellkamp.
13 | License: MIT (see LICENSE for details)
14 | """
15 |
16 | from __future__ import with_statement
17 |
18 | __author__ = 'Marcel Hellkamp'
19 | __version__ = '0.11.6'
20 | __license__ = 'MIT'
21 |
22 | # The gevent server adapter needs to patch some modules before they are imported
23 | # This is why we parse the commandline parameters here but handle them later
24 | if __name__ == '__main__':
25 | from optparse import OptionParser
26 | _cmd_parser = OptionParser(usage="usage: %prog [options] package.module:app")
27 | _opt = _cmd_parser.add_option
28 | _opt("--version", action="store_true", help="show version number.")
29 | _opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.")
30 | _opt("-s", "--server", default='wsgiref', help="use SERVER as backend.")
31 | _opt("-p", "--plugin", action="append", help="install additional plugin/s.")
32 | _opt("--debug", action="store_true", help="start server in debug mode.")
33 | _opt("--reload", action="store_true", help="auto-reload on file changes.")
34 | _cmd_options, _cmd_args = _cmd_parser.parse_args()
35 | if _cmd_options.server and _cmd_options.server.startswith('gevent'):
36 | import gevent.monkey; gevent.monkey.patch_all()
37 |
38 | import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\
39 | os, re, subprocess, sys, tempfile, threading, time, urllib, warnings
40 |
41 | from datetime import date as datedate, datetime, timedelta
42 | from tempfile import TemporaryFile
43 | from traceback import format_exc, print_exc
44 |
45 | try: from json import dumps as json_dumps, loads as json_lds
46 | except ImportError: # pragma: no cover
47 | try: from simplejson import dumps as json_dumps, loads as json_lds
48 | except ImportError:
49 | try: from django.utils.simplejson import dumps as json_dumps, loads as json_lds
50 | except ImportError:
51 | def json_dumps(data):
52 | raise ImportError("JSON support requires Python 2.6 or simplejson.")
53 | json_lds = json_dumps
54 |
55 |
56 |
57 | # We now try to fix 2.5/2.6/3.1/3.2 incompatibilities.
58 | # It ain't pretty but it works... Sorry for the mess.
59 |
60 | py = sys.version_info
61 | py3k = py >= (3,0,0)
62 | py25 = py < (2,6,0)
63 | py31 = (3,1,0) <= py < (3,2,0)
64 |
65 | # Workaround for the missing "as" keyword in py3k.
66 | def _e(): return sys.exc_info()[1]
67 |
68 | # Workaround for the "print is a keyword/function" Python 2/3 dilemma
69 | # and a fallback for mod_wsgi (resticts stdout/err attribute access)
70 | try:
71 | _stdout, _stderr = sys.stdout.write, sys.stderr.write
72 | except IOError:
73 | _stdout = lambda x: sys.stdout.write(x)
74 | _stderr = lambda x: sys.stderr.write(x)
75 |
76 | # Lots of stdlib and builtin differences.
77 | if py3k:
78 | import http.client as httplib
79 | import _thread as thread
80 | from urllib.parse import urljoin, SplitResult as UrlSplitResult
81 | from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote
82 | urlunquote = functools.partial(urlunquote, encoding='latin1')
83 | from http.cookies import SimpleCookie
84 | from collections import MutableMapping as DictMixin
85 | import pickle
86 | from io import BytesIO
87 | basestring = str
88 | unicode = str
89 | json_loads = lambda s: json_lds(touni(s))
90 | callable = lambda x: hasattr(x, '__call__')
91 | imap = map
92 | else: # 2.x
93 | import httplib
94 | import thread
95 | from urlparse import urljoin, SplitResult as UrlSplitResult
96 | from urllib import urlencode, quote as urlquote, unquote as urlunquote
97 | from Cookie import SimpleCookie
98 | from itertools import imap
99 | import cPickle as pickle
100 | from StringIO import StringIO as BytesIO
101 | if py25:
102 | msg = "Python 2.5 support may be dropped in future versions of Bottle."
103 | warnings.warn(msg, DeprecationWarning)
104 | from UserDict import DictMixin
105 | def next(it): return it.next()
106 | bytes = str
107 | else: # 2.6, 2.7
108 | from collections import MutableMapping as DictMixin
109 | json_loads = json_lds
110 |
111 | # Some helpers for string/byte handling
112 | def tob(s, enc='utf8'):
113 | return s.encode(enc) if isinstance(s, unicode) else bytes(s)
114 | def touni(s, enc='utf8', err='strict'):
115 | return s.decode(enc, err) if isinstance(s, bytes) else unicode(s)
116 | tonat = touni if py3k else tob
117 |
118 | # 3.2 fixes cgi.FieldStorage to accept bytes (which makes a lot of sense).
119 | # 3.1 needs a workaround.
120 | if py31:
121 | from io import TextIOWrapper
122 | class NCTextIOWrapper(TextIOWrapper):
123 | def close(self): pass # Keep wrapped buffer open.
124 |
125 | # File uploads (which are implemented as empty FiledStorage instances...)
126 | # have a negative truth value. That makes no sense, here is a fix.
127 | class FieldStorage(cgi.FieldStorage):
128 | def __nonzero__(self): return bool(self.list or self.file)
129 | if py3k: __bool__ = __nonzero__
130 |
131 | # A bug in functools causes it to break if the wrapper is an instance method
132 | def update_wrapper(wrapper, wrapped, *a, **ka):
133 | try: functools.update_wrapper(wrapper, wrapped, *a, **ka)
134 | except AttributeError: pass
135 |
136 |
137 |
138 | # These helpers are used at module level and need to be defined first.
139 | # And yes, I know PEP-8, but sometimes a lower-case classname makes more sense.
140 |
141 | def depr(message):
142 | warnings.warn(message, DeprecationWarning, stacklevel=3)
143 |
144 | def makelist(data): # This is just to handy
145 | if isinstance(data, (tuple, list, set, dict)): return list(data)
146 | elif data: return [data]
147 | else: return []
148 |
149 |
150 | class DictProperty(object):
151 | ''' Property that maps to a key in a local dict-like attribute. '''
152 | def __init__(self, attr, key=None, read_only=False):
153 | self.attr, self.key, self.read_only = attr, key, read_only
154 |
155 | def __call__(self, func):
156 | functools.update_wrapper(self, func, updated=[])
157 | self.getter, self.key = func, self.key or func.__name__
158 | return self
159 |
160 | def __get__(self, obj, cls):
161 | if obj is None: return self
162 | key, storage = self.key, getattr(obj, self.attr)
163 | if key not in storage: storage[key] = self.getter(obj)
164 | return storage[key]
165 |
166 | def __set__(self, obj, value):
167 | if self.read_only: raise AttributeError("Read-Only property.")
168 | getattr(obj, self.attr)[self.key] = value
169 |
170 | def __delete__(self, obj):
171 | if self.read_only: raise AttributeError("Read-Only property.")
172 | del getattr(obj, self.attr)[self.key]
173 |
174 |
175 | class cached_property(object):
176 | ''' A property that is only computed once per instance and then replaces
177 | itself with an ordinary attribute. Deleting the attribute resets the
178 | property. '''
179 |
180 | def __init__(self, func):
181 | self.func = func
182 |
183 | def __get__(self, obj, cls):
184 | if obj is None: return self
185 | value = obj.__dict__[self.func.__name__] = self.func(obj)
186 | return value
187 |
188 |
189 | class lazy_attribute(object):
190 | ''' A property that caches itself to the class object. '''
191 | def __init__(self, func):
192 | functools.update_wrapper(self, func, updated=[])
193 | self.getter = func
194 |
195 | def __get__(self, obj, cls):
196 | value = self.getter(cls)
197 | setattr(cls, self.__name__, value)
198 | return value
199 |
200 |
201 |
202 |
203 |
204 |
205 | ###############################################################################
206 | # Exceptions and Events ########################################################
207 | ###############################################################################
208 |
209 |
210 | class BottleException(Exception):
211 | """ A base class for exceptions used by bottle. """
212 | pass
213 |
214 |
215 |
216 |
217 |
218 |
219 | ###############################################################################
220 | # Routing ######################################################################
221 | ###############################################################################
222 |
223 |
224 | class RouteError(BottleException):
225 | """ This is a base class for all routing related exceptions """
226 |
227 |
228 | class RouteReset(BottleException):
229 | """ If raised by a plugin or request handler, the route is reset and all
230 | plugins are re-applied. """
231 |
232 | class RouterUnknownModeError(RouteError): pass
233 |
234 |
235 | class RouteSyntaxError(RouteError):
236 | """ The route parser found something not supported by this router """
237 |
238 |
239 | class RouteBuildError(RouteError):
240 | """ The route could not been built """
241 |
242 |
243 | class Router(object):
244 | ''' A Router is an ordered collection of route->target pairs. It is used to
245 | efficiently match WSGI requests against a number of routes and return
246 | the first target that satisfies the request. The target may be anything,
247 | usually a string, ID or callable object. A route consists of a path-rule
248 | and a HTTP method.
249 |
250 | The path-rule is either a static path (e.g. `/contact`) or a dynamic
251 | path that contains wildcards (e.g. `/wiki/`). The wildcard syntax
252 | and details on the matching order are described in docs:`routing`.
253 | '''
254 |
255 | default_pattern = '[^/]+'
256 | default_filter = 're'
257 | #: Sorry for the mess. It works. Trust me.
258 | rule_syntax = re.compile('(\\\\*)'\
259 | '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)'\
260 | '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)'\
261 | '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))')
262 |
263 | def __init__(self, strict=False):
264 | self.rules = {} # A {rule: Rule} mapping
265 | self.builder = {} # A rule/name->build_info mapping
266 | self.static = {} # Cache for static routes: {path: {method: target}}
267 | self.dynamic = [] # Cache for dynamic routes. See _compile()
268 | #: If true, static routes are no longer checked first.
269 | self.strict_order = strict
270 | self.filters = {'re': self.re_filter, 'int': self.int_filter,
271 | 'float': self.float_filter, 'path': self.path_filter}
272 |
273 | def re_filter(self, conf):
274 | return conf or self.default_pattern, None, None
275 |
276 | def int_filter(self, conf):
277 | return r'-?\d+', int, lambda x: str(int(x))
278 |
279 | def float_filter(self, conf):
280 | return r'-?[\d.]+', float, lambda x: str(float(x))
281 |
282 | def path_filter(self, conf):
283 | return r'.+?', None, None
284 |
285 | def add_filter(self, name, func):
286 | ''' Add a filter. The provided function is called with the configuration
287 | string as parameter and must return a (regexp, to_python, to_url) tuple.
288 | The first element is a string, the last two are callables or None. '''
289 | self.filters[name] = func
290 |
291 | def parse_rule(self, rule):
292 | ''' Parses a rule into a (name, filter, conf) token stream. If mode is
293 | None, name contains a static rule part. '''
294 | offset, prefix = 0, ''
295 | for match in self.rule_syntax.finditer(rule):
296 | prefix += rule[offset:match.start()]
297 | g = match.groups()
298 | if len(g[0])%2: # Escaped wildcard
299 | prefix += match.group(0)[len(g[0]):]
300 | offset = match.end()
301 | continue
302 | if prefix: yield prefix, None, None
303 | name, filtr, conf = g[1:4] if not g[2] is None else g[4:7]
304 | if not filtr: filtr = self.default_filter
305 | yield name, filtr, conf or None
306 | offset, prefix = match.end(), ''
307 | if offset <= len(rule) or prefix:
308 | yield prefix+rule[offset:], None, None
309 |
310 | def add(self, rule, method, target, name=None):
311 | ''' Add a new route or replace the target for an existing route. '''
312 | if rule in self.rules:
313 | self.rules[rule][method] = target
314 | if name: self.builder[name] = self.builder[rule]
315 | return
316 |
317 | target = self.rules[rule] = {method: target}
318 |
319 | # Build pattern and other structures for dynamic routes
320 | anons = 0 # Number of anonymous wildcards
321 | pattern = '' # Regular expression pattern
322 | filters = [] # Lists of wildcard input filters
323 | builder = [] # Data structure for the URL builder
324 | is_static = True
325 | for key, mode, conf in self.parse_rule(rule):
326 | if mode:
327 | is_static = False
328 | mask, in_filter, out_filter = self.filters[mode](conf)
329 | if key:
330 | pattern += '(?P<%s>%s)' % (key, mask)
331 | else:
332 | pattern += '(?:%s)' % mask
333 | key = 'anon%d' % anons; anons += 1
334 | if in_filter: filters.append((key, in_filter))
335 | builder.append((key, out_filter or str))
336 | elif key:
337 | pattern += re.escape(key)
338 | builder.append((None, key))
339 | self.builder[rule] = builder
340 | if name: self.builder[name] = builder
341 |
342 | if is_static and not self.strict_order:
343 | self.static[self.build(rule)] = target
344 | return
345 |
346 | def fpat_sub(m):
347 | return m.group(0) if len(m.group(1)) % 2 else m.group(1) + '(?:'
348 | flat_pattern = re.sub(r'(\\*)(\(\?P<[^>]*>|\((?!\?))', fpat_sub, pattern)
349 |
350 | try:
351 | re_match = re.compile('^(%s)$' % pattern).match
352 | except re.error:
353 | raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, _e()))
354 |
355 | def match(path):
356 | """ Return an url-argument dictionary. """
357 | url_args = re_match(path).groupdict()
358 | for name, wildcard_filter in filters:
359 | try:
360 | url_args[name] = wildcard_filter(url_args[name])
361 | except ValueError:
362 | raise HTTPError(400, 'Path has wrong format.')
363 | return url_args
364 |
365 | try:
366 | combined = '%s|(^%s$)' % (self.dynamic[-1][0].pattern, flat_pattern)
367 | self.dynamic[-1] = (re.compile(combined), self.dynamic[-1][1])
368 | self.dynamic[-1][1].append((match, target))
369 | except (AssertionError, IndexError): # AssertionError: Too many groups
370 | self.dynamic.append((re.compile('(^%s$)' % flat_pattern),
371 | [(match, target)]))
372 | return match
373 |
374 | def build(self, _name, *anons, **query):
375 | ''' Build an URL by filling the wildcards in a rule. '''
376 | builder = self.builder.get(_name)
377 | if not builder: raise RouteBuildError("No route with that name.", _name)
378 | try:
379 | for i, value in enumerate(anons): query['anon%d'%i] = value
380 | url = ''.join([f(query.pop(n)) if n else f for (n,f) in builder])
381 | return url if not query else url+'?'+urlencode(query)
382 | except KeyError:
383 | raise RouteBuildError('Missing URL argument: %r' % _e().args[0])
384 |
385 | def match(self, environ):
386 | ''' Return a (target, url_agrs) tuple or raise HTTPError(400/404/405). '''
387 | path, targets, urlargs = environ['PATH_INFO'] or '/', None, {}
388 | if path in self.static:
389 | targets = self.static[path]
390 | else:
391 | for combined, rules in self.dynamic:
392 | match = combined.match(path)
393 | if not match: continue
394 | getargs, targets = rules[match.lastindex - 1]
395 | urlargs = getargs(path) if getargs else {}
396 | break
397 |
398 | if not targets:
399 | raise HTTPError(404, "Not found: " + repr(environ['PATH_INFO']))
400 | method = environ['REQUEST_METHOD'].upper()
401 | if method in targets:
402 | return targets[method], urlargs
403 | if method == 'HEAD' and 'GET' in targets:
404 | return targets['GET'], urlargs
405 | if 'ANY' in targets:
406 | return targets['ANY'], urlargs
407 | allowed = [verb for verb in targets if verb != 'ANY']
408 | if 'GET' in allowed and 'HEAD' not in allowed:
409 | allowed.append('HEAD')
410 | raise HTTPError(405, "Method not allowed.", Allow=",".join(allowed))
411 |
412 |
413 | class Route(object):
414 | ''' This class wraps a route callback along with route specific metadata and
415 | configuration and applies Plugins on demand. It is also responsible for
416 | turing an URL path rule into a regular expression usable by the Router.
417 | '''
418 |
419 | def __init__(self, app, rule, method, callback, name=None,
420 | plugins=None, skiplist=None, **config):
421 | #: The application this route is installed to.
422 | self.app = app
423 | #: The path-rule string (e.g. ``/wiki/:page``).
424 | self.rule = rule
425 | #: The HTTP method as a string (e.g. ``GET``).
426 | self.method = method
427 | #: The original callback with no plugins applied. Useful for introspection.
428 | self.callback = callback
429 | #: The name of the route (if specified) or ``None``.
430 | self.name = name or None
431 | #: A list of route-specific plugins (see :meth:`Bottle.route`).
432 | self.plugins = plugins or []
433 | #: A list of plugins to not apply to this route (see :meth:`Bottle.route`).
434 | self.skiplist = skiplist or []
435 | #: Additional keyword arguments passed to the :meth:`Bottle.route`
436 | #: decorator are stored in this dictionary. Used for route-specific
437 | #: plugin configuration and meta-data.
438 | self.config = ConfigDict(config)
439 |
440 | def __call__(self, *a, **ka):
441 | depr("Some APIs changed to return Route() instances instead of"\
442 | " callables. Make sure to use the Route.call method and not to"\
443 | " call Route instances directly.")
444 | return self.call(*a, **ka)
445 |
446 | @cached_property
447 | def call(self):
448 | ''' The route callback with all plugins applied. This property is
449 | created on demand and then cached to speed up subsequent requests.'''
450 | return self._make_callback()
451 |
452 | def reset(self):
453 | ''' Forget any cached values. The next time :attr:`call` is accessed,
454 | all plugins are re-applied. '''
455 | self.__dict__.pop('call', None)
456 |
457 | def prepare(self):
458 | ''' Do all on-demand work immediately (useful for debugging).'''
459 | self.call
460 |
461 | @property
462 | def _context(self):
463 | depr('Switch to Plugin API v2 and access the Route object directly.')
464 | return dict(rule=self.rule, method=self.method, callback=self.callback,
465 | name=self.name, app=self.app, config=self.config,
466 | apply=self.plugins, skip=self.skiplist)
467 |
468 | def all_plugins(self):
469 | ''' Yield all Plugins affecting this route. '''
470 | unique = set()
471 | for p in reversed(self.app.plugins + self.plugins):
472 | if True in self.skiplist: break
473 | name = getattr(p, 'name', False)
474 | if name and (name in self.skiplist or name in unique): continue
475 | if p in self.skiplist or type(p) in self.skiplist: continue
476 | if name: unique.add(name)
477 | yield p
478 |
479 | def _make_callback(self):
480 | callback = self.callback
481 | for plugin in self.all_plugins():
482 | try:
483 | if hasattr(plugin, 'apply'):
484 | api = getattr(plugin, 'api', 1)
485 | context = self if api > 1 else self._context
486 | callback = plugin.apply(callback, context)
487 | else:
488 | callback = plugin(callback)
489 | except RouteReset: # Try again with changed configuration.
490 | return self._make_callback()
491 | if not callback is self.callback:
492 | update_wrapper(callback, self.callback)
493 | return callback
494 |
495 | def __repr__(self):
496 | return '<%s %r %r>' % (self.method, self.rule, self.callback)
497 |
498 |
499 |
500 |
501 |
502 |
503 | ###############################################################################
504 | # Application Object ###########################################################
505 | ###############################################################################
506 |
507 |
508 | class Bottle(object):
509 | """ Each Bottle object represents a single, distinct web application and
510 | consists of routes, callbacks, plugins, resources and configuration.
511 | Instances are callable WSGI applications.
512 |
513 | :param catchall: If true (default), handle all exceptions. Turn off to
514 | let debugging middleware handle exceptions.
515 | """
516 |
517 | def __init__(self, catchall=True, autojson=True):
518 | #: If true, most exceptions are caught and returned as :exc:`HTTPError`
519 | self.catchall = catchall
520 |
521 | #: A :class:`ResourceManager` for application files
522 | self.resources = ResourceManager()
523 |
524 | #: A :class:`ConfigDict` for app specific configuration.
525 | self.config = ConfigDict()
526 | self.config.autojson = autojson
527 |
528 | self.routes = [] # List of installed :class:`Route` instances.
529 | self.router = Router() # Maps requests to :class:`Route` instances.
530 | self.error_handler = {}
531 |
532 | # Core plugins
533 | self.plugins = [] # List of installed plugins.
534 | self.hooks = HooksPlugin()
535 | self.install(self.hooks)
536 | if self.config.autojson:
537 | self.install(JSONPlugin())
538 | self.install(TemplatePlugin())
539 |
540 |
541 | def mount(self, prefix, app, **options):
542 | ''' Mount an application (:class:`Bottle` or plain WSGI) to a specific
543 | URL prefix. Example::
544 |
545 | root_app.mount('/admin/', admin_app)
546 |
547 | :param prefix: path prefix or `mount-point`. If it ends in a slash,
548 | that slash is mandatory.
549 | :param app: an instance of :class:`Bottle` or a WSGI application.
550 |
551 | All other parameters are passed to the underlying :meth:`route` call.
552 | '''
553 | if isinstance(app, basestring):
554 | prefix, app = app, prefix
555 | depr('Parameter order of Bottle.mount() changed.') # 0.10
556 |
557 | segments = [p for p in prefix.split('/') if p]
558 | if not segments: raise ValueError('Empty path prefix.')
559 | path_depth = len(segments)
560 |
561 | def mountpoint_wrapper():
562 | try:
563 | request.path_shift(path_depth)
564 | rs = HTTPResponse([])
565 | def start_response(status, headerlist):
566 | rs.status = status
567 | for name, value in headerlist: rs.add_header(name, value)
568 | return rs.body.append
569 | body = app(request.environ, start_response)
570 | if body and rs.body: body = itertools.chain(rs.body, body)
571 | rs.body = body or rs.body
572 | return rs
573 | finally:
574 | request.path_shift(-path_depth)
575 |
576 | options.setdefault('skip', True)
577 | options.setdefault('method', 'ANY')
578 | options.setdefault('mountpoint', {'prefix': prefix, 'target': app})
579 | options['callback'] = mountpoint_wrapper
580 |
581 | self.route('/%s/<:re:.*>' % '/'.join(segments), **options)
582 | if not prefix.endswith('/'):
583 | self.route('/' + '/'.join(segments), **options)
584 |
585 | def merge(self, routes):
586 | ''' Merge the routes of another :class:`Bottle` application or a list of
587 | :class:`Route` objects into this application. The routes keep their
588 | 'owner', meaning that the :data:`Route.app` attribute is not
589 | changed. '''
590 | if isinstance(routes, Bottle):
591 | routes = routes.routes
592 | for route in routes:
593 | self.add_route(route)
594 |
595 | def install(self, plugin):
596 | ''' Add a plugin to the list of plugins and prepare it for being
597 | applied to all routes of this application. A plugin may be a simple
598 | decorator or an object that implements the :class:`Plugin` API.
599 | '''
600 | if hasattr(plugin, 'setup'): plugin.setup(self)
601 | if not callable(plugin) and not hasattr(plugin, 'apply'):
602 | raise TypeError("Plugins must be callable or implement .apply()")
603 | self.plugins.append(plugin)
604 | self.reset()
605 | return plugin
606 |
607 | def uninstall(self, plugin):
608 | ''' Uninstall plugins. Pass an instance to remove a specific plugin, a type
609 | object to remove all plugins that match that type, a string to remove
610 | all plugins with a matching ``name`` attribute or ``True`` to remove all
611 | plugins. Return the list of removed plugins. '''
612 | removed, remove = [], plugin
613 | for i, plugin in list(enumerate(self.plugins))[::-1]:
614 | if remove is True or remove is plugin or remove is type(plugin) \
615 | or getattr(plugin, 'name', True) == remove:
616 | removed.append(plugin)
617 | del self.plugins[i]
618 | if hasattr(plugin, 'close'): plugin.close()
619 | if removed: self.reset()
620 | return removed
621 |
622 | def run(self, **kwargs):
623 | ''' Calls :func:`run` with the same parameters. '''
624 | run(self, **kwargs)
625 |
626 | def reset(self, route=None):
627 | ''' Reset all routes (force plugins to be re-applied) and clear all
628 | caches. If an ID or route object is given, only that specific route
629 | is affected. '''
630 | if route is None: routes = self.routes
631 | elif isinstance(route, Route): routes = [route]
632 | else: routes = [self.routes[route]]
633 | for route in routes: route.reset()
634 | if DEBUG:
635 | for route in routes: route.prepare()
636 | self.hooks.trigger('app_reset')
637 |
638 | def close(self):
639 | ''' Close the application and all installed plugins. '''
640 | for plugin in self.plugins:
641 | if hasattr(plugin, 'close'): plugin.close()
642 | self.stopped = True
643 |
644 | def match(self, environ):
645 | """ Search for a matching route and return a (:class:`Route` , urlargs)
646 | tuple. The second value is a dictionary with parameters extracted
647 | from the URL. Raise :exc:`HTTPError` (404/405) on a non-match."""
648 | return self.router.match(environ)
649 |
650 | def get_url(self, routename, **kargs):
651 | """ Return a string that matches a named route """
652 | scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/'
653 | location = self.router.build(routename, **kargs).lstrip('/')
654 | return urljoin(urljoin('/', scriptname), location)
655 |
656 | def add_route(self, route):
657 | ''' Add a route object, but do not change the :data:`Route.app`
658 | attribute.'''
659 | self.routes.append(route)
660 | self.router.add(route.rule, route.method, route, name=route.name)
661 | if DEBUG: route.prepare()
662 |
663 | def route(self, path=None, method='GET', callback=None, name=None,
664 | apply=None, skip=None, **config):
665 | """ A decorator to bind a function to a request URL. Example::
666 |
667 | @app.route('/hello/:name')
668 | def hello(name):
669 | return 'Hello %s' % name
670 |
671 | The ``:name`` part is a wildcard. See :class:`Router` for syntax
672 | details.
673 |
674 | :param path: Request path or a list of paths to listen to. If no
675 | path is specified, it is automatically generated from the
676 | signature of the function.
677 | :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of
678 | methods to listen to. (default: `GET`)
679 | :param callback: An optional shortcut to avoid the decorator
680 | syntax. ``route(..., callback=func)`` equals ``route(...)(func)``
681 | :param name: The name for this route. (default: None)
682 | :param apply: A decorator or plugin or a list of plugins. These are
683 | applied to the route callback in addition to installed plugins.
684 | :param skip: A list of plugins, plugin classes or names. Matching
685 | plugins are not installed to this route. ``True`` skips all.
686 |
687 | Any additional keyword arguments are stored as route-specific
688 | configuration and passed to plugins (see :meth:`Plugin.apply`).
689 | """
690 | if callable(path): path, callback = None, path
691 | plugins = makelist(apply)
692 | skiplist = makelist(skip)
693 | def decorator(callback):
694 | # TODO: Documentation and tests
695 | if isinstance(callback, basestring): callback = load(callback)
696 | for rule in makelist(path) or yieldroutes(callback):
697 | for verb in makelist(method):
698 | verb = verb.upper()
699 | route = Route(self, rule, verb, callback, name=name,
700 | plugins=plugins, skiplist=skiplist, **config)
701 | self.add_route(route)
702 | return callback
703 | return decorator(callback) if callback else decorator
704 |
705 | def get(self, path=None, method='GET', **options):
706 | """ Equals :meth:`route`. """
707 | return self.route(path, method, **options)
708 |
709 | def post(self, path=None, method='POST', **options):
710 | """ Equals :meth:`route` with a ``POST`` method parameter. """
711 | return self.route(path, method, **options)
712 |
713 | def put(self, path=None, method='PUT', **options):
714 | """ Equals :meth:`route` with a ``PUT`` method parameter. """
715 | return self.route(path, method, **options)
716 |
717 | def delete(self, path=None, method='DELETE', **options):
718 | """ Equals :meth:`route` with a ``DELETE`` method parameter. """
719 | return self.route(path, method, **options)
720 |
721 | def error(self, code=500):
722 | """ Decorator: Register an output handler for a HTTP error code"""
723 | def wrapper(handler):
724 | self.error_handler[int(code)] = handler
725 | return handler
726 | return wrapper
727 |
728 | def hook(self, name):
729 | """ Return a decorator that attaches a callback to a hook. Three hooks
730 | are currently implemented:
731 |
732 | - before_request: Executed once before each request
733 | - after_request: Executed once after each request
734 | - app_reset: Called whenever :meth:`reset` is called.
735 | """
736 | def wrapper(func):
737 | self.hooks.add(name, func)
738 | return func
739 | return wrapper
740 |
741 | def handle(self, path, method='GET'):
742 | """ (deprecated) Execute the first matching route callback and return
743 | the result. :exc:`HTTPResponse` exceptions are caught and returned.
744 | If :attr:`Bottle.catchall` is true, other exceptions are caught as
745 | well and returned as :exc:`HTTPError` instances (500).
746 | """
747 | depr("This method will change semantics in 0.10. Try to avoid it.")
748 | if isinstance(path, dict):
749 | return self._handle(path)
750 | return self._handle({'PATH_INFO': path, 'REQUEST_METHOD': method.upper()})
751 |
752 | def default_error_handler(self, res):
753 | return tob(template(ERROR_PAGE_TEMPLATE, e=res))
754 |
755 | def _handle(self, environ):
756 | try:
757 | environ['bottle.app'] = self
758 | request.bind(environ)
759 | response.bind()
760 | route, args = self.router.match(environ)
761 | environ['route.handle'] = route
762 | environ['bottle.route'] = route
763 | environ['route.url_args'] = args
764 | return route.call(**args)
765 | except HTTPResponse:
766 | return _e()
767 | except RouteReset:
768 | route.reset()
769 | return self._handle(environ)
770 | except (KeyboardInterrupt, SystemExit, MemoryError):
771 | raise
772 | except Exception:
773 | if not self.catchall: raise
774 | stacktrace = format_exc()
775 | environ['wsgi.errors'].write(stacktrace)
776 | return HTTPError(500, "Internal Server Error", _e(), stacktrace)
777 |
778 | def _cast(self, out, peek=None):
779 | """ Try to convert the parameter into something WSGI compatible and set
780 | correct HTTP headers when possible.
781 | Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like,
782 | iterable of strings and iterable of unicodes
783 | """
784 |
785 | # Empty output is done here
786 | if not out:
787 | if 'Content-Length' not in response:
788 | response['Content-Length'] = 0
789 | return []
790 | # Join lists of byte or unicode strings. Mixed lists are NOT supported
791 | if isinstance(out, (tuple, list))\
792 | and isinstance(out[0], (bytes, unicode)):
793 | out = out[0][0:0].join(out) # b'abc'[0:0] -> b''
794 | # Encode unicode strings
795 | if isinstance(out, unicode):
796 | out = out.encode(response.charset)
797 | # Byte Strings are just returned
798 | if isinstance(out, bytes):
799 | if 'Content-Length' not in response:
800 | response['Content-Length'] = len(out)
801 | return [out]
802 | # HTTPError or HTTPException (recursive, because they may wrap anything)
803 | # TODO: Handle these explicitly in handle() or make them iterable.
804 | if isinstance(out, HTTPError):
805 | out.apply(response)
806 | out = self.error_handler.get(out.status_code, self.default_error_handler)(out)
807 | return self._cast(out)
808 | if isinstance(out, HTTPResponse):
809 | out.apply(response)
810 | return self._cast(out.body)
811 |
812 | # File-like objects.
813 | if hasattr(out, 'read'):
814 | if 'wsgi.file_wrapper' in request.environ:
815 | return request.environ['wsgi.file_wrapper'](out)
816 | elif hasattr(out, 'close') or not hasattr(out, '__iter__'):
817 | return WSGIFileWrapper(out)
818 |
819 | # Handle Iterables. We peek into them to detect their inner type.
820 | try:
821 | out = iter(out)
822 | first = next(out)
823 | while not first:
824 | first = next(out)
825 | except StopIteration:
826 | return self._cast('')
827 | except HTTPResponse:
828 | first = _e()
829 | except (KeyboardInterrupt, SystemExit, MemoryError):
830 | raise
831 | except Exception:
832 | if not self.catchall: raise
833 | first = HTTPError(500, 'Unhandled exception', _e(), format_exc())
834 |
835 | # These are the inner types allowed in iterator or generator objects.
836 | if isinstance(first, HTTPResponse):
837 | return self._cast(first)
838 | if isinstance(first, bytes):
839 | return itertools.chain([first], out)
840 | if isinstance(first, unicode):
841 | return imap(lambda x: x.encode(response.charset),
842 | itertools.chain([first], out))
843 | return self._cast(HTTPError(500, 'Unsupported response type: %s'\
844 | % type(first)))
845 |
846 | def wsgi(self, environ, start_response):
847 | """ The bottle WSGI-interface. """
848 | try:
849 | out = self._cast(self._handle(environ))
850 | # rfc2616 section 4.3
851 | if response._status_code in (100, 101, 204, 304)\
852 | or environ['REQUEST_METHOD'] == 'HEAD':
853 | if hasattr(out, 'close'): out.close()
854 | out = []
855 | start_response(response._status_line, response.headerlist)
856 | return out
857 | except (KeyboardInterrupt, SystemExit, MemoryError):
858 | raise
859 | except Exception:
860 | if not self.catchall: raise
861 | err = '
4 |
5 | %for user in users:
6 | {{user.full_name}}
7 | %end
8 |
--------------------------------------------------------------------------------
/website.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | #
3 | # This implements the website logic, this is where you would do any dynamic
4 | # programming for the site pages and render them from templaes.
5 | #
6 | # NOTE: This file will need heavy customizations. Search for "XXX".
7 | #
8 | # See the README.md for more information
9 | #
10 | # Written by Sean Reifschneider , 2013
11 | #
12 | # Part of the python-bottle-skeleton project at:
13 | #
14 | # https://github.com/linsomniac/python-bottle-skeleton
15 | #
16 | # I hereby place this work, python-bottle-wrapper, into the public domain.
17 |
18 | # XXX Remove these two lines if you aren't using a database
19 | from bottledbwrap import dbwrap
20 | import model
21 |
22 | from bottle import (view, TEMPLATE_PATH, Bottle, static_file, request,
23 | redirect, BaseTemplate)
24 |
25 | # XXX Remove these lines and the next section if you aren't processing forms
26 | from wtforms import (Form, TextField, DateTimeField, SelectField,
27 | PasswordField, validators)
28 |
29 |
30 | # XXX Form validation example
31 | class NewUserFormProcessor(Form):
32 | name = TextField('Username', [validators.Length(min=4, max=25)])
33 | full_name = TextField('Full Name', [validators.Length(min=4, max=60)])
34 | email_address = TextField(
35 | 'Email Address', [validators.Length(min=6, max=35)])
36 | password = PasswordField(
37 | 'New Password',
38 | [validators.Required(),
39 | validators.EqualTo('confirm',
40 | message='Passwords must match')
41 | ])
42 | confirm = PasswordField('Repeat Password')
43 |
44 |
45 | def build_application():
46 | # XXX Define application routes in this class
47 |
48 | app = Bottle()
49 |
50 | # Pretty much this entire function needs to be written for your
51 |
52 | BaseTemplate.defaults['app'] = app # XXX Template global variable
53 | TEMPLATE_PATH.insert(0, 'views') # XXX Location of HTML templates
54 |
55 | # XXX Routes to static content
56 | @app.route('/')
57 | @app.route('/static/')
58 | def static(path):
59 | 'Serve static content.'
60 | return static_file(path, root='static/')
61 |
62 | # XXX Index page
63 | @app.route('/', name='index') # XXX URL to page
64 | @view('index') # XXX Name of template
65 | def index():
66 | 'A simple form that shows the date'
67 |
68 | import datetime
69 |
70 | now = datetime.datetime.now()
71 |
72 | # any local variables can be used in the template
73 | return locals()
74 |
75 | # XXX User list page
76 | @app.route('/users', name='user_list') # XXX URL to page
77 | @view('users') # XXX Name of template
78 | def user_list():
79 | 'A simple page from a dabase.'
80 |
81 | db = dbwrap.session()
82 |
83 | users = db.query(model.User).order_by(model.User.name)
84 |
85 | # any local variables can be used in the template
86 | return locals()
87 |
88 | # XXX User details dynamically-generated URL
89 | @app.route('/users/', name='user') # XXX URL to page
90 | @view('user') # XXX Name of template
91 | def user_info(username):
92 | 'A simple page from a dabase.'
93 |
94 | user = model.user_by_name(username)
95 |
96 | # any local variables can be used in the template
97 | return locals()
98 |
99 | # XXX A simple form example, not used on the demo site
100 | @app.route('/form') # XXX URL to page
101 | @view('form') # XXX Name of template
102 | def static_form():
103 | 'A simple form processing example'
104 |
105 | form = NewUserFormProcessor(request.forms.decode())
106 | if request.method == 'POST' and form.validate():
107 | # XXX Do something with form fields here
108 |
109 | # if successful
110 | redirect('/users/%s' % form.name.data)
111 |
112 | # any local variables can be used in the template
113 | return locals()
114 |
115 | # XXX Create a new user, form processing, including GET and POST
116 | @app.get('/new-user', name='user_new') # XXX GET URL to page
117 | @app.post('/new-user') # XXX POST URL to page
118 | @view('user-new') # XXX Name of template
119 | def new_user():
120 | 'A sample of interacting with a form and a database.'
121 |
122 | form = NewUserFormProcessor(request.forms.decode())
123 |
124 | if request.method == 'POST' and form.validate():
125 | db = dbwrap.session()
126 |
127 | sean = model.User(
128 | full_name=form.full_name.data, name=form.name.data,
129 | email_address=form.email_address.data)
130 | db.add(sean)
131 | db.commit()
132 |
133 | redirect(app.get_url('user', username=form.name.data))
134 |
135 | # any local variables can be used in the template
136 | return locals()
137 |
138 | # REQUIRED: return the application handle herre
139 | return app
140 |
--------------------------------------------------------------------------------