I have validation rules in Cerberus that require a custom validator. When accessing fields in self.document, I have to also validate those fields are present, even if using the "required" flag. I am looking for a way for the "required" flag to handle this for me.
For example, say I have a dictionary named data with arrays a and b and the stipulations that both a and b are required and that len(a) == len(b).
# Schema
schema = {'data':
{'type': 'dict',
'schema': {'a': {'type': 'list',
'required': True,
'length_b': True},
'b': {'type': 'list',
'required': True}}}}
# Validator
class myValidator(cerberus.Validator):
def _validate_length_b(self, length_b, field, value):
"""Validates a field has the same length has b"""
if length_b:
b = self.document.get('b')
if not len(b) == len(value):
self._error(field, 'is not equal to length of b array')
This works fine if a and b are present:
good = {'data': {'a': [1, 2, 3],
'b': [1, 2, 3]}}
v = myValidator()
v.validate(good, schema)
# True
bad = {'data': {'a': [1, 2, 3],
'b': [1, 3]}}
v.validate(bad, schema)
# False
v.errors
# {'data': [{'a': ['is not equal to length of b array']}]}
However, if b is missing, it returns a TypeError from len().
very_bad = {'data': {'a': [1, 2, 3]}}
v.validate(very_bad, schema)
# TypeError: object of type 'NoneType' has no len()
How can I get validate to return False instead (as b is not present)? My desired output is below:
v.validate(very_bad, schema)
# False
v.errors
# {'data': ['b': ['required field']]}
Taking Validating that two params have same amount elements using Cerberus as inspiration, could do:
schema = {'data':
{'type': 'dict',
'schema': {'a': {'type': 'list',
'required': True,
'match_length': 'b'},
'b': {'type': 'list',
'required': True}}}}
class MyValidator(cerberus.Validator):
def _validate_match_length(self, other, field, value):
if other not in self.document:
return False
elif len(value) != len(self.document[other]):
self._error(field,
"Length doesn't match field %s's length." % other)
Then:
v = MyValidator(schema)
good = {'data': {'a': [1, 2, 3],
'b': [1, 2, 3]}}
v.validate(good)
-> True
bad = {'data': {'a': [1, 2, 3],
'b': [1, 3]}}
v.validate(bad)
-> False
v.errors
-> {'data': [{'a': ["Length doesn't match field b's length."]}]}
very_bad = {'data': {'a': [1, 2, 3]}}
v.validate(very_bad)
-> False
v.errors
-> {'data': [{'b': ['required field']}]}
Related
How to iterate over a dictionary / JSON using a dynamic query.
For example consider the below dict
dict = {'Adam': {
'English': {
'Score': 99,
'Time': 3400,
'Classes': 4},
'Math': {
'Score': 45,
'Time': 779,
'Classes': 5}},
'Tim': {
'English': {
'Score': 74,
'Time': 12,
'Classes': 99},
'Math': {
'Score': 12,
'Time': 333,
'Classes': 1}}
}
I want to set the value of a given path for example
path = '/Adam/English/Score'
new_value = 87
Note that the value assigned could be another dict as well for example
path = '/Adam/English'
new_value = {'Score': 11,
'Time': 2,
'Classes': 9}
Any help would be useful.
Edit: Below is my attempt
keys = path.split('/')[1:]
new_data = None
for key in keys:
if new_data is None:
new_data = dict[key]
else:
new_data = new_data[key]
new_data = new_value
print(dict)
But here the dict still has the old value
I made some assumptions, for example that '/' is not part of any dict-keys and that the path must be valid. Adjust the function as needed.
def deep_set(d, path, value):
sep = '/'
*trail, last = path.strip(sep).split(sep)
for part in trail:
d = d[part]
d[last] = value
Demo:
>>> d = {'a': 1}
>>> deep_set(d, 'a', 2)
>>> d
{'a': 2}
>>> d = {'a': {'b': 1}}
>>> deep_set(d, 'a/b', 2)
>>> d
{'a': {'b': 2}}
edit:
Note that if there are consecutive '/' characters then the empty string will be looked up as a dict key. e.g.
'a/b//c'.split('/') -> ['a', 'b', '', 'c']
It's unclear whether you want to treat leading/trailling '/' characters as part of the path or not (in my function, they are removed with str.strip). Again, adjust as needed.
I get unexpected behaviour for the following code:
import cerberus
v = cerberus.Validator()
schema = {'list_of_values': {'type': 'list',
'schema': {'items': [{'type': 'string', 'coerce': str},
{'type': 'integer', 'coerce': int}]}}
}
document = {'list_of_values': [['hello', 100], [123, "122"]]}
v.validate(document, schema)
v.errors
I am expecting to have no errors, as the coercion should take care of the types. But I am getting
{'list_of_values': [{1: [{0: ['must be of string type'],
1: ['must be of integer type']}]}]}
Is this a bug? Am I misunderstanding how the coercion works?
#funky-future
Something not right on your end, I can indeed reproduce the problem just by copy paste the example into the prompt:
>>> import cerberus
>>> v = cerberus.Validator()
>>> schema = {'list_of_values': {'type': 'list',
... 'schema': {'items': [{'type': 'string', 'coerce': str},
... {'type': 'integer', 'coerce': int}]}}
... }
>>> document = {'list_of_values': [['hello', 100], [123, "122"]]}
>>> v.validate(document, schema)
False
>>> v.errors
{'list_of_values': [{1: [{0: ['must be of string type'], 1: ['must be of integer type']}]}]}
Python3.5.2, cerberus1.2
I'm using cerberus to validate data. One of my fields is optional - it doesn't need to be present for every item. However, the key must be populated at least once across the entire data array.
As an example, say I want to validate the key 'c' occurs in at least one dictionary in my data list:
from cerberus import Validator
has_c = {'data': [{'a': 1, 'b': 2}, {'b': 2}, {'c': 3}]}
no_c = {'data': [{'a': 1, 'b': 2}, {'a': 1}]}
schema = {'data':
{'type': 'list',
'schema': {
'type': 'dict',
'schema': {
'a': {'required': True},
'b': {'required': True},
'c': {'required': False, 'at_least_one': True}
}
}
}
}
class MyValidator(Validator) # Some fancy code...
....
v = MyValidator()
v.validate(has_c, schema) # Passes
v.validate(no_c, schema) # Fails
This seems doable outside of cerberus, but I'd like to keep the method in my validator if possible.
If you want the method to be in the Validator subclass, then you will want to create a custom rule just like you were thinking.
from cerberus import Validator
test_with_c = {'data': [{'a': 1, 'b': 2}, {'b': 2}, {'c': 3}]}
test_with_no_c = {'data': [{'a': 1, 'b': 2}, {'a': 1}]}
class MyValidator(Validator):
def _validate_has_c(self, has_c, field, value):
seen_c = False
for v in value:
if "c" in v:
seen_c = True
if has_c and not seen_c:
self._error(field, "Must contain a 'c' key")
schema = {
"data": {
"type": "list",
"has_c": True
}
}
v = MyValidator(schema)
print(v(test_with_c), v.errors)
print(v(test_with_no_c), v.errors)
Running this will yield the results you want with respect to looking for a c key in one of the elements. Running that code yields
True {}
False {'data': ["Must contain a 'c' key"]}
I would like to build a multi-level dictionary such like:
A = {
'a': {
'A': {
'1': {},
'2': {},
},
'B': {
'1': {},
'2': {},
},
},
'b': {
'A': {
'1': {},
'2': {},
},
'B': {
'1': {},
'2': {},
},
},
}
My question is that whether it existed a function that I can build the above diction by:
D = function(['a', 'b'], ['A', 'B'], ['1', '2'], {})
This uses the copy function to allow you specify a different leaf node. Otherwise all the leaves will point to the same dictionary.
from copy import copy
def multidict(*args):
if len(args) == 1:
return copy(args[0])
out = {}
for x in args[0]:
out[x] = multidict(*args[1:])
return out
print multidict(['a', 'b'], ['A', 'B'], ['1', '2'], {})
def multi(*args):
if len(args) > 1:
return {arg:multi(*args[1:]) for arg in args[0]}
else:
return args[0]
multi(['a', 'b'], ['A', 'B'], ['1', '2'], {})
returns
{'a': {'A': {'1': {}, '2': {}}, 'B': {'1': {}, '2': {}}},
'b': {'A': {'1': {}, '2': {}}, 'B': {'1': {}, '2': {}}}}
EDIT: In my solution, the last argument {} will be copied into every leaf of the output as a reference to the same dictionary. If this is a problem (replacing it with an immutable object, such as float, integer or string is a different thing), use the copy.copy idea of #matt.
It's easy to write using recusion
def multi_level_dict(*args):
x = dict()
if args:
for k in args[0]:
x[k] = multi_level_dict(*args[1:])
return x
your case would be
multi_level_dict(["a", "b"], ["A", "B"], ["1", "2"])
or even
multi_level_dict("ab", "AB", "12")
dict comprehension is a cool approach for that, but only if you nesting depth is fixed:
{x:{y:{z:{} for z in ['1', '2']} for y in 'AB'} for x in 'ab'}
Let's say we have:
from collections import defaultdict
original_dict = { 'somegroupofelements':{'name':1, 'group':1 ,'results':[1,2,3,4]}, 'somegroupofelements2':{'name':2, 'group': 2 ,'results':[1,2,3,4]}, 'somegroupofelements3':{'name':3, 'group':3 ,'results':[1,2,3,4]} }
new_dict = defaultdict(list)
for key, value in original_dict.iteritems():
# i need to organize things grouped for making the right latex tables
# and for updating some values...
value['key']=key
new_dict[value['group']].append(value)
I want that new_dict, after I've done my work, to be organized again just like the original_dict? Like reconstruct the original_dict from the new_dict.
So you end up with a dictionary in the form:
>>> d = { 100 : [{'name':1, 'group':100, 'key':'group1'},
... {'name':2, 'group':100, 'key':'group2'}],
... 200 : [{'name':3, 'group':200, 'key':'group3'}] }
Which can be transformed back into a dictionary using a dict comprehension:
>>> orig = { x['key']:x for v in d.values() for x in v }
>>> orig
{'group1': {'group': 100, 'name': 1, 'key': 'group1'},
'group3': {'group': 200, 'name': 3, 'key': 'group3'},
'group2': {'group': 100, 'name': 2, 'key': 'group2'}}
If you want, you can then delete the superflous key field of the items:
>>> for v in orig.values(): del v['key']
...
>>> orig
{'group1': {'group': 100, 'name': 1},
'group3': {'group': 200, 'name': 3},
'group2': {'group': 100, 'name': 2}}