Исходные данные — вы строите некий сервис, и узнаете, что будете получать данные из вне в определенном формате.
Предположим что это будет JSON, структуру его определяете не вы, и вообще вот она:
Внутри проекта конечно хотелось бы чтобы структура была другой:
Видимо придется преобразовывать к внутреннему формату, попробуем так:
Но тут закрадывается подозрение, что присланный образец включает не все возможные поля. Учтем это и напишем более гибкий вариант:
Неплохо, гибко, легко расширяется до большого числа полей. Как только станет доступна полная спецификация мы легко расширим наш код до нее.
И тут вдруг приходит небольшое обновление — 'userEmail' оказывается необязательное поле. Плюс добавляется поле userTitle, которое по умолчанию, если не передается, должно быть 'Bachelor'.
Наши руки не для скуки, в ожидании когда же наконец придет полная информация учтем возможность опциональных полей и значений по умолчанию.
Черт, так мало полей и так много кода. Было проще, лучше решим пока в лоб.
Ах, хороший знакомый код без излишней сложности, прекрасно. Но что будет когда придет полная спецификация? Видимо вернемся ко второму варианту, добавим к нему проверку данных, хорошие сообщения об ошибках, упакуем его в библиотеку и будем использовать.
Хм, но уже есть такая библиотека, смотрите:
Брать здесь github.com/Deepwalker/trafaret
Полный код этого топика в виде скрипта здесь gist.github.com/2023370
Небольшое дополнение по просьбе от nimnull об ошибках.
Ошибки нужно отлавливать и человеческим языком сообщать о них в ответе. Обычно, может бывают исключения конечно, ошибку может исправить человек, а потому и писать сообщение нужно так, чтобы адресат понял.
В приведенных выше примерах ошибки нормально будут сгенерированны только в последнем, потому что в остальных я не усложнял код. Я решил что ошибки это все таки про валидацию, а в trafaret это только база, trafaret о преобразовании данных.
Точнее github.com/barbuza/contract, из которого trafaret был сделан, и был именно о валидации и ничего больше. Хорошая вещь, четко выполняет поставленную задачу. Но у меня была немного другая задача, и впечатление от '>>' из funcparserlib. Собственно в funcparserlib '>>' делает ровным счетом тоже, что и в трафарете — передает собранные данные в пользовательскую функцию на обработку.
Вернемся к ошибкам. Ошибки в трафарете это экземпляры trafaret.DataError. У каждого DataError есть аттрибут error. Для простых типов, как то Float, Int, String и тп, это строка с описанием ошибки на английском. Для Dict, Mapping и List это словарь. Для Dict и Mapping очевидно — элементы словаря это ошибки собранные из проверки полей. В случае List ключами будут числа — номера позиций элементов. Остальные варианты организации смотрятся неподходящими.
То есть пример:
Да, как видно из примера ошибку надо ловить, но сейчас не об этом — мы получили вложенные ошибки, и благодаря этому можем точно определить где и какие ошибки имеем.
Есть небольшой хелпер чтобы ошибку перевести в более удобный вид:
И в еще более удобный, для некоторых целей:
Про последний пример — нет, трафарет с формами не работает. То есть он не содержит в себе ни одного виджета, и не строит формы по мапперам/таблицам алхимии или одно чудного орма. Но проверять данные пришедшие с HTML формы вполне может.
На этом наверное все, задавайте вопросы, могу дополнить.
В заключение подкину хардкорный пример использования и гибкости:
Предположим что это будет JSON, структуру его определяете не вы, и вообще вот она:
sample_data = {
'userNameFirst': 'Adam',
'userNameSecond': 'Smith',
'userPassword': 'supersecretpassword',
'userEmail': 'adam@smith.math.edu',
'userRoles': 'teacher, worker, admin',
}
Внутри проекта конечно хотелось бы чтобы структура была другой:
import hashlib
desired_data = {
'name': 'Adam',
'second_name': 'Smith',
'password': hashlib.md5('supersecretpassword').hexdigest(),
'email': 'adam@smith.math.edu',
'roles': ['teacher', 'worker', 'admin'],
}
Видимо придется преобразовывать к внутреннему формату, попробуем так:
new_data = {
'name': sample_data['userNameFirst'],
'second_name': sample_data['userNameSecond'],
'password': hashlib.md5(sample_data['userPassword']).hexdigest(),
'email': sample_data['userEmail'],
'roles': [s.strip() for s in sample_data['userRoles'].split(',')]
}
assert new_data == desired_data, 'Uh oh'
Но тут закрадывается подозрение, что присланный образец включает не все возможные поля. Учтем это и напишем более гибкий вариант:
FIELDS = {
'userNameFirst': 'name',
'userNameSecond': 'second_name',
'userEmail': 'email',
}
new_data = dict((n2, sample_data[n1]) for n1, n2 in FIELDS.items())
new_data['roles'] = [s.strip() for s in sample_data['userRoles'].split(',')]
new_data['password'] = hashlib.md5(sample_data['userPassword']).hexdigest()
assert new_data == desired_data, 'Uh oh'
Неплохо, гибко, легко расширяется до большого числа полей. Как только станет доступна полная спецификация мы легко расширим наш код до нее.
И тут вдруг приходит небольшое обновление — 'userEmail' оказывается необязательное поле. Плюс добавляется поле userTitle, которое по умолчанию, если не передается, должно быть 'Bachelor'.
Наши руки не для скуки, в ожидании когда же наконец придет полная информация учтем возможность опциональных полей и значений по умолчанию.
desired_data['title'] = 'Bachelor' # Добавим поле в проверочные данные
FIELDS = {
'userNameFirst': 'name',
'userNameSecond': 'second_name',
'userEmail': ('email', '__optional'),
'userTitle': ('title', 'Bachelor'),
}
new_data = {}
for old, new in FIELDS.items():
if isinstance(new, tuple):
new, default = new
if old not in sample_data:
if default == '__optional':
continue
new_data[new] = default
else:
new_data[new] = sample_data[old]
new_data['roles'] = [s.strip() for s in sample_data['userRoles'].split(',')]
new_data['password'] = hashlib.md5(sample_data['userPassword']).hexdigest()
assert new_data == desired_data, 'Uh oh'
Черт, так мало полей и так много кода. Было проще, лучше решим пока в лоб.
new_data = {
'name': sample_data['userNameFirst'],
'second_name': sample_data['userNameSecond'],
'password': hashlib.md5(sample_data['userPassword']).hexdigest(),
'roles': [s.strip() for s in sample_data['userRoles'].split(',')]
}
if 'userEmail' in sample_data:
new_data['email'] = sample_data['userEmail']
new_data['title'] = sample_data.get('userTitle', 'Bachelor')
assert new_data == desired_data, 'Uh oh'
Ах, хороший знакомый код без излишней сложности, прекрасно. Но что будет когда придет полная спецификация? Видимо вернемся ко второму варианту, добавим к нему проверку данных, хорошие сообщения об ошибках, упакуем его в библиотеку и будем использовать.
Хм, но уже есть такая библиотека, смотрите:
import trafaret as t
hash_md5 = lambda d: hashlib.md5(d).hexdigest()
comma_to_list = lambda d: [s.strip() for s in d.split(',')]
converter = t.Dict({
t.Key('userNameFirst') >> 'name': t.String,
t.Key('userNameSecond') >> 'second_name': t.String,
t.Key('userPassword') >> 'password': hash_md5,
t.Key('userEmail', optional=True) >> 'email': t.Email,
t.Key('userTitle', default='Bachelor') >> 'title': t.String,
t.Key('userRoles') >> 'roles': comma_to_list,
})
assert converter.check(sample_data) == desired_data
Брать здесь github.com/Deepwalker/trafaret
Полный код этого топика в виде скрипта здесь gist.github.com/2023370
Небольшое дополнение по просьбе от nimnull об ошибках.
Ошибки нужно отлавливать и человеческим языком сообщать о них в ответе. Обычно, может бывают исключения конечно, ошибку может исправить человек, а потому и писать сообщение нужно так, чтобы адресат понял.
В приведенных выше примерах ошибки нормально будут сгенерированны только в последнем, потому что в остальных я не усложнял код. Я решил что ошибки это все таки про валидацию, а в trafaret это только база, trafaret о преобразовании данных.
Точнее github.com/barbuza/contract, из которого trafaret был сделан, и был именно о валидации и ничего больше. Хорошая вещь, четко выполняет поставленную задачу. Но у меня была немного другая задача, и впечатление от '>>' из funcparserlib. Собственно в funcparserlib '>>' делает ровным счетом тоже, что и в трафарете — передает собранные данные в пользовательскую функцию на обработку.
Вернемся к ошибкам. Ошибки в трафарете это экземпляры trafaret.DataError. У каждого DataError есть аттрибут error. Для простых типов, как то Float, Int, String и тп, это строка с описанием ошибки на английском. Для Dict, Mapping и List это словарь. Для Dict и Mapping очевидно — элементы словаря это ошибки собранные из проверки полей. В случае List ключами будут числа — номера позиций элементов. Остальные варианты организации смотрятся неподходящими.
То есть пример:
>>> import trafaret as t
>>> c = t.Dict({'a': t.List(t.Int)})
>>> c.check({'a': [4, 5]})
{'a': [4, 5]}
>>> c.check({'a': [4, 'a', 6]})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "trafaret/__init__.py", line 110, in check
return self._convert(self._check_val(value))
File "trafaret/__init__.py", line 804, in _check_val
raise DataError(error=errors)
trafaret.DataError: {'a': DataError({1: DataError(value cant be converted to int)})}
Да, как видно из примера ошибку надо ловить, но сейчас не об этом — мы получили вложенные ошибки, и благодаря этому можем точно определить где и какие ошибки имеем.
Есть небольшой хелпер чтобы ошибку перевести в более удобный вид:
>>> t.extract_error(c, {'a': [4, 'a', 6]})
{'a': {1: 'value cant be converted to int'}}
И в еще более удобный, для некоторых целей:
>>> from trafaret.utils import unfold
>>> unfold(t.extract_error(c, {'a': [4, 'a', 6]}), prefix='form')
{'form__a__1': 'value cant be converted to int'}
Про последний пример — нет, трафарет с формами не работает. То есть он не содержит в себе ни одного виджета, и не строит формы по мапперам/таблицам алхимии или одно чудного орма. Но проверять данные пришедшие с HTML формы вполне может.
На этом наверное все, задавайте вопросы, могу дополнить.
В заключение подкину хардкорный пример использования и гибкости:
>>> todt = lambda m: datetime(*[int(i) for i in m.groups()])
>>> (t.String(regex='^year=(\d+),month=(\d+),day=(\d+)$') >> todt).check('year=2011,month=07,day=23')
datetime.datetime(2011, 7, 23, 0, 0)