├── tests ├── __init__.py ├── dummy_class.py └── test_bottle_cbv.py ├── setup.cfg ├── .flake8 ├── setup.py ├── LICENSE ├── .gitignore ├── README.rst └── bottleCBV.py /tests/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'fari' 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | ignore = W293, W503 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | BottleCBV 3 | ------------- 4 | 5 | Class based views for Bottle 6 | """ 7 | from setuptools import setup 8 | 9 | setup( 10 | name='BottleCBV', 11 | version='0.1', 12 | url='https://github.com/techchunks/bottleCBV', 13 | license='BSD', 14 | author='Technology Chunks', 15 | author_email='aedil12155@gmail.com', 16 | description='Class based views for Bottle apps', 17 | long_description="Class Based View for Bottle (Inspired by flask-classy)", 18 | py_modules=['bottleCBV'], 19 | zip_safe=False, 20 | include_package_data=True, 21 | platforms='any', 22 | install_requires=[ 23 | 'bottle==0.12.7' 24 | ], 25 | keywords=['bottle', 'bottlepy', 'class-based-view'], 26 | classifiers=[ 27 | "Programming Language :: Python", 28 | "Development Status :: 4 - Beta", 29 | "Environment :: Other Environment", 30 | "Intended Audience :: Developers", 31 | "License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)", 32 | "Operating System :: OS Independent", 33 | "Topic :: Software Development :: Libraries :: Python Modules", 34 | 'Framework :: Bottle' 35 | ], 36 | test_suite='tests' 37 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, techchunks 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | .static_storage/ 57 | .media/ 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | 107 | # General 108 | .DS_Store 109 | .AppleDouble 110 | .LSOverride 111 | 112 | # Icon must end with two \r 113 | Icon 114 | 115 | 116 | # Thumbnails 117 | ._* 118 | 119 | # Files that might appear in the root of a volume 120 | .DocumentRevisions-V100 121 | .fseventsd 122 | .Spotlight-V100 123 | .TemporaryItems 124 | .Trashes 125 | .VolumeIcon.icns 126 | .com.apple.timemachine.donotpresent 127 | 128 | # Directories potentially created on remote AFP share 129 | .AppleDB 130 | .AppleDesktop 131 | Network Trash Folder 132 | Temporary Items 133 | .apdisk 134 | 135 | # JetBrains 136 | 137 | .idea -------------------------------------------------------------------------------- /tests/dummy_class.py: -------------------------------------------------------------------------------- 1 | from bottleCBV import BottleView, route 2 | 3 | 4 | VALUE1 = "value1" 5 | 6 | 7 | def get_value(): 8 | return VALUE1 9 | 10 | 11 | def mydecorator(original_function): 12 | def new_function(*args, **kwargs): 13 | resp = original_function(*args, **kwargs) 14 | return "decorator:%s" % resp 15 | return new_function 16 | 17 | 18 | class BasicView(BottleView): 19 | 20 | def index(self): 21 | """A docstring for testing that docstrings are set""" 22 | return "Index" 23 | 24 | def get(self, obj_id): 25 | return "Get:" + obj_id 26 | 27 | def put(self, id): 28 | return "Put " + id 29 | 30 | def post(self): 31 | return "Post" 32 | 33 | def delete(self, id): 34 | return "Delete " + id 35 | 36 | def mymethod(self): 37 | return "My Method" 38 | 39 | def mymethod_args(self, p_one, p_two): 40 | return "My Method %s %s" % (p_one, p_two,) 41 | 42 | @route("/endpoint/") 43 | def mymethod_route(self): 44 | return "Custom Route" 45 | 46 | @route("/endpoint/", method=["POST", "PUT"]) 47 | def mymethod_route_post(self): 48 | from bottle import request 49 | return "Custom Route %s" % request.method.upper() 50 | 51 | @route("/route1/") 52 | @route("/route2/") 53 | def multi_routed_method(self): 54 | return "Multi Routed Method" 55 | 56 | 57 | class RouteBaseView(BottleView): 58 | base_route = "my" 59 | def index(self): 60 | return "index-route-base" 61 | 62 | 63 | class RoutePrefixView(BottleView): 64 | route_prefix = "/" 65 | def index(self): 66 | return "index-route-prefix" 67 | 68 | def post(self): 69 | return "post-route-prefix" 70 | 71 | def get(self): 72 | return "get-route-prefix" 73 | 74 | 75 | class DecoratorView(BottleView): 76 | decorators = [mydecorator] 77 | def index(self): 78 | return "index" 79 | 80 | def post(self): 81 | return "post" 82 | 83 | def get(self, val): 84 | return "get:%s" % val 85 | 86 | def myfunc(self, arg1): 87 | return "get:myfunc:%s" % arg1 88 | 89 | @route("/my-custom-route/") 90 | def my_custom_route(self): 91 | return "get:my-custom-route" 92 | 93 | class SingleDecoratorView(BottleView): 94 | 95 | def index(self): 96 | return "index" 97 | 98 | @mydecorator 99 | def post(self): 100 | return "post" 101 | 102 | def get(self, val): 103 | return "get:%s" % val 104 | 105 | -------------------------------------------------------------------------------- /tests/test_bottle_cbv.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from bottle import Bottle 4 | from nose.tools import * 5 | from webtest import TestApp 6 | 7 | from .dummy_class import BasicView, RouteBaseView, RoutePrefixView 8 | from .dummy_class import DecoratorView, SingleDecoratorView 9 | 10 | 11 | app = Bottle() 12 | BasicView.register(app) 13 | RouteBaseView.register(app) 14 | RoutePrefixView.register(app) 15 | DecoratorView.register(app) 16 | SingleDecoratorView.register(app) 17 | 18 | test_app = TestApp(app) 19 | 20 | class TestBasicView(unittest.TestCase): 21 | def test_basic_index_url(self): 22 | response = test_app.get("/basic/") 23 | eq_("Index", response.body) 24 | 25 | 26 | def test_basic_get_url(self): 27 | response = test_app.get("/basic/1234/") 28 | eq_("Get:1234", response.body) 29 | 30 | 31 | def test_basic_post_url(self): 32 | response = test_app.post("/basic/") 33 | eq_("Post", response.body) 34 | 35 | 36 | def test_mymethod_get_url(self): 37 | response = test_app.get("/basic/mymethod/") 38 | eq_("My Method", response.body) 39 | 40 | 41 | def test_mymethod_with_params_get_url(self): 42 | response = test_app.get("/basic/mymethod-args/arg1/arg2/") 43 | eq_("My Method arg1 arg2", response.body) 44 | 45 | 46 | def test_mymethod_custom_route_get(self): 47 | response = test_app.get("/endpoint/") 48 | eq_("Custom Route", response.body) 49 | 50 | 51 | def test_mymethod_custom_route_post(self): 52 | response = test_app.post("/endpoint/") 53 | eq_("Custom Route POST", response.body) 54 | response = test_app.put("/endpoint/") 55 | eq_("Custom Route PUT", response.body) 56 | 57 | 58 | def test_multi_route_method(self): 59 | response = test_app.get("/route1/") 60 | eq_("Multi Routed Method", response.body) 61 | response = test_app.get("/route2/") 62 | eq_("Multi Routed Method", response.body) 63 | 64 | 65 | class TestRouteBase(unittest.TestCase): 66 | 67 | def test_route_base(self): 68 | response = test_app.get("/my/routebase/") 69 | eq_("index-route-base", response.body) 70 | 71 | 72 | class TestRoutePrefix(unittest.TestCase): 73 | 74 | def test_route_base(self): 75 | response = test_app.get("/") 76 | eq_("index-route-prefix", response.body) 77 | 78 | 79 | class TestDecorators(unittest.TestCase): 80 | 81 | def test_route_base(self): 82 | response = test_app.get("/decorator/") 83 | eq_("decorator:index", response.body) 84 | 85 | def test_route_base_post(self): 86 | response = test_app.post("/decorator/") 87 | eq_("decorator:post", response.body) 88 | 89 | def test_route_base_get(self): 90 | response = test_app.get("/decorator/123/") 91 | eq_("decorator:get:123", response.body) 92 | 93 | def test_route_base_myfunc(self): 94 | response = test_app.get("/decorator/myfunc/123/") 95 | eq_("decorator:get:myfunc:123", response.body) 96 | 97 | def test_route_base_my_custom_route(self): 98 | response = test_app.get("/my-custom-route/") 99 | eq_("decorator:get:my-custom-route", response.body) 100 | 101 | 102 | class TestSingleDecorator(unittest.TestCase): 103 | 104 | def test_route_base(self): 105 | response = test_app.get("/singledecorator/") 106 | eq_("index", response.body) 107 | 108 | def test_route_base_post(self): 109 | response = test_app.post("/singledecorator/") 110 | eq_("decorator:post", response.body) 111 | 112 | def test_route_base_get(self): 113 | response = test_app.get("/singledecorator/123/") 114 | eq_("get:123", response.body) 115 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | BottleCBV (Bottle Class Based View) 2 | =================================== 3 | 4 | .. module:: bottleCBV 5 | 6 | `bottleCBV` is an class based view extension for bottle framework, that will automatically generate 7 | routes based on methods in the views defined (Inspired by `Flask-Classy`). 8 | 9 | Installation 10 | ------------ 11 | 12 | Install the extension with:: 13 | 14 | $ pip install bottleCBV 15 | 16 | How it works 17 | ------------ 18 | 19 | Let's see how to use it whilst building something with it. 20 | 21 | 22 | Special Methods: 23 | **************** 24 | 25 | HTTP methods below are treated as special methods, there are not registered based on the method name but HTTP method 26 | 27 | 28 | ["get", "put", "post", "delete", "index", "options"] 29 | 30 | 31 | Example: 32 | ******** 33 | For the very simple example, registering the all the routes in the class can be used as follow, 34 | :: 35 | 36 | from bottle import Bottle, run 37 | from bottleCBV import BottleView 38 | 39 | app = Bottle() 40 | 41 | class ExampleView(BottleView): 42 | 43 | def index(self): 44 | return "Index Examples" 45 | 46 | def get(self, item_key): 47 | return "Get Example %s" % item_key 48 | 49 | def post(self): 50 | return "Post Example" 51 | 52 | def put(self, item_key): 53 | return "Put Example %s" % item_key 54 | 55 | # automatically create routes for any method which is not special methods 56 | # also its smart enough to generate route based on number of arguments method takes 57 | def some_method(self, arg1, arg2) 58 | return "Get Some Method with %s and %s" % (arg1, arg2) 59 | 60 | ExampleView.register(app) 61 | # Run the app 62 | app.run(port=8080) 63 | 64 | 65 | When you register the app it will basically register following endpoints to the app 66 | 67 | :: 68 | 69 | Method: GET 70 | Endpoint: /example/ 71 | 72 | Method: GET 73 | Endpoint: /example// 74 | 75 | Method: POST 76 | Endpoint: /example/ 77 | 78 | Method: PUT 79 | Endpoint: /example// 80 | 81 | Method: 82 | Endpoint: /example/some-method/// 83 | 84 | 85 | Access them as below: 86 | 87 | :: 88 | 89 | curl -XGET "http://localhost:8080/example/" 90 | OUTPUT: `Index Examples` 91 | 92 | `curl -XGET "http://localhost:8080/example/1/"` 93 | OUTPUT: `Get Example 1` 94 | 95 | `curl -XPOST "http://localhost:8080/example/"` 96 | OUTPUT: `Post Example` 97 | 98 | `curl -XPUT "http://localhost:8080/example/1/"` 99 | OUTPUT: `Put Example 1` 100 | 101 | `curl -XGET "http://localhost:8080/example/some-method/1/2/"` 102 | OUTPUT: `Get Some Method with 1 and 2` 103 | 104 | 105 | Adding Custom Route: 106 | ******************** 107 | Custom Rule can add by using ```route``` decorator e.g, 108 | 109 | :: 110 | 111 | from bottleCBV import BottleView, route 112 | 113 | class ExampleView(BottleView): 114 | ... 115 | ... 116 | @route("/my-custom-route/", method=["GET", "POST"]) 117 | def somemethod(self): 118 | return "My Custom Route" 119 | 120 | ... 121 | ... 122 | 123 | So, now the route/rule registered for the method above will be, 124 | 125 | :: 126 | 127 | Method: GET 128 | Endpoint: /my-custom-route/ 129 | 130 | Method: POST 131 | Endpoint: /my-custom-route/ 132 | 133 | 134 | **Note**: `you can obiviously add multiple routes to one method by adding additional route decorators to it with the new route/rule` 135 | 136 | 137 | Adding decorators: 138 | ****************** 139 | To add decorator to any method you can simply use traditional way as follow, 140 | 141 | :: 142 | 143 | class ExampleView(BottleView): 144 | ... 145 | ... 146 | @mydecorator 147 | def somemethod(self): 148 | ... 149 | 150 | ... 151 | 152 | To add decorator to all the methods in the class, simple add an attribute to the class definition with a list of decorators, 153 | and that will be applied to all the methods in the class 154 | 155 | :: 156 | 157 | class ExampleView(BottleView): 158 | decorators = [mydecorator1, mydecorator2, .... ] 159 | 160 | def get(self, item_key): 161 | ... 162 | 163 | @route("/my-custom-route/", method=["GET", "POST"]) 164 | def somemethod(self): 165 | ... 166 | 167 | ... 168 | 169 | 170 | is the same as: 171 | 172 | :: 173 | 174 | class ExampleView(BottleView): 175 | 176 | @mydecorator1 177 | @mydecorator2 178 | def get(self, item_key): 179 | ... 180 | 181 | @route("/my-custom-route/", method=["GET", "POST"]) 182 | @mydecorator1 183 | @mydecorator2 184 | def somemethod(self): 185 | ... 186 | ... 187 | ... 188 | 189 | Adding Route Base Prefix: 190 | ************************* 191 | So if you want to add a base prefix to your route, it is as simple as adding a variable in your View as below, 192 | 193 | :: 194 | 195 | class ExampleView(BottleView): 196 | base_route = "/my" 197 | ... 198 | ... 199 | 200 | So, now all the routes in ExampleView will be registered as follows 201 | :: 202 | 203 | Method: GET 204 | Endpoint: /my/example/ 205 | 206 | Method: GET 207 | Endpoint: /my/example// 208 | 209 | Method: POST 210 | Endpoint: /my/example/ 211 | 212 | Method: PUT 213 | Endpoint: /my/example// 214 | 215 | 216 | Adding Route Prefix: 217 | ******************** 218 | So if you want to add a base prefix to your route, it is as simple as adding a variable in your View as below, 219 | 220 | :: 221 | 222 | class ExampleView(BottleView): 223 | route_prefix = "/custom-route" 224 | ... 225 | ... 226 | 227 | So, now all the routes in ExampleView will be registered as follows 228 | 229 | :: 230 | 231 | Method: GET 232 | Endpoint: /custom-route/ 233 | 234 | Method: GET 235 | Endpoint: /custom-route// 236 | ... 237 | ... 238 | 239 | 240 | Note: you can add both base_route and route_prefix, 241 | that will generate a combination of both e.g, /route_base/route_prefix/ 242 | 243 | -------------------------------------------------------------------------------- /bottleCBV.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | import inspect 4 | 5 | _py2 = sys.version_info[0] == 2 6 | _py3 = sys.version_info[0] == 3 7 | 8 | 9 | # noinspection PyPep8Naming 10 | class route(object): 11 | def __init__(self, rule, **options): 12 | """ 13 | Class Initializer - This will only execute if using BottleCBV's original route() style. 14 | """ 15 | 16 | # Not sure if this is needed, need to test what happens when you specify a rule but not options in BottleCBV. 17 | if not options: 18 | options = dict(method='ANY') 19 | self.rule = rule 20 | self.options = options 21 | 22 | def __call__(self, func): 23 | f = func 24 | rule = self.rule 25 | options = self.options 26 | 27 | def decorator(*_, **__): 28 | if not hasattr(f, '_rule_cache') or f._rule_cache is None: 29 | f._rule_cache = {f.__name__: [(rule, options)]} 30 | elif f.__name__ not in f._rule_cache: 31 | f._rule_cache[f.__name__] = [(rule, options)] 32 | else: 33 | f._rule_cache[f.__name__].append((rule, options)) 34 | return f 35 | 36 | return decorator() 37 | 38 | @staticmethod 39 | def decorate(f, rule, **options): 40 | if not hasattr(f, '_rule_cache') or f._rule_cache is None: 41 | f._rule_cache = {f.__name__: [(rule, options)]} 42 | elif f.__name__ not in f._rule_cache: 43 | f._rule_cache[f.__name__] = [(rule, options)] 44 | else: 45 | f._rule_cache[f.__name__].append((rule, options)) 46 | return f 47 | 48 | @staticmethod 49 | def get(rule): 50 | """ 51 | GET Method 52 | CRUD Use Case: Read 53 | Example: 54 | Request a user profile 55 | """ 56 | options = dict(method='GET') 57 | 58 | def decorator(f): 59 | return route.decorate(f, rule, **options) 60 | 61 | return decorator 62 | 63 | @staticmethod 64 | def post(rule): 65 | """ 66 | POST Method 67 | CRUD Use Case: Create 68 | Example: 69 | Create a new user 70 | """ 71 | options = dict(method='POST') 72 | 73 | def decorator(f): 74 | return route.decorate(f, rule, **options) 75 | 76 | return decorator 77 | 78 | @staticmethod 79 | def put(rule): 80 | """ 81 | PUT Method 82 | CRUD Use Case: Update / Replace 83 | Example: 84 | Set item# 4022 to Red Seedless Grapes, instead of tomatoes 85 | """ 86 | options = dict(method='PUT') 87 | 88 | def decorator(f): 89 | return route.decorate(f, rule, **options) 90 | 91 | return decorator 92 | 93 | @staticmethod 94 | def patch(rule): 95 | """ 96 | PATCH Method 97 | CRUD Use Case: Update / Modify 98 | Example: 99 | Rename then user's name from Jon to John 100 | """ 101 | options = dict(method='PATCH') 102 | 103 | def decorator(f): 104 | return route.decorate(f, rule, **options) 105 | 106 | return decorator 107 | 108 | @staticmethod 109 | def delete(rule): 110 | """ 111 | DELETE Method 112 | CRUD Use Case: Delete 113 | Example: 114 | Delete user# 12403 (John) 115 | """ 116 | options = dict(method='DELETE') 117 | 118 | def decorator(f): 119 | return route.decorate(f, rule, **options) 120 | 121 | return decorator 122 | 123 | @staticmethod 124 | def head(rule): 125 | """ 126 | HEAD Method 127 | CRUD Use Case: Read (in-part) 128 | Note: This is the same as GET, but without the response body. 129 | 130 | This is useful for items such as checking if a user exists, such as this example: 131 | Request: GET /user/12403 132 | Response: (status code) 404 - Not Found 133 | 134 | If you are closely following the REST standard, you can also verify if the requested PATCH (update) was 135 | successfully applied, in this example: 136 | Request: PUT /user/12404 { "name": "John"} 137 | Response: (status code) 304 - Not Modified 138 | """ 139 | options = dict(method='HEAD') 140 | 141 | def decorator(f): 142 | return route.decorate(f, rule, **options) 143 | 144 | return decorator 145 | 146 | @staticmethod 147 | def any(rule): 148 | """ 149 | From the Bottle Documentation: 150 | 151 | The non-standard ANY method works as a low priority fallback: Routes that listen to ANY will match requests 152 | regardless of their HTTP method but only if no other more specific route is defined. This is helpful for 153 | proxy-routes that redirect requests to more specific sub-applications. 154 | """ 155 | options = dict(method='ANY') 156 | 157 | def decorator(f): 158 | return route.decorate(f, rule, **options) 159 | 160 | return decorator 161 | 162 | 163 | class BottleView(object): 164 | """ Class based view implementation for bottle (following flask-classy architech) 165 | """ 166 | decorators = [] 167 | DEFAULT_ROUTES = ["get", "put", "post", "delete", "index", "options"] 168 | base_route = None 169 | route_prefix = None 170 | view_identifier = "view" 171 | 172 | @classmethod 173 | def register(cls, app, base_route=None, route_prefix=None): 174 | """ Register all the possible routes of the subclass 175 | :param app: bottle app instance 176 | :param base_route: prepend to the route rule (/base_route/) 177 | :param route_prefix: used when want to register custom rule, which is not class name 178 | """ 179 | if cls is BottleView: 180 | raise TypeError("cls must be a subclass of BottleView, not BottleView itself") 181 | 182 | cls._app = app 183 | cls.route_prefix = route_prefix or cls.route_prefix 184 | cls.base_route = base_route or cls.base_route 185 | # import ipdb; ipdb.set_trace() 186 | # get all the valid members of the class to register Endpoints 187 | routes = cls._get_interesting_members(BottleView) 188 | 189 | # initialize the class 190 | klass = cls() 191 | 192 | # Iterate through class members to register Endpoints 193 | for func_name, func in routes: 194 | # print "*"*50 195 | 196 | method_args = inspect.getargspec(func)[0] 197 | # Get 198 | rule = cls._build_route_rule(func_name, *method_args) 199 | method = "GET" 200 | 201 | if func_name in cls.DEFAULT_ROUTES: 202 | if func_name == "index": 203 | method = "GET" 204 | else: 205 | method = func_name.upper() 206 | 207 | # create name for endpoint 208 | endpoint = "%s:%s" % (cls.__name__, func_name) 209 | callable_method = getattr(klass, func_name) 210 | for decorator in cls.decorators: 211 | callable_method = decorator(callable_method) 212 | 213 | try: 214 | # noinspection PyProtectedMember 215 | custom_rule = func._rule_cache 216 | except AttributeError: 217 | method_args = inspect.getargspec(func)[0] 218 | rule = cls._build_route_rule(func_name, *method_args) 219 | method = "GET" 220 | 221 | if func_name in cls.DEFAULT_ROUTES: 222 | if func_name == "index": 223 | method = "GET" 224 | else: 225 | method = func_name.upper() 226 | 227 | cls._app.route(callback=callable_method, method=method, 228 | path=rule, name=endpoint) 229 | else: 230 | custom_rule_list = custom_rule.values() 231 | if _py3: 232 | custom_rule_list = list(custom_rule_list) 233 | 234 | for cached_rule in custom_rule_list[0]: 235 | rule, options = cached_rule 236 | try: 237 | method = options.pop("method") 238 | except KeyError: 239 | method = "GET" 240 | 241 | try: 242 | endpoint = options.pop("name") 243 | except KeyError: 244 | pass 245 | 246 | cls._app.route(callback=callable_method, path=rule, 247 | method=method, name=endpoint, **options) 248 | 249 | print ("%s : %s, Endpoint: %s" % (method, rule, endpoint)) 250 | 251 | @classmethod 252 | def _build_route_rule(cls, func_name, *method_args): 253 | 254 | klass_name = cls.__name__.lower() 255 | klass_name = (klass_name[:-len(cls.view_identifier)] 256 | if klass_name.endswith(cls.view_identifier) 257 | else klass_name) 258 | 259 | rule = klass_name 260 | 261 | if not (cls.base_route or cls.route_prefix): 262 | rule = klass_name 263 | elif not cls.base_route and cls.route_prefix: 264 | rule = cls.route_prefix 265 | elif cls.base_route and not cls.route_prefix: 266 | rule = "%s/%s" % (cls.base_route, klass_name) 267 | elif cls.base_route and cls.route_prefix: 268 | rule = "%s/%s" % (cls.base_route, cls.route_prefix) 269 | 270 | rule_parts = [rule] 271 | 272 | if func_name not in cls.DEFAULT_ROUTES: 273 | rule_parts.append(func_name.replace("_", "-").lower()) 274 | 275 | ignored_rule_args = ['self'] 276 | if hasattr(cls, 'base_args'): 277 | ignored_rule_args += cls.base_args 278 | 279 | for arg in method_args: 280 | if arg not in ignored_rule_args: 281 | rule_parts.append("<%s>" % arg) 282 | 283 | result = "/%s/" % join_paths(*rule_parts) 284 | result = re.sub(r'(/)\1+', r'\1', result) 285 | result = re.sub("/{2,}", "/", result) 286 | 287 | return result 288 | 289 | @classmethod 290 | def _get_interesting_members(cls, base_class): 291 | """Returns a list of methods that can be routed to""" 292 | base_members = dir(base_class) 293 | predicate = inspect.ismethod if _py2 else inspect.isfunction 294 | all_members = inspect.getmembers(cls, predicate=predicate) 295 | return [member for member in all_members 296 | if not member[0] in base_members 297 | and ((hasattr(member[1], "__self__") 298 | and not member[1].__self__ in cls.__class__.__mro__) if _py2 else True) 299 | and not member[0].startswith("_")] 300 | 301 | 302 | def join_paths(*path_pieces): 303 | """Join parts of a url path""" 304 | # Remove blank strings, and make sure everything is a string 305 | cleaned_parts = map(str, filter(None, path_pieces)) 306 | if _py3: 307 | cleaned_parts = list(cleaned_parts) 308 | 309 | return "/".join(cleaned_parts) 310 | --------------------------------------------------------------------------------