天天看點

雲計算(3)- python routes URL映射管理

作者:遊蕩人生ing

概述:

Routes是python重新實作的Rails routes system,用來将urls映射到應用具體的action上。routes子產品不僅僅是在openstack中進行使用,flask架構也是對其進行了封裝。

WSGI:

Web伺服器網關接口(python Web Server Gateway Interface)是為python語言定義的web伺服器和web應用程式或架構之間的一種簡單而通用的接口。

Wsgi區分為兩個部分:伺服器和應用程式。在處理一個WSGI請求時,伺服器會為應用程式提供環境資訊及一個回調函數,當應用程式完成處理後,透過前述的回調函數,将結果回傳給伺服器。

WSGI對于app對象有以下要求:

  1. 必須是一個可調用的對象
  2. 接收兩個參數必選參數environ,start_response
  3. 傳回值必須是可疊代的,使用者表示http body。
class APPClass(object):
    def __init__(**kwargs):
        super(APPCLass, cls).__init__(**kwargs)
    def function(environ, start_response):
        pass
    def __call__(environ, start_response):
        pass           

environ包含請求的是以資訊,下面列舉必須包含的變量:

  • REQUEST_METHOD HTTP請求方法,例如GET,POST
  • SCRIPT_NAME URL路徑的起始部分對應的應用對象,如果應用程式對象對應伺服器的根,那麼這個值可以為空字元串。
  • PATH_INFO URL除淋淋起始部分後的剩餘部分,用于找到對應的應用程式對象。如果請求的路徑是根路徑,這個值為空字元串。
  • QUERY_STRING URL路徑中?後面的部分,GET的傳參
  • CONTENT_LENGTH HTTP請求中的Content-Length部分
  • SERVER_NAME,SERVER_POR
  • SERVER_PROTOCOL 用戶端使用的協定,例如HTTP/1.0,HTTP/1.1,它決定了如何處理HTTP請求的頭部。

start_response是HTTP相應的開始,被調用時伺服器檢查headers中的錯誤,禁止start_response直接将response_headers傳遞給用戶端,它必須把它們存儲起來,一直到應用程式第一次疊代傳回一個非空資料後,才能将response_headers傳遞給用戶端。

start_response(status, response_headers, exec_info=None)           
# wsgiref工具參考文檔:https://docs.python.org/zh-tw/3.11/library/wsgiref.html

from wsgiref.util import setup_testing_defaults
from wsgiref.simple_server import make_server

# A relatively simple WSGI application. It's going to print out the
# environment dictionary after being updated by setup_testing_defaults
def simple_app(environ, start_response):
    setup_testing_defaults(environ)

    status = '200 OK'
    headers = [('Content-type', 'text/plain; charset=utf-8')]

    start_response(status, headers)

    ret = [("%s: %s\n" % (key, value)).encode("utf-8")
           for key, value in environ.items()]
    return ret

with make_server('', 8000, simple_app) as httpd:
    print("Serving on port 8000...")
    httpd.serve_forever()           

WSGI,uWSGI,uwsgi三者的關系:

WSGI是一個Python Web應用程式與Web伺服器之間的接口規範,它定義了應用程式和伺服器之間的标準接口,使得應用程式可以再不同的Web伺服器上運作。

uWSGI是一個Web伺服器,用C語言編寫的Web應用程式容器。uWSGI伺服器可以作為一個獨立的應用伺服器,也可以與其他Web伺服器(比如Nginx,Apache)一起使用,通過WSGI協定與Python應用程式通信。

uwsgi是一個與uWSGI伺服器相關的協定,uwsgi協定是一種二進制協定,它定義了uWSGI伺服器與應用程式之間的通信協定。uwsgi協定允許uWSGI伺服器與應用程式支架進行雙向通信,進而提供了性能。

webob

Webob是一個封裝了WSGI的請求和應答的python庫。

Webob提供了多個對象,大部分都用來處理HTTP的請求,包括對HTTP頭的解析,内容的處理,建立WSGI的應答(包括HTTP的狀态,頭和body)等。Webob最重要的兩個類是Request和Response,Request主要用來構造和解析HTTP請求,而Response主要用來構造HTTP的應答。

def __call__(self, req, *args, **kw):
        """Call this as a WSGI application or with a request"""
        func = self.func
        if func is None:
            if args or kw:
                raise TypeError(
                    "Unbound %s can only be called with the function it "
                    "will wrap" % self.__class__.__name__)
            func = req
            return self.clone(func)
        if isinstance(req, dict):
            if len(args) != 1 or kw:
                raise TypeError(
                    "Calling %r as a WSGI app with the wrong signature" %
                    self.func)
            environ = req
            start_response = args[0]
            req = self.RequestClass(environ)  #定義RequestClass
            req.response = req.ResponseClass()   # 定義ResponseClass
            try:
                args, kw = self._prepare_args(None, None)
                resp = self.call_func(req, *args, **kw)  # 回調函數
            except HTTPException as exc:
                resp = exc
            if resp is None:
                ## FIXME: I'm not sure what this should be?
                resp = req.response
            if isinstance(resp, text_type):
                resp = bytes_(resp, req.charset)
            if isinstance(resp, bytes):
                body = resp
                resp = req.response
                resp.write(body)
            if resp is not req.response:
                resp = req.response.merge_cookies(resp)
            return resp(environ, start_response)  # 傳回資料
        else:
            args, kw = self._prepare_args(args, kw)
            return self.call_func(req, *args, **kw)           

方法介紹

routes.Mapper.connect()

routes.url()

routes.url_for()

routes.Mapper.resource()

routes.Mapper.redirect()

routes.Mapper.match()

使用方法

  • 注冊路由,路由名稱'zbj', 路徑是 '/clj', controller為 'main', action為 'index'# 比對到此條路由URL的請求:交由controller類處理,請求預調用的函數index
map.connect('zbj', '/clj', controller='main', action='index')           
  • 比對路由,match傳回比對的資料以及controller類,routematch比match多傳回route對象。
map.connect('/home/{action:index|jia}/{id:\d+}', controller='home',action='index')
res = map.match('/home/jia/200')
print(res)  # {'action': 'index', 'id': '200', 'controller': 'home'}           
  • 路由集合,具體參數使用方法自行百度
map.resource("message", 'message', controller=controller, path_prefix='/{project_id}',
             name_prefix='lala_', collection={"list_many":'GET', 'create_many':'POST'},
             member={'update_many':"POST", 'delete_many':"POST"},
            new={'preview':"POST"}, 
            parent_resource=dict(member_name='haha', collection_name='heihei'))           

範例

參考openstack nova的源碼進行梳理

def _create_controller(main_controller, action_controller_list):  # 将執行個體函數注冊到Resourced對象中
    controller = wsgi.Resource(main_controller())
    for ctl in action_controller_list:  # 如果執行個體中存在被wsgi_action裝飾的函數,将該函數屬性儲存。已備調用
        controller.register_actions(ctl())
    return controller

ROUTE_LIST = (
    ('/flavors', {
        'GET': [flavor_controller, 'index'],
        'POST': [flavor_controller, 'create']
    })
 )

 # 使用偏函數完成類的注冊
agents_controller = functools.partial(
    _create_controller, agents.AgentController, [])

# paste加載app的入口
class APIRouterV21(base_wsgi.Router):  # 繼承父類
    def __init__(self, custom_routes=None):
    		# 初始化執行個體,将map對象傳給父類
        super(APIRouterV21, self).__init__(nova.api.openstack.ProjectMapper())

        for path, methods in ROUTE_LIST + custom_routes:
            for method, controller_info in methods.items():
                controller = controller_info[0]()   # 控制類
                action = controller_info[1]  #動作
                self.map.create_route(path, method, controller, action)  # 将執行個體映射到routes.Mapper.connect(),供url映射調用

    @classmethod
    def factory(cls, global_config, **local_config):
        return cls()  #  傳回執行個體對象           
class Resource(wsgi.Application):
    def __init__(self, controller):
        self.controller = controller
        self.default_serializers = dict(json=JSONDictSerializer)
        self.wsgi_actions = {}
        if controller:
            self.register_actions(controller)

    def register_actions(self, controller):  # 被wsgi_action裝飾器裝飾的執行個體函數
        actions = getattr(controller, 'wsgi_actions', {})  # 如何實作該方法,可以參考元類定義
        for key, method_name in actions.items():
            self.wsgi_actions[key] = getattr(controller, method_name)

    def get_action_args(self, request_environment):  # 解析比對的參數,包含action,GET參數
        if hasattr(self.controller, 'get_action_args'):
            return self.controller.get_action_args(request_environment)
        try:
            args = request_environment['wsgiorg.routing_args'][1].copy()
        except (KeyError, IndexError, AttributeError):
            return {}
        return args

    def get_body(self, request):
        content_type = request.get_content_type()
        return content_type, request.body

    @webob.dec.wsgify(RequestClass=Request) 
    def __call__(self, request):   # 調用執行個體,調用真正的應用程式
        context = request.environ['nova.context']
        action_args = self.get_action_args(request.environ)  # 擷取get參數
        action = action_args.pop('action', None)

        try:
            content_type, body = self.get_body(request)   # 擷取body裡面的資料
            accept = request.best_match_content_type()
        except exception.InvalidContentType:
            msg = _("Unsupported Content-Type")
            return Fault(webob.exc.HTTPUnsupportedMediaType(explanation=msg))
        return self._process_stack(request, action, action_args, content_type, body, accept)

    def _process_stack(self, request, action, action_args, content_type, body, accept):
        meth = self.get_method(request, action, content_type, body)
        contents = self._get_request_content(body, request)
        
        action_args.update(contents)
        response = None
        try:
            with ResourceExceptionHandler():
                action_result = self.dispatch(meth, request, action_args)  # 回調執行個體方法,傳回傳回值
        except Fault as ex:
            response = ex

        if not response:
            resp_obj = None
            if type(action_result) is dict or action_result is None:
                resp_obj = ResponseObject(action_result)
            elif isinstance(action_result, ResponseObject):
                resp_obj = action_result
            else:
                response = action_result

            if resp_obj:
                if hasattr(meth, 'wsgi_code'):
                    resp_obj._default_code = meth.wsgi_code

            if resp_obj and not response:
                response = resp_obj.serialize(request, accept)
        return response

    def get_method(self, request, action, content_type, body):
        meth = self._get_method(request, action, content_type, body)
        return meth

    def _get_method(self, request, action, content_type, body):
        try:
            meth = getattr(self, action) if not self.controller else getattr(self.controller, action)
            return meth
        except AttributeError:
                raise
        action_name = action_peek(body) if action == 'action' else action
        return (self.wsgi_actions[action_name])

    def dispatch(self, method, request, action_args):
        return method(req=request, **action_args)           
class APIMapper(routes.Mapper):
    def routematch(self, url=None, environ=None):
        if url == "":
            result = self._match("", environ)
            return result[0], result[1]
        return routes.Mapper.routematch(self, url, environ)

    def connect(self, *args, **kargs):
        if not kargs['requirements'].get('format'):
            kargs['requirements']['format'] = 'json|xml'
        return routes.Mapper.connect(self, *args, **kargs)

class ProjectMapper(APIMapper):
    def create_route(self, path, method, controller, action):
        project_id_token = self._get_project_id_token()
        self.connect('/%s%s' % (project_id_token, path), conditions=dict(method=[method]), 
                     controller=controller, action=action)
        self.connect(path, conditions=dict(method=[method]), controller=controller, 
                     action=action)           
class Request(webob.Request):
    def __init__(self, environ, *args, **kwargs):
        if CONF.wsgi.secure_proxy_ssl_header:
            scheme = environ.get(CONF.wsgi.secure_proxy_ssl_header)
            if scheme:
                environ['wsgi.url_scheme'] = scheme
        super(Request, self).__init__(environ, *args, **kwargs)

class Router(object):
    def __init__(self, mapper):
        self.map = mapper
        self._router = routes.middleware.RoutesMiddleware(self._dispatch, self.map)   
				# 調取app,将url與執行個體的映射關系,回調函數傳給RoutesMiddleware。該函數會進行url的比對

    @webob.dec.wsgify(RequestClass=Request)
    def __call__(self, req):
        return self._router  #調取RoutesMiddleware

    @staticmethod
    @webob.dec.wsgify(RequestClass=Request)
    def _dispatch(req):  # 将url比對成功app進行調用,調取Resouce類的__call__函數
        match = req.environ['wsgiorg.routing_args'][1]
        app = match['controller']  # Resource對象,封裝真正的類對象
        return app            
# routes.middleware.py
class RoutesMiddleware(object):
    def __init__(self, wsgi_app, mapper, use_method_override=True, path_info=True, singleton=True):
        self.app = wsgi_app
        self.mapper = mapper
        self.path_info = path_info

    def __call__(self, environ, start_response):
        if self.singleton:  是否是單例模式
            config = request_config()
            config.mapper = self.mapper
            config.environ = environ
            match = config.mapper_dict
            route = config.route
        else:
            results = self.mapper.routematch(environ=environ)
            if results:
                match, route = results[0], results[1]  # 傳回比對參數(包括action,傳入的參數),以及route對象
            else:
                match = route = None

        url = URLGenerator(self.mapper, environ)
        environ['wsgiorg.routing_args'] = ((url), match)
        environ['routes.route'] = route
        environ['routes.url'] = url

        response = self.app(environ, start_response)  # 回調函數
        return response           
class APIMapper(routes.Mapper):
    def routematch(self, url=None, environ=None):   # 在RoutesMiddler中進行調用
        if url == "":
            result = self._match("", environ)
            return result[0], result[1]
        return routes.Mapper.routematch(self, url, environ)

    def connect(self, *args, **kargs):
        if not kargs['requirements'].get('format'):
            kargs['requirements']['format'] = 'json|xml'
        return routes.Mapper.connect(self, *args, **kargs)           

參考文檔

routes安裝:https://pypi.org/project/Routes/

routes文檔:https://routes.readthedocs.io/en/latest/

雲計算(3)- python routes URL映射管理

小計:

  1. 元類的使用方法
  2. openstack定時任務
  3. python3 異步函數