├── tests └── __init__.py ├── rails ├── models │ └── __init__.py ├── controllers │ └── __init__.py ├── response.py ├── tools.py ├── exceptions.py ├── __init__.py ├── views │ ├── base.py │ ├── jinja.py │ └── __init__.py ├── request.py ├── config.py └── router.py ├── docs ├── chapters │ ├── models.md │ ├── views.md │ ├── response.md │ ├── request.md │ ├── features.md │ └── controllers.md └── README.md ├── .gitignore ├── LICENSE ├── setup.py └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rails/models/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rails/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/chapters/models.md: -------------------------------------------------------------------------------- 1 | Models 2 | === -------------------------------------------------------------------------------- /docs/chapters/views.md: -------------------------------------------------------------------------------- 1 | Views 2 | === -------------------------------------------------------------------------------- /docs/chapters/response.md: -------------------------------------------------------------------------------- 1 | Response 2 | === -------------------------------------------------------------------------------- /rails/response.py: -------------------------------------------------------------------------------- 1 | from webob import Response as WebobResponse 2 | 3 | 4 | class Response(WebobResponse): 5 | """ 6 | Extended response object. 7 | """ 8 | pass 9 | -------------------------------------------------------------------------------- /rails/tools.py: -------------------------------------------------------------------------------- 1 | """ 2 | Set of useful tools. 3 | """ 4 | import sys 5 | 6 | 7 | def import_module(module_name): 8 | """ 9 | Improt module and return it. 10 | """ 11 | __import__(module_name) 12 | module = sys.modules[module_name] 13 | return module 14 | -------------------------------------------------------------------------------- /rails/exceptions.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class PageNotFound(Exception): 4 | 5 | def __init__(self, msg, error_code=None, **kwargs): 6 | self.msg = msg 7 | self.placeholders = kwargs 8 | 9 | def __str__(self): 10 | return self.msg.format(**self.placeholders) 11 | -------------------------------------------------------------------------------- /rails/__init__.py: -------------------------------------------------------------------------------- 1 | from wsgiref.simple_server import make_server 2 | from .router import Router 3 | 4 | 5 | def run(host='127.0.0.1', port=8000): 6 | """ 7 | Run web server. 8 | """ 9 | print("Server running on {}:{}".format(host, port)) 10 | app_router = Router() 11 | server = make_server(host, port, app_router) 12 | server.serve_forever() 13 | -------------------------------------------------------------------------------- /docs/chapters/request.md: -------------------------------------------------------------------------------- 1 | Request 2 | === 3 | 4 | 5 | **Request** - is a class that represent user request. It contain query string, request method (get, post, put, delete) and more. 6 | 7 | 8 | Methods 9 | --- 10 | 11 | - **``get_controller_name()``** `: string`. Return controller name based on query string. If controller isn't given it return 'index'. 12 | 13 | - **``get_action_name()``** `: string`. Return action name based on query string. If action name isn't given it return 'index'. 14 | 15 | - **``get_url_params()``** `: list`. Return all parameters that placed after controller and action names in url. 16 | 17 | - **`get_url_param`** `: string`. Return url parameter with given index. -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Python on Rails. Documentation 2 | === 3 | 4 | Here you can learn more about Python on Rails - from Quick Start guide to detailed description about each component of the framework. 5 | 6 | 7 | Table of content 8 | --- 9 | 10 | - [List of features](chapters/features.md) - what is different from other web frameworks 11 | - [Controllers](chapters/controllers.md) - business logic of your project. 12 | - [Models](chapters/models.md) - ORM layer between database and your code. Include data validation and HTML forms generation. 13 | - [Views](chapters/views.md) - convert models to HTML or JSON representation. Allow to generate different representation of the same dataset. 14 | - [Request](chapters/request.md) 15 | - [Response](chapters/response.md) -------------------------------------------------------------------------------- /rails/views/base.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class BaseView(object): 4 | """ 5 | Base View interface. 6 | 7 | Defines an external interface for all Views. 8 | """ 9 | _template_dir = None 10 | _engine = None 11 | 12 | def __init__(self, template_dir): 13 | self._template_dir = template_dir 14 | self._engine = self._load_engine(template_dir) 15 | 16 | def _load_engine(self, template_dir): 17 | """ 18 | Load template engine by name and return an instance. 19 | """ 20 | raise Exception("Not implemented") 21 | 22 | def render(self, template_name, variables=None): 23 | """ 24 | Render a template with the passed variables. 25 | """ 26 | raise Exception("Not implemented") 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *,cover 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | 56 | # PyBuilder 57 | target/ 58 | -------------------------------------------------------------------------------- /docs/chapters/features.md: -------------------------------------------------------------------------------- 1 | Features in PythonRails 2 | === 3 | 4 | 5 | Top features 6 | --- 7 | 8 | - [x] **Fastest way to find requested page**. Load all controllers and their actions to dict to speedup lookup of desired url address. 9 | - [x] **Dynamic url mapping**. You don't need to define url mapping in a separate place. Each url address has corresponding handler with the same name as requested in url. Now it's very easy to find the page handler in few seconds. Also it avoid problem with url overlap. 10 | 11 | 12 | Other features 13 | --- 14 | 15 | - [ ] **Middleware as expected**. When we need to do something before and after call the desired controller - we use middleware. 16 | - [ ] **Templates in python style**. It will be good to create templates without closing tags in hierarchical structure, like we do in Python code or in YAML. 17 | - [ ] **Auth out of the box**. Don't lose your time. Create new project and focus on project logic. You can login via external websites (facebook, twitter) in one click as well as via email. 18 | -------------------------------------------------------------------------------- /rails/views/jinja.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Environment 2 | from jinja2 import FileSystemLoader 3 | from .base import BaseView 4 | 5 | 6 | class JinjaView(BaseView): 7 | """ 8 | Jinja view. 9 | """ 10 | 11 | def _load_engine(self, template_dir): 12 | """ 13 | Load template engine by name and return an instance. 14 | """ 15 | return Environment(loader=FileSystemLoader(template_dir)) 16 | 17 | def render(self, template_name, variables=None): 18 | """ 19 | Render a template with the passed variables. 20 | """ 21 | if variables is None: 22 | variables = {} 23 | template = self._engine.get_template(template_name) 24 | return template.render(**variables) 25 | 26 | def render_source(self, source, variables=None): 27 | """ 28 | Render a source with the passed variables. 29 | """ 30 | if variables is None: 31 | variables = {} 32 | template = self._engine.from_string(source) 33 | return template.render(**variables) 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anton Danilchenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /rails/request.py: -------------------------------------------------------------------------------- 1 | from webob import Request as WebobRequest 2 | 3 | 4 | class Request(WebobRequest): 5 | """ 6 | Represent user request. 7 | 8 | It contain query string, request method (get, post, put, delete) and more. 9 | """ 10 | 11 | def get_controller_name(self): 12 | """ 13 | Return controller name based on query string. 14 | """ 15 | return self.path_info.strip('/').split('/', 1)[0] or 'index' 16 | 17 | def get_action_name(self): 18 | """ 19 | Return action name based on query string. 20 | """ 21 | try: 22 | return self.path_info.strip('/').split('/', 2)[1] 23 | except IndexError: 24 | return 'index' 25 | 26 | def get_url_params(self): 27 | """ 28 | Return all parameters that placed after controller and action names in url. 29 | """ 30 | return self.path_info.strip('/').split('/')[2:] 31 | 32 | def get_url_param(self, index, default=None): 33 | """ 34 | Return url parameter with given index. 35 | 36 | Args: 37 | - index: starts from zero, and come after controller and 38 | action names in url. 39 | """ 40 | params = self.get_url_params() 41 | return params[index] if index < len(params) else default 42 | 43 | @property 44 | def is_ajax(self): 45 | """ 46 | Check is it an AJAX request. 47 | """ 48 | return self.is_xhr 49 | -------------------------------------------------------------------------------- /rails/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration for a project. 3 | """ 4 | from .tools import import_module 5 | 6 | 7 | class Config(object): 8 | _data = None 9 | 10 | def __init__(self): 11 | """ 12 | Load config for a project. 13 | """ 14 | if Config._data: 15 | raise Exception('Config already loaded') 16 | Config._data = self._load_config() 17 | 18 | def _load_config(self): 19 | """ 20 | Load project's config and return dict. 21 | 22 | TODO: Convert the original dotted representation to hierarchical. 23 | """ 24 | config = import_module('config') 25 | variables = [var for var in dir(config) if not var.startswith('_')] 26 | return {var: getattr(config, var) for var in variables} 27 | 28 | @staticmethod 29 | def get(name, default=None): 30 | """ 31 | Return variable by name from the project's config. 32 | 33 | Name can be a dotted path, like: 'rails.db.type'. 34 | """ 35 | if '.' not in name: 36 | raise Exception("Config path should be divided by at least one dot") 37 | section_name, var_path = name.split('.', 1) 38 | section = Config._data.get(section_name) 39 | return section.get(var_path) 40 | 41 | 42 | def get_config(name, default=None): 43 | """ 44 | Return variable by name from the project's config. 45 | 46 | Name can be a dotted path, like: 'rails.db.type'. 47 | """ 48 | return Config.get(name, default) 49 | -------------------------------------------------------------------------------- /rails/views/__init__.py: -------------------------------------------------------------------------------- 1 | from ..tools import import_module 2 | 3 | 4 | class View(object): 5 | """ 6 | General View layer. 7 | 8 | Hides details of implementation of concrete template engine. 9 | """ 10 | _instance = None 11 | 12 | def __init__(self, template_engine_name, template_dir): 13 | # init only once 14 | if View._instance: 15 | raise Exception('Use View.render() to render template') 16 | View._instance = self._load_view(template_engine_name, template_dir) 17 | 18 | def _load_view(self, template_engine_name, template_dir): 19 | """ 20 | Load view by name and return an instance. 21 | """ 22 | file_name = template_engine_name.lower() 23 | class_name = "{}View".format(template_engine_name.title()) 24 | try: 25 | view_module = import_module("rails.views.{}".format(file_name)) 26 | except ImportError: 27 | raise Exception("Template engine '{}' not found in 'rails.views'".format(file_name)) 28 | view_class = getattr(view_module, class_name) 29 | return view_class(template_dir) 30 | 31 | @staticmethod 32 | def render(template_name, variables=None): 33 | """ 34 | Render a template with the passed variables. 35 | """ 36 | return View._instance.render(template_name, variables) 37 | 38 | 39 | def render_template(template_name, variables=None): 40 | """ 41 | Render a template with the passed variables. 42 | 43 | Used a template engine that defined in a project settings. 44 | """ 45 | return View.render(template_name, variables) 46 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Rails 3 | ==================== 4 | 5 | **Python on Rails** is a web framework with an idea to simplify web development. 6 | It's not a clone of **Ruby on Rails**. This project created to help developers to 7 | write less code that is easy maintainable. 8 | 9 | 10 | Quick start 11 | ------------- 12 | 13 | Read `Quick Start `_ on GitHub. 14 | """ 15 | 16 | try: 17 | from setuptools import setup 18 | except ImportError: 19 | from distutils.core import setup 20 | 21 | 22 | version = '0.0.5' 23 | 24 | 25 | setup( 26 | name='Rails', 27 | version=version, 28 | url='https://github.com/pythonrails/rails', 29 | license='MIT', 30 | author='Anton Danilchenko', 31 | author_email='anton@danilchenko.me', 32 | description='Rails - python web framework', 33 | keywords='rails web framework development', 34 | long_description=__doc__, 35 | classifiers=[ 36 | 'Development Status :: 3 - Alpha', 37 | 'Environment :: Web Environment', 38 | 'Intended Audience :: Developers', 39 | 'License :: OSI Approved :: MIT License', 40 | 'Operating System :: OS Independent', 41 | 'Programming Language :: Python', 42 | 'Programming Language :: Python :: 3', 43 | 'Programming Language :: Python :: 3.3', 44 | 'Programming Language :: Python :: 3.4', 45 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 46 | 'Topic :: Software Development :: Libraries :: Python Modules' 47 | ], 48 | packages=['rails', 'rails.controllers', 'rails.models', 'rails.views'], 49 | install_requires=['webob'], 50 | include_package_data=True, 51 | zip_safe=False, 52 | platforms='any' 53 | ) 54 | -------------------------------------------------------------------------------- /docs/chapters/controllers.md: -------------------------------------------------------------------------------- 1 | Controllers 2 | === 3 | 4 | **Controllers** - business logic of your project. Controller contains multiple **actions** - methods of the controller. 5 | 6 | Project contains one or multiple controllers. All controllers located in `controllers` folder. 7 | 8 | When you call a page `/blogs` the `controllers.blogs.Blogs` involved to handle this request. In this case - the `index` action will be called. When you call a page `/blogs/create` the same `Blogs` controller used, but with his `create` method. 9 | 10 | Main project controller called `Index`. When you request website - you see welcome page that generated by `Index` controller and his `index` action. If you request page that doesn't exists - the `not_found` action calls, starting from requested controller and ending search the `not_found` action in the `Index` controller. 11 | 12 | 13 | Naming 14 | --- 15 | 16 | Controller is a class that defined in the file with the same name as the controller, but in lower case. For example, the `Blogs` controller should be defined in `controllers/blogs.py` file. If you have controller with name like this `BlogArticles` than place it in file `controllers/blog_articles.py` (insert underscore for each new word in class name). 17 | 18 | Controller has **actions** - functions that defined inside the controller class. Any function that defined in the controller - is an action and can be accessed from the url address. If you need to add new funtion that sould be hidded from the public access - prefix it with underscore, for example `_get_something()`. 19 | 20 | When you need to show "change user profile" page - you need to define an action `change` inside the controller `UserProfile`. Url address for this page is `/user_profile/change/` (you can send form as POST request to this url address to handle save data action as well as handle GET request to show data). 21 | 22 | Also we have few special actions: 23 | - First one is **`index`** that called when we provide nothing in url address after the controller name. For example, requested `/blogs` page. In this case we call `Blogs` controller with `index` action. 24 | - Second one is **`not_found`** that called when requested action doesn't found inside the requested controller. 25 | 26 | Action function get only one argument - the [request](request.md) object. The `request` give you access to all data from request, including requested url address, quesry string, request method, GET or POST data and more. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Rails - Python web framework 2 | === 3 | 4 | **Python on Rails** is a web framework with an idea to simplify web development. It's not a clone of **Ruby on Rails**. This project created for **lazy developers** who like to write less code. Code that should be good structured in small and big projects. 5 | 6 | 7 | > **Do you use Django?** Look to my new project [Django(mini)](https://github.com/djangomini/djangomini) - simplified infrastrusture to create Django projects. It speed-up development process in few times. Try it. 8 | 9 | 10 | Quick start 11 | --- 12 | 13 | - create new [virtual env](https://bitbucket.org/dhellmann/virtualenvwrapper) for test project: `mkvirtualenv test_project` 14 | - install Rails with [pip](https://pypi.python.org/pypi/Rails): `pip install Rails` 15 | - clone our [test project](https://github.com/PythonRails/examples): `git clone git@github.com:PythonRails/examples.git` 16 | - open a test project and install dependencies: `cd examples/blog` and `pip install -r requirements.txt` 17 | - run project `python app.py` 18 | - check how it works, open: [127.0.0.1:8800](http://127.0.0.1:8800) 19 | 20 | Continue to read [documentation](docs) to get started. 21 | 22 | 23 | Project features 24 | --- 25 | 26 | - [x] **Flat project structure**. When you need to create new controller or model - just do it. No worry about *"In which file do I need to place this code?"*. All models place in `models` folder and all controllers create in `controllers` folder. 27 | - [x] **No routers**. With routers we can overlap some urls and have problems with accessing desired url. In our case we have simple mapping between url address and controller name and action. Access to `/users/details/15` calls a controller `Users` and an action `details()` with argument `15`. The main project controller is `Index` and can be used to render homepage with action `index()`. 28 | - [ ] **Less coupling**. You can choose any **Model backend** *(like [SQL Alchemy](http://www.sqlalchemy.org), [SQLObject](http://www.sqlobject.org), [PonyORM](https://ponyorm.com))* and any **View backend** *([Jinja2](http://jinja.pocoo.org), [Mako](http://www.makotemplates.org), [Chameleon](http://chameleon.readthedocs.org/en/latest/))*. It configurable in the project settings file. 29 | - [ ] **Middlewares**. When we need to do something before and after a call to a desired controller - we use middleware. 30 | - [ ] **Auth out of the box**. Focus on coding a new logic. You have out-of-the-box ability to login via external websites *(facebook, twitter)* in one click as well as via email + password. 31 | - [ ] *[and something else](docs/chapters/features.md)* 32 | 33 | 34 | Development 35 | --- 36 | 37 | **Have an idea?** [Create Pull Request](https://github.com/PythonRails/rails/pulls) or [Create New Issue](https://github.com/PythonRails/rails/issues). 38 | 39 | Like us on [facebook](https://www.facebook.com/PythonRails). 40 | -------------------------------------------------------------------------------- /rails/router.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import os 3 | import sys 4 | import traceback 5 | from webob.exc import HTTPNotFound, HTTPInternalServerError 6 | from .config import Config 7 | from .config import get_config 8 | from .request import Request 9 | from .response import Response 10 | from .exceptions import PageNotFound 11 | from .tools import import_module 12 | from .views import View 13 | 14 | 15 | class Router(object): 16 | """ 17 | Main project router that calls appropriate controller. 18 | 19 | TODO: 20 | - decorate each controller's call with middleware 21 | - (done) load all controllers and their actions to dict to speedup 22 | lookup of desired url address 23 | """ 24 | 25 | def __init__(self): 26 | """ 27 | Load all controllers. 28 | 29 | It allow us to speed-up get controller by given url. 30 | """ 31 | self._controllers = {} 32 | self._project_dir = os.path.dirname(os.path.realpath(sys.argv[0])) 33 | self._load_config() 34 | self._load_controllers() 35 | self._init_view() 36 | 37 | def __call__(self, environ, start_response): 38 | """ 39 | Find appropriate controller for requested address. 40 | 41 | Return Response object that support the WSGI interface. 42 | """ 43 | request = Request(environ) 44 | try: 45 | controller_name = request.get_controller_name() 46 | action_name = request.get_action_name() 47 | action_handler = self.get_action_handler(controller_name, action_name) 48 | if not callable(action_handler): 49 | # action handler should be a callable function 50 | raise PageNotFound( 51 | "Controller '{name}' doesn't have action '{action}'", 52 | name=controller_name, 53 | action=action_name 54 | ) 55 | resp = action_handler(request) 56 | if not isinstance(resp, Response): 57 | raise Exception("Controller should return Response object, but given '{}'".format(type(resp))) 58 | except PageNotFound as err: 59 | message = self._format_error_message(str(err), with_traceback=True) 60 | return HTTPNotFound(message)(environ, start_response) 61 | except Exception as err: 62 | message = self._format_error_message(str(err), with_traceback=True) 63 | return HTTPInternalServerError(message)(environ, start_response) 64 | 65 | return resp(environ, start_response) 66 | 67 | def _load_config(self): 68 | """ 69 | Load config for current project. 70 | """ 71 | self._config = Config() 72 | 73 | def _load_controllers(self): 74 | """ 75 | Load all controllers from folder 'controllers'. 76 | 77 | Ignore files with leading underscore (for example: controllers/_blogs.py) 78 | """ 79 | for file_name in os.listdir(os.path.join(self._project_dir, 'controllers')): 80 | # ignore disabled controllers 81 | if not file_name.startswith('_'): 82 | module_name = file_name.split('.', 1)[0] 83 | module_path = "controllers.{}".format(module_name) 84 | module = import_module(module_path) 85 | # transform 'blog_articles' file name to 'BlogArticles' class 86 | controller_class_name = module_name.title().replace('_', '') 87 | controller_class = getattr(module, controller_class_name) 88 | controller = controller_class() 89 | for action_name in dir(controller): 90 | action = getattr(controller, action_name) 91 | if action_name.startswith('_') or not callable(action): 92 | continue 93 | url_path = "/".join([module_name, action_name]) 94 | self._controllers[url_path] = action 95 | return self._controllers 96 | 97 | def _init_view(self): 98 | """ 99 | Initialize View with project settings. 100 | """ 101 | views_engine = get_config('rails.views.engine', 'jinja') 102 | templates_dir = os.path.join(self._project_dir, "views", "templates") 103 | self._view = View(views_engine, templates_dir) 104 | 105 | def _format_error_message(self, msg, with_traceback=False): 106 | if with_traceback: 107 | tb = traceback.format_exc() 108 | msg += "

Traceback

\n\n
{}
".format(tb) 109 | return msg 110 | 111 | def get_action_handler(self, controller_name, action_name): 112 | """ 113 | Return action of controller as callable. 114 | 115 | If requested controller isn't found - return 'not_found' action 116 | of requested controller or Index controller. 117 | """ 118 | try_actions = [ 119 | controller_name + '/' + action_name, 120 | controller_name + '/not_found', 121 | # call Index controller to catch all unhandled pages 122 | 'index/not_found' 123 | ] 124 | # search first appropriate action handler 125 | for path in try_actions: 126 | if path in self._controllers: 127 | return self._controllers[path] 128 | return None 129 | --------------------------------------------------------------------------------