Домой Edit me on GitHub

2017-11-28

Диспетчеризация URL

Каждый поступающий на сервер приложений Pyramid запрос (request) должен найти вид (view), который и будет его обрабатывать.

В Pyramid имеется два базовых подхода к поиску нужного вида для обрабатываемого запроса: на основе сопоставления (matching), как в большинстве подобных фреймворков, и обхода (traversal), как в Zope. Кроме того, в одном приложении можно с успехом сочетать оба подхода.

Pattern Matching

Простейший пример с заданием маршрута (заимствован из документации):

# Здесь config - экземпляр pyramid.config.Configurator
config.add_route('idea', 'site/{id}')
config.add_view('mypackage.views.site_view', route_name='idea')

Traversal

Использование обхода лучше проиллюстрировать на небольшом примере:

from wsgiref.simple_server import make_server
from pyramid.config import Configurator
from pyramid.response import Response

# Класс некоторого ресурса
class Resource(dict):
    pass

# Дерево ресурсов (жёстко закодированное) в фабрике корня
def get_root(request):
    return Resource({'a': Resource({'b': Resource({'c': Resource()})})})

# Вид-для-вызова, который умеет показывать ресурс Resource (в context)
def hello_world_of_resources(context, request):
    output = "Ресурс и его дети: %s" % context
    return Response(output)

if __name__ == '__main__':
    config = Configurator(root_factory=get_root)
    config.add_view(hello_world_of_resources, context=Resource)
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

В этом примере иерархия для обхода жестко задана в методе get_root с помощью вложенных словарей, тогда как реальные приложения должны сами определять необходимый доступ по ключам (метод __getitem__ помогает организовать такой доступ). В коде также присутствует корневая фабрика, с которой собственно и начинается обход узлов (node) дерева ресурсов. Вид-для-вызова (view callable) представлен функцией hello_world_of_resources. Говоря несколько упрощённо, на основе URL запроса в результате обхода иерархии Pyramid находит ресурс и применяет к нему «наилучший» вид-для-вызова (в нашем примере — он единственный).

Обход словаря

Предупреждение

В примерах используется Python3

Метод обхода дерева (traversal), позволяет находить ресурсы во вложенных структурах. Такой механизм хорошо применим для некоторых практических задач, например список всех URL маршрутов сайта является деревом, в котором каждый конечный URL это отдельная ветка дерева. Поэтому всю структуру сайта можно поместить в словарь.

К примеру, известно, что смерь сказочного персонажа «кащея» находится в яйце, которое в свою очередь в утке, которая в зайце и т.д. В сумме получается вложенная структура, которую можно описать так:

остров -> дуб -> сундук -> заяц -> утка -> яйцо -> игла -> СмертьКощея

Мы можем такую, плоскую, вложенную структуру легко представить в виде URL:

http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла

А так-как любой URL является веткой дерева, то несложно описать это в Python:

{
     'остров': {
         'дуб': {
             'сундук': {
                 'заяц': {
                     'утка': {
                         'яйцо': {
                             'игла': СмертьКощея()
                         }
                     }
                 }
             }
         }
     }
 }

СмертьКощея() - это объект класса СмертьКощея, который может выглядеть к примеру так:

class СмертьКощея(object):

    def __json__(self, request):

        return {
            'имя': 'кощей',
            'статус': request.context == self and 'мертв' or 'жив ещё',
        }

В принципе, этого достаточно чтобы наш сайт-убийца «кощея» заработал. Осталось лишь прописать пути и добавить представление (view).

View будет просто возвращать объект, например, если мы ввели:

URL: 'остров'
объект: {'дуб': {
            'сундук': {
                'заяц': {
                    'утка': {
                        'яйцо': {
                            'игла': СмертьКощея()
                        }
                    }
                }
            }
        }}
URL: 'остров/дуб'
объект: {'сундук': {
            'заяц': {
                'утка': {
                    'яйцо': {
                        'игла': СмертьКощея()
                    }
                }
            }
        }}
URL: 'остров/дуб/сундук'
объект: {'заяц': {
            'утка': {
                'яйцо': {
                    'игла': СмертьКощея()
                }
            }
        }}
URL: 'остров/дуб/сундук/заяц'
объект: {'утка': {
            'яйцо': {
                'игла': СмертьКощея()
            }
        }}
URL: 'остров/дуб/сундук/заяц/утка'
объект: {'яйцо': {
            'игла': СмертьКощея()
        }}
URL: 'остров/дуб/сундук/заяц/утка/яйцо'
объект: {'игла': СмертьКощея()}
URL: 'остров/дуб/сундук/заяц/утка/яйцо/игла'
объект: СмертьКощея()

Такие функции-представления (View) должны принимать 2 параметра, где первый параметр будет являться объектом, обычно именуемым context, а второй параметр request:

def traverse_koshey(context, request):
    return context  # Наш объект

Роуты создаются почти так же как в pattern matching, за исключением того, что структура путей передается в виде «фабрики», которая возвращает словарь или ему подобный (dict-like) объект. Путь указывается в виде статической и динамической части, например /fixedpath/*traverse:

config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)

Фабрика, которая возвращает структуру сайта:

def my_factory(request):
    return {
        'остров': {
            'дуб': {
                'сундук': {
                    'заяц': {
                        'утка': {
                            'яйцо': {
                                'игла': СмертьКощея()
                            }
                        }
                    }
                }
            }
        }
    }

View добавляется стандартно:

config.add_view(traverse_koshey, route_name='koshey', renderer='json')

Все готово, можно перемещаться по объектам:

Полный пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from wsgiref.simple_server import make_server

from pyramid.config import Configurator


def traverse_koshey(context, request):
    return context


class СмертьКощея(object):

    def __json__(self, request):

        return {
            'имя': 'кощей',
            'статус': request.context == self and 'мертв' or 'жив ещё',
        }


def my_factory(request):
    return {
        'остров': {
            'дуб': {
                'сундук': {
                    'заяц': {
                        'утка': {
                            'яйцо': {
                                'игла': СмертьКощея()
                            }
                        }
                    }
                }
            }
        }
    }


if __name__ == '__main__':
    config = Configurator()

    # Traversal routing
    config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
    config.add_view(traverse_koshey, route_name='koshey', renderer='json')

    # Make app and serve
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

Есть один нюанс, json renderer, по умолчанию, все не латинские символы отображает как UTF коды \uxxxx, поэтому мы увидим следующий вывод:

{"\u043e\u0441\u0442\u0440\u043e\u0432": {"\u0434\u0443\u0431": {"\u0441\u0443\u043d\u0434\u0443\u043a": {"\u0437\u0430\u044f\u0446": {"\u0443\u0442\u043a\u0430": {"\u044f\u0439\u0446\u043e": {"\u0438\u0433\u043b\u0430": {"\u0441\u0442\u0430\u0442\u0443\u0441": "\u0436\u0438\u0432 \u0435\u0449\u0451", "\u0438\u043c\u044f": "\u043a\u043e\u0449\u0435\u0439"}}}}}}}}

Но можно изменить его поведение следующим образом:

 from pyramid.renderers import JSON
 ...
 config.add_renderer('myjson', JSON(indent=4, ensure_ascii=False))
 config.add_view(traverse_koshey, route_name='koshey', renderer='myjson')

Результат:

http://localhost:8080/mytraversal/

{
    "остров": {
        "дуб": {
            "сундук": {
                "заяц": {
                    "утка": {
                        "яйцо": {
                            "игла": {
                                "имя": "кощей",
                                "статус": "жив ещё"
                            }
                        }
                    }
                }
            }
        }
    }
}

http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла

{
    "имя": "кощей",
    "статус": "мертв"
}

Полный код:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
from wsgiref.simple_server import make_server

from pyramid.config import Configurator
from pyramid.renderers import JSON


def traverse_koshey(context, request):
    return context


class СмертьКощея(object):

    def __json__(self, request):

        return {
            'имя': 'кощей',
            'статус': request.context == self and 'мертв' or 'жив ещё',
        }


def my_factory(request):
    return {
        'остров': {
            'дуб': {
                'сундук': {
                    'заяц': {
                        'утка': {
                            'яйцо': {
                                'игла': СмертьКощея()
                            }
                        }
                    }
                }
            }
        }
    }


if __name__ == '__main__':
    config = Configurator()

    # ensure_ascii JSON renderer
    config.add_renderer('myjson', JSON(indent=4, ensure_ascii=False))

    # Traversal routing
    config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
    config.add_view(traverse_koshey, route_name='koshey', renderer='myjson')

    # Make app and serve
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

Привязка View к ресурсам

В Пирамиде объект (context) который передается во вью, именуют еще как «ресурс». Есть возможность жестко привязать View к типу ресурса. Например, наше представление traverse_koshey должно вызываться, только когда пришел объект класса СмертьКощея:

config.add_view(traverse_koshey, route_name='koshey_context',
                renderer='myjson',
                context=СмертьКощея)

Параметр context указывает на то, что это View принадлежит ТОЛЬКО объектам класса СмертьКащея.

Все пути, кроме полного (который возвращает нужный объект), вернут 404 код ответа. Полный путь http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла.

Добавим в нашу структуру еще ресурсов:

def my_factory(request):
    return {
        'превед': Человек(),
        'остров': {
            'ясень': {
                'что то здесь': 'не так!'
            },
            'дуб': {
                'сундук': {
                    'заяц': {
                        'утка': {
                            'яйцо': {
                                'игла': СмертьКощея()
                            }
                        }
                    }
                }
            }
        }
    }

Здесь Человек() это новый тип ресурса, который имеет метод __getitem__ как у словаря и при обращении по ключу возвращает другой ресурс:

class Человек(object):

    name = 'Человек'

    def __getitem__(self, name):
        return Имя(name)


class Имя(object):

    __parent__ = Человек()

    def __init__(self, name):
        self.name = name

Например мы обращаемся по URL http://localhost:8080/mytraversal/превед/Пирамид. превед вернет ресурс Человек, а Пирамид вызовет метод __getitem__, который вернет ресурс Имя('Пирамид'). Таким образом мы можем строить дерево динамически при помощи dict-like объектов.

Для ресурса Имя мы можем создать отдельное представление и жестко привязать его к этому типу.

def traverse_hello(context, request):
    """
    http://localhost:8080/mytraversal/первед/Пирамид
    """
    return Response('Превед ' + context.__parent__.name + ' ' + context.name)

...

config.add_view(traverse_hello, route_name='koshey_context',
                renderer='text',
                context=Имя)

Результат вывода по адресу http://localhost:8080/mytraversal/превед/Пирамид, будет обычный текст (Content-Type: plain/text):

Превед Человек Пирамид

Полный пример:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
from wsgiref.simple_server import make_server

from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.renderers import JSON


def traverse_koshey(context, request):
    """
    http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла
    """
    return context


def traverse_hello(context, request):
    """
    http://localhost:8080/mytraversal/первед/Пирамид
    """
    return Response('Превед ' + context.__parent__.name + ' ' + context.name)


class Человек(object):

    name = 'Человек'

    def __getitem__(self, name):
        return Имя(name)

    def __json__(self, request):
        return {'name': self.name}


class Имя(object):

    __parent__ = Человек()

    def __init__(self, name):
        self.name = name

    def __json__(self, request):
        return {'name': self.name}


class СмертьКощея(object):

    def __json__(self, request):

        return {
            'имя': 'кощей',
            'статус': request.context == self and 'мертв' or 'жив ещё',
        }


def my_factory(request):
    return {
        'превед': Человек(),
        'остров': {
            'ясень': {
                'что то здесь': 'не так!'
            },
            'дуб': {
                'сундук': {
                    'заяц': {
                        'утка': {
                            'яйцо': {
                                'игла': СмертьКощея()
                            }
                        }
                    }
                }
            }
        }
    }


if __name__ == '__main__':
    config = Configurator()

    # ensure_ascii JSON renderer
    config.add_renderer('myjson', JSON(indent=4, ensure_ascii=False))

    # Traversal routing
    config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
    config.add_view(traverse_koshey, route_name='koshey', renderer='myjson')

    # Traversal routing with context constraint
    config.add_route('koshey_context', '/mytraversal_context/*traverse',
                     factory=my_factory)
    config.add_view(traverse_koshey, route_name='koshey_context',
                    renderer='myjson',
                    context=СмертьКощея)
    config.add_view(traverse_hello, route_name='koshey_context',
                    renderer='text',
                    context=Имя)

    # Make app and serve
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()

Комбинация обоих методов

Фреймворк Pyramid позволяет использовать оба способа URL маршрутизации одновременно.

Добавим к примеру с «кащеем» hello world с использованием pattern matching:

def hello_world(request):
    return Response('Hello %(name)s!' % request.matchdict)

...

# Pattern matching routes
config.add_route('hello', '/hello/{name}')
config.add_view(hello_world, route_name='hello')

Полный пример:

Комбинированный способ маршрутизации traversal и pattern matching
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
from wsgiref.simple_server import make_server

from pyramid.config import Configurator
from pyramid.response import Response
from pyramid.renderers import JSON


def hello_world(request):
    return Response('Hello %(name)s!' % request.matchdict)


def traverse_koshey(context, request):
    """
    http://localhost:8080/mytraversal/остров/дуб/сундук/заяц/утка/яйцо/игла
    """
    return context


def traverse_hello(context, request):
    """
    http://localhost:8080/mytraversal/первед/Пирамид
    """
    return Response('Превед ' + context.__parent__.name + ' ' + context.name)


class Человек(object):

    name = 'Человек'

    def __getitem__(self, name):
        return Имя(name)

    def __json__(self, request):
        return {'name': self.name}


class Имя(object):

    __parent__ = Человек()

    def __init__(self, name):
        self.name = name

    def __json__(self, request):
        return {'name': self.name}


class СмертьКощея(object):

    def __json__(self, request):

        return {
            'имя': 'кощей',
            'статус': request.context == self and 'мертв' or 'жив ещё',
        }


def my_factory(request):
    return {
        'превед': Человек(),
        'остров': {
            'ясень': {
                'что то здесь': 'не так!'
            },
            'дуб': {
                'сундук': {
                    'заяц': {
                        'утка': {
                            'яйцо': {
                                'игла': СмертьКощея()
                            }
                        }
                    }
                }
            }
        }
    }


if __name__ == '__main__':
    config = Configurator()

    # Pattern matching routes
    config.add_route('hello', '/hello/{name}')
    config.add_view(hello_world, route_name='hello')

    # ensure_ascii JSON renderer
    config.add_renderer('myjson', JSON(indent=4, ensure_ascii=False))

    # Traversal routing
    config.add_route('koshey', '/mytraversal/*traverse', factory=my_factory)
    config.add_view(traverse_koshey, route_name='koshey', renderer='myjson')

    # Traversal routing with context constraint
    config.add_route('koshey_context', '/mytraversal_context/*traverse',
                     factory=my_factory)
    config.add_view(traverse_koshey, route_name='koshey_context',
                    renderer='myjson',
                    context=СмертьКощея)
    config.add_view(traverse_hello, route_name='koshey_context',
                    renderer='text',
                    context=Имя)

    # Make app and serve
    app = config.make_wsgi_app()
    server = make_server('0.0.0.0', 8080, app)
    server.serve_forever()
Previous: Базы данных (Models) Next: REST API