Используем возможности питона на полную. Часть 1

в 0:29, , рубрики: flask, mongodb, python, ненормальное программирование, метки: , , ,

На тему крутых возможностей питона на хабре был уже не один пост. Но мой личный опыт показал, что даже прошаренные в питоне люди не до конца понимают когда можно и когда нужно их использовать. Поэтому этот пост будет посвящён исключительно практической стороне вопроса.

Мы напишем маленький уютненький блог используя Flask и MongoDB. К слову, использовать мы будем экзотические для многих функциональные элементы языка, хотя не только их. Чего же тут ненормального? Практически весь код, за исключением маленького бутстрапа, будет храниться в БД.

Кстати, одна условность: мы запилим голый JSON-интерфейс (можно назвать REST, но стандартам он вряд ли будет соответствовать полностью). Темплейтами пусть занимается кто-нибудь другой :)

Итак, поехали.
Создаём болванку Flask-проекта.

from flask import Flask

app = Flask(__name__)


@app.route('/')
def goodbye_world():
    return 'Goodbye world!'


if __name__ == '__main__':
    app.run()

Теперь делаем простой коннект к БД. А куда ж мы без БД?

from pymongo import MongoClient

db = MongoClient()['rapira']

А теперь начинается безудержное веселье. Итак, нам нужно сделать экзекутора. Это функция, которая будет исполнять получаемый код. Если вы хотите узнать побольше о том, как это должно работать, рекомендую краткую вводную статью.

Итак, а вот и оно:

def execute(code_object, needful_objects, presets={}):
    local_storage = presets
    exec code_object in local_storage
    return dict(
        filter(
            None,
            map(lambda x: (x, local_storage[x])
                    if x in local_storage
                    else None,
                needful_objects)
        )
    )

Предлагаю разобрать этот фрагмент. Вы уже знаете, что exec может работать в произвольных областях видимости. Вы также знаете, что exec может принимать скомпилированный фрагмент кода в качестве первого параметра. Именно так мы его здесь и используем: создаём локальную область видимости, и исполняем заранее скомпилированный кусок кода в ней. После этого формируем словарь, в котором содержатся все объекты, которые у нас запросили (список имён нужных объектов предъявляется в итерабельном needful_objects).

Проверим как это работает:

>>> c = compile('''
x=5
y=6
''', '<string>', 'exec')
>>> execute(c, ('x', 'y'))
{'y': 6, 'x': 5}
>>> execute(c, ('x', 'y', 'asdsad'))
{'y': 6, 'x': 5}

Как мы видим, всё в порядке. Едем дальше. Итак, мы уже можем исполнить произвольный скомпилированный код, но нам нужно иметь возможность удобно скомпилить произвольный код. До этой функции догадайтесь сами, это не сложно.

Почему бы нам не объединить compile и execute в одно целое? Потому что скомпиленный код исполняется быстрее, чем голый текст. Поэтому функция compile будет вызываться значительно реже, чем execute. Такие дела.

Итак, у нас уже есть экзекутор, есть коннект с базой и есть интерфейс для связи с внешним миром. Пришло время позаботиться об инструментарии для работы с БД.

def create(collection_name):
    def decorator(func):
        def wr(**kwargs):
            return db[collection_name].save(func(**kwargs))
        return wr
    return decorator


def change(collection_name):
    def decorator(func):
        def wr(**kwargs):
            doc = db[collection_name].find_one(kwargs.pop('_id'))
            changed_doc = func(doc, **kwargs)
            return db[collection_name].save(changed_doc)
        return wr
    return decorator

Вот эти два простеньких декоратора значительно упростят нам жизнь. К слову, второй нам пока не очень нужен, но пусть будет раз уже написал.

Теперь нам нужно описать свой первый тип данных. Пока что это придётся сделать вне базы, но зато уже практически тем же способом, каким типы данных будут описываться в базе. С тем лишь отличием, что к базе мы обращаться не будем.

create_code = """
@db.create('code')
def create(code=str, name=str, type=str, act=str):
    return {'text': text, 'name': name, 'type': type, 'act': act}
"""

Собственно, как несложно понять, функция в строке делает ровно то, что мы сейчас делаем вручную: генерирует для базы запись, содержащую информацию об исполняемой функции. Чтобы было проще это понять, давайте сделаем так:

doc = {'text': create_code, 'name': "create", 'type': "code", 'act': "POST"}

Что мы имеем? Такой же словарь, какой возвращает приведённая выше функция. То есть при помощи той функции можно создать и занести в базу её саму. Но проверять это на практике пока рано. Нам нужен механизм который сможет адекватно прочесть информацию из базы и выстроить её в единую структуру.

compiled_struct = {}

functions = db.db.code.find()
if functions.count() == 0:
    import _code
    t = executor.execute(executor.comp(_code.doc['text']), ('create',), {'db': db})
    t['create'](**_code.doc)
    functions = db.db.code.find()

structure = make_structure(functions)

for function in structure:
    if not function['type'] in compiled_struct:
        compiled_struct[function['type']] = {}
    if not function['name'] in compiled_struct[function['type']]:
        compiled_struct[function['type']][function['name']] = {}
    compiled_struct[function['type']][function['name']][function['method']] = executor.comp(function['text'])

Собственно, вот и оно. Теперь напишем простейший роут ко всему этому добру:

@app.route('/<type_name>/<act_name>', methods=['GET', 'POST'])
def main(type_name, act_name):
    if not type_name in compiled_struct or not act_name in compiled_struct[type_name]:
        return abort(404)
    return jsonify({'response': executor.execute(compiled_struct[type_name][act_name][request.method],
                                                 (act_name,),
                                                 {'db': db})[act_name](**request.form.to_dict(flat=True))
    })

и вуаля, всё работает. Бутстрап готов.
Если отправить POST-запрос по адресу /code/create с параметрами text=«def hello():n return 'hello yopta'» name=«hello» type=«lol» method=«GET», то после перезапуска можно зайти на /lol/hello и увидеть:

{
  "response": "hello yopta"
}

На этом сегодня и закончим. Ваше домашнее задание — разобрать два последних куска кода и сделать первый из них элегантней.

В следующий раз мы поработаем над инструментарием для создания верификаторов детализированной структуры данных, сделаем автоматический апдейт путей при изменении их состояния в базе, прикрутим хэндлеры к методам, создадим тип «юзеры» и сделаем механизм для проверки прав доступа к тем или иным функциям.

А, чуть не забыл. Код вы можете забрать в репозитории.

Если у кого-то есть какие-либо вопросы по тем или иным фрагментам — не стесняйтесь, спрашивайте в комментах.

Автор: uthunderbird

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js