Saturday, December 21, 2013

Update JSON leaf value using Python

От някоко месеца се опитвам да направя продукт (казва се PyTak), чрез която лесно да се правят сценарии
за тесване на REST API и тяхното оркестиране. Целия код може да си го свалите от GitHub

Получава се добре за сега - Python-a си покза силата. Ето един примерен (от лесните):

def main():

    login()

    scenario = [
        CreateTag(assign={"name" : "new-tag"}),
        DeleteTag(bind={"name" : "0.data.name"})
    ]

    run(scenario)

    validate(CreateTag(), {'xy.1.y.1.y2.0.y22': 3})

Нищо работа - създаваме tag с име new-tag и после го изтриваме като използваме върнатия резултат. Трудността идва, когато трябва да се промени request body.

За целта предефинирах <<. Целта е максимално да се улесни нашия QA Team. Примерен фрагмент:


create_post = CreateAPost() << { "title" : "New Employee - [XXXX]", "body" : "Please welcome our new employee. [XXXX]", "type" : "TEXT", "tags" : [{"name" : "tag2"}, {"name" : "tag3"}, {"name" : "tag4"}] }





По време на тестването често се налага да се слагат случайни стойности, за да се избегнат колизии. Въведох правилото - ако някой постави [XXXX] да се замени автоматично с случайно число.
От тук се получи задачата, която искам да споделя.

Да се заменят вскички стойности в JSON, които съдържат дадена стрингова стойност

Важно е да се уточни, че пропускаме ключове, чиито стойност е масив и обхождаме ключовете в самия масив. Ето моето решение, в което използвам mutual recursion.

import re

def extract_paths(json):
    """Select all JSON keys using mutual recursion"""

    _path = []
    _query = []

    def __form_path(in_dict, in_list):
        if _path:
            _query.append(_path[:])
            del _path[:]

        if in_list:
            for el in in_list:
                _path.append(el)

        if in_dict:
            for el in in_dict:
                _path.append(el)

    def dict_update(json, in_list=None, in_dict=None):

        if isinstance(json, dict):
            for key, value in json.iteritems():
                _path.append(key)
                dict_update(value, in_dict=_path[:-1])

        if isinstance(json, list):
            list_update(json, in_list=_path[:-1])

        else:
            __form_path(in_dict, in_list)

        return _query

    def list_update(inner_list, in_list=None, in_dict=None):

        if isinstance(inner_list, list):
            for idx, value in enumerate(inner_list):
                _path.append(idx)
                list_update(value, in_list=_path[:-1])

        if isinstance(inner_list, dict):
            dict_update(inner_list, in_dict=_path[:-1])

        else:
            __form_path(in_dict, in_list)

        return _query

    return dict_update(json)

def update_leaf_if_value_func(json, change_func):
    """Apply 'change_func' to every leaf using the leaf as an argument"""

    all_leafs = [path for path in extract_paths(json)]
    for query in all_leafs:
        selected = __upto_key_for_change(json, query)
        if isinstance(selected[query[-1]], dict) or isinstance(selected[query[-1]], list):
            continue

        selected[query[-1]] = change_func(selected[query[-1]])

    return json

def update_leaf_if_value(json, value_pattern, new_value):
    """Replace 'value_pattern' in a JSON leaf with new_value"""

    all_leafs = [path for path in extract_paths(json)]
    for query in all_leafs:
        selected = __upto_key_for_change(json, query)
        if isinstance(selected[query[-1]], dict) or isinstance(selected[query[-1]], list):
            continue

        selected[query[-1]] = re.sub(value_pattern, new_value, selected[query[-1]])

    return json

def update(json, key, new_value):
    """Update *all* JSON keys that match 'key' with a given 'value'

    Algorithm explanation:

        1. Select path to JSON key referred by the given key
        2. Fold the JSON structure up to this key (not included)
        3. Update the leaf of folded JSON using given key
    """

    matched = [path for path in extract_paths(json) if path[-1] == key]
    for query in matched:
        selected = __upto_key_for_change(json, query)
        selected[query[-1]] = new_value

    return json

def update_leaf(json, key, new_value):
    """Update *all* JSON leafs that match 'key' with a given 'value'

    Algorithm explanation:

        1. Select path to JSON leaf referred by the given key
        2. Fold the JSON structure up to this key (not included)
        3. Update the leaf of folded JSON using given key
    """

    matched = [path for path in extract_paths(json) if path[-1] == key]
    for query in matched:
        selected = __upto_key_for_change(json, query)
        # skip keys that refer dictionary or list
        if isinstance(selected[query[-1]], dict) or isinstance(selected[query[-1]], list):
            continue

        # update only leafs
        selected[query[-1]] = new_value

    return json

#___________________________________________________________
#                                          Helper functions

def __upto_key_for_change(data, query):
    def __select(data, query):
        return data[query]

    return reduce(__select, query[:-1], data)



Ще стане добре - сигурен съм. Ако ми остане време ще разкрия и още интересни части от този проект, който е само в зародиш.

No comments:

algorithms (1) cpp (3) cv (1) daily (4) emacs (2) freebsd (4) java (3) javascript (1) JSON (1) linux (2) Lisp (7) misc (8) programming (16) Python (4) SICP (1) source control (4) sql (1) думи (8)