PageRenderTime 101ms CodeModel.GetById 34ms RepoModel.GetById 0ms app.codeStats 1ms

/doqu/validators.py

https://bitbucket.org/neithere/doqu/
Python | 395 lines | 353 code | 4 blank | 38 comment | 5 complexity | ca8d619a3011941bd8ebff4494537737 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-3.0
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Doqu is a lightweight schema/query framework for document databases.
  4. # Copyright © 2009—2010 Andrey Mikhaylenko
  5. #
  6. # This file is part of Doqu.
  7. #
  8. # Doqu is free software: you can redistribute it and/or modify
  9. # it under the terms of the GNU Lesser General Public License as published
  10. # by the Free Software Foundation, either version 3 of the License, or
  11. # (at your option) any later version.
  12. #
  13. # Doqu is distributed in the hope that it will be useful,
  14. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. # GNU Lesser General Public License for more details.
  17. #
  18. # You should have received a copy of the GNU Lesser General Public License
  19. # along with Doqu. If not, see <http://gnu.org/licenses/>.
  20. """
  21. Validators
  22. ==========
  23. A validator simply takes an input and verifies it fulfills some criterion, such as
  24. a maximum length for a string. If the validation fails, a
  25. :class:`~ValidationError` is raised. This simple system allows chaining any
  26. number of validators on fields.
  27. The module is heavily inspired by (and partially ripped off from) the
  28. `WTForms`_ validators. However, ours serve a bit different purpose. First,
  29. error messages are not needed here (the errors will not be displayed to end
  30. users). Second, these validators include **query filtering** capabilities.
  31. Usage example::
  32. class Person(Document):
  33. validators = {
  34. 'first_name': [required(), length(min=2)],
  35. 'age': [number_range(min=18)],
  36. }
  37. This document will raise :class:`ValidationError` if you attempt to save it
  38. with wrong values. You can call :meth:`Document.is_valid` to ensure everything is OK.
  39. Now let's query the database for all objects of `Person`::
  40. Person.objects(db)
  41. Doqu does not deal with tables or collections, it follows the DRY (Don't
  42. Repeat Yourself) principle and uses the same validators to determine what
  43. database records belong to given document class. The schema defined above is
  44. alone equivalent to the following query::
  45. ...where(first_name__exists=True, age__gte=18).where_not(first_name='')
  46. This is actually the base query available as ``Person.objects(db)``.
  47. .. note:: not all validators affect document-related queries. See detailed
  48. documentation on each validator.
  49. .. _WTForms: http://wtforms.simplecodes.com
  50. """
  51. import re
  52. __all__ = [
  53. # exceptions
  54. 'StopValidation', 'ValidationError',
  55. # validators
  56. 'Email', 'email',
  57. 'EqualTo', 'equal_to',
  58. 'Equals', 'equals',
  59. 'Exists', 'exists',
  60. 'IPAddress', 'ip_address',
  61. 'Length', 'length',
  62. 'NumberRange', 'number_range',
  63. 'Optional', 'optional',
  64. 'Required', 'required',
  65. 'Regexp', 'regexp',
  66. 'URL', 'url',
  67. 'AnyOf', 'any_of',
  68. 'NoneOf', 'none_of'
  69. # TODO 'Unique', 'unique',
  70. ]
  71. #--------------+
  72. # Exceptions |
  73. #--------------+
  74. class StopValidation(Exception):
  75. """
  76. Causes the validation chain to stop.
  77. If StopValidation is raised, no more validators in the validation chain are
  78. called.
  79. """
  80. pass
  81. class ValidationError(ValueError):
  82. """
  83. Raised when a validator fails to validate its input.
  84. """
  85. pass
  86. #--------------+
  87. # Validators |
  88. #--------------+
  89. class Equals(object):
  90. """
  91. Compares the value to another value.
  92. :param other_value:
  93. The other value to compare to.
  94. Adds conditions to the document-related queries.
  95. """
  96. def __init__(self, other_value):
  97. self.other_value = other_value
  98. def __call__(self, instance, value):
  99. if not self.other_value == value:
  100. raise ValidationError
  101. def filter_query(self, query, name):
  102. return query.where(**{
  103. name: self.other_value
  104. })
  105. class EqualTo(object):
  106. """
  107. Compares the values of two fields.
  108. :param name:
  109. The name of the other field to compare to.
  110. """
  111. def __init__(self, name):
  112. self.name = name
  113. def __call__(self, instance, value):
  114. if not instance[self.name] == value:
  115. raise ValidationError
  116. class Exists(object):
  117. """
  118. Ensures given field exists in the record. This does not affect validation
  119. of a document with pre-defined structure but does affect queries.
  120. Adds conditions to the document-related queries.
  121. """
  122. def __call__(self, instance, value):
  123. # of course it exists!
  124. pass
  125. def filter_query(self, query, name):
  126. return query.where(**{
  127. '{0}__exists'.format(name): True
  128. })
  129. class Length(object):
  130. """
  131. Validates the length of a string.
  132. :param min:
  133. The minimum required length of the string. If not provided, minimum
  134. length will not be checked.
  135. :param max:
  136. The maximum length of the string. If not provided, maximum length
  137. will not be checked.
  138. """
  139. def __init__(self, min=None, max=None):
  140. assert not all(x is None for x in (min,max))
  141. self.min = min
  142. self.max = max
  143. def __call__(self, instance, value):
  144. if self.min is not None and len(value) < self.min:
  145. raise ValidationError
  146. if self.max is not None and self.max < len(value):
  147. raise ValidationError
  148. class NumberRange(object):
  149. """
  150. Validates that a number is of a minimum and/or maximum value, inclusive.
  151. This will work with any comparable number type, such as floats and
  152. decimals, not just integers.
  153. :param min:
  154. The minimum required value of the number. If not provided, minimum
  155. value will not be checked.
  156. :param max:
  157. The maximum value of the number. If not provided, maximum value
  158. will not be checked.
  159. Adds conditions to the document-related queries.
  160. """
  161. def __init__(self, min=None, max=None):
  162. assert min is not None or max is not None
  163. self.min = min
  164. self.max = max
  165. def __call__(self, instance, value):
  166. if self.min is not None and value < self.min:
  167. raise ValidationError
  168. if self.max is not None and self.max < value:
  169. raise ValidationError
  170. def filter_query(self, query, name):
  171. conditions = {}
  172. if self.min is not None:
  173. conditions.update({'%s__gte'%name: self.min})
  174. if self.max is not None:
  175. conditions.update({'%s__lte'%name: self.max})
  176. return query.where(**conditions)
  177. class Optional(object):
  178. """
  179. Allows empty value (i.e. ``bool(value) == False``) and terminates the
  180. validation chain for this field (i.e. no more validators are applied to
  181. it). Note that errors raised prior to this validator are not suppressed.
  182. """
  183. def __call__(self, instance, value):
  184. if not value:
  185. raise StopValidation
  186. class Required(object):
  187. """
  188. Requires that the value is not empty, i.e. ``bool(value)`` returns `True`.
  189. The `bool` values can also be `False` (but not anything else).
  190. Adds conditions to the document-related queries: the field must exist and
  191. be not equal to an empty string.
  192. """
  193. def __call__(self, instance, value):
  194. if not value and value != False:
  195. raise ValidationError
  196. def filter_query(self, query, name):
  197. # defined and not empty
  198. return query.where(**{
  199. '{0}__exists'.format(name): True,
  200. }).where_not(**{
  201. '{0}__equals'.format(name): '',
  202. })
  203. class Regexp(object):
  204. """
  205. Validates the field against a user provided regexp.
  206. :param regex:
  207. The regular expression string to use.
  208. :param flags:
  209. The regexp flags to use, for example `re.IGNORECASE` or `re.UNICODE`.
  210. .. note:: the pattern must be provided as string because compiled patterns
  211. cannot be used in database lookups.
  212. Adds conditions to the document-related queries: the field must match the
  213. pattern.
  214. """
  215. def __init__(self, pattern, flags=0):
  216. # pre-compiled patterns are not accepted because they can't be used in
  217. # database lookups
  218. assert isinstance(pattern, basestring)
  219. self.pattern = pattern
  220. self.regex = re.compile(pattern, flags)
  221. def __call__(self, instance, value):
  222. if not self.regex.match(value or ''):
  223. raise ValidationError
  224. def filter_query(self, query, name):
  225. return query.where(**{'{0}__matches'.format(name): True})
  226. class Email(Regexp):
  227. """
  228. Validates an email address. Note that this uses a very primitive regular
  229. expression and should only be used in instances where you later verify by
  230. other means, such as email activation or lookups.
  231. Adds conditions to the document-related queries: the field must match the
  232. pattern.
  233. """
  234. def __init__(self):
  235. super(Email, self).__init__(r'^.+@[^.].*\.[a-z]{2,10}$', re.IGNORECASE)
  236. class IPAddress(Regexp):
  237. """
  238. Validates an IP(v4) address.
  239. Adds conditions to the document-related queries: the field must match the
  240. pattern.
  241. """
  242. def __init__(self):
  243. super(IPAddress, self).__init__(r'^([0-9]{1,3}\.){3}[0-9]{1,3}$')
  244. class URL(Regexp):
  245. """
  246. Simple regexp based url validation. Much like the email validator, you
  247. probably want to validate the url later by other means if the url must
  248. resolve.
  249. :param require_tld:
  250. If true, then the domain-name portion of the URL must contain a .tld
  251. suffix. Set this to false if you want to allow domains like
  252. `localhost`.
  253. Adds conditions to the document-related queries: the field must match the
  254. pattern.
  255. """
  256. def __init__(self, require_tld=True):
  257. tld_part = (require_tld and ur'\.[a-z]{2,10}' or u'')
  258. regex = ur'^[a-z]+://([^/:]+%s|([0-9]{1,3}\.){3}[0-9]{1,3})(:[0-9]+)?(\/.*)?$' % tld_part
  259. super(URL, self).__init__(regex, re.IGNORECASE)
  260. class Unique(object):
  261. def __call__(self, instance, value):
  262. # XXX TODO
  263. raise NotImplementedError
  264. class AnyOf(object):
  265. """
  266. Compares the incoming data to a sequence of valid inputs.
  267. :param choices:
  268. A sequence of valid inputs.
  269. Adds conditions to the document-related queries.
  270. """
  271. def __init__(self, choices):
  272. self.choices = choices
  273. def __call__(self, instance, value):
  274. if value not in self.choices:
  275. raise ValidationError
  276. def filter_query(self, query, name):
  277. return query.where(**{name+'__in': self.choices})
  278. class NoneOf(object):
  279. """
  280. Compares the incoming data to a sequence of invalid inputs.
  281. :param choices:
  282. A sequence of invalid inputs.
  283. Adds conditions to the document-related queries.
  284. """
  285. def __init__(self, choices):
  286. self.choices = choices
  287. def __call__(self, instance, value):
  288. if value in self.choices:
  289. raise ValidationError
  290. def filter_query(self, query, name):
  291. return query.where_not(**{name+'__in': self.choices})
  292. email = Email
  293. equals = Equals
  294. equal_to = EqualTo
  295. exists = Exists
  296. ip_address = IPAddress
  297. length = Length
  298. number_range = NumberRange
  299. optional = Optional
  300. required = Required
  301. regexp = Regexp
  302. # TODO: unique = Unique
  303. url = URL
  304. any_of = AnyOf
  305. none_of = NoneOf