PageRenderTime 49ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/doqu/ext/mongodb/lookups.py

https://bitbucket.org/aprilmay/doqu-h
Python | 155 lines | 98 code | 19 blank | 38 comment | 8 complexity | 69268db548c16f36eb7f59ffb35e9ac1 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. from functools import wraps
  21. import re
  22. from doqu.backend_base import LookupManager
  23. __all__ = ['lookup_manager']
  24. class MongoLookupManager(LookupManager):
  25. """
  26. Lookup manager for the Doqu's MongoDB adapter.
  27. """
  28. def combine_conditions(self, conditions):
  29. """
  30. Expects a list of conditions, each returned by a lookup processor from
  31. the Doqu MongoDB adapter.
  32. Returns the resulting `query document`_ ("spec").
  33. .. _query document: http://mongodb.org/display/DOCS/Querying
  34. """
  35. # we merge all conditions into a single dictionary; calling find() in a
  36. # sequence may be a better idea(?) because smth like:
  37. # [{'foo': {'$gt': 0}}, {'foo': {'$lt': 5}}]
  38. # will yield an equivalent of `foo < 5` instead of `0 < foo < 5`.
  39. # We try to alleviate this issue by respecting an extra level but a
  40. # more complex structure can be crippled.
  41. spec = {}
  42. for condition in conditions:
  43. for name, clause in condition.iteritems():
  44. if isinstance(clause, dict):
  45. spec.setdefault(name, {}).update(clause)
  46. else:
  47. # exact or regex. Specifying multiple conditions against
  48. # same fields will result in name clashes so we try to
  49. # avoid that by wrapping "simple" conditions in an array.
  50. # Note that this doesn't remove all possible problems, just
  51. # the most common ones.
  52. conds = spec.setdefault(name, {}).setdefault('$all', [])
  53. conds.append(clause)
  54. #print 'MONGO spec', spec
  55. return spec
  56. lookup_manager = MongoLookupManager()
  57. DEFAULT_OPERATION = 'equals'
  58. #----------------------------------------------------------------------------------
  59. # See http://www.mongodb.org/display/DOCS/Advanced+Queries
  60. #
  61. lookup_processors = {
  62. 'contains': lambda v: (
  63. ('$all', [re.compile(x) for x in v])
  64. if isinstance(v, (list,tuple))
  65. else lookup_processors['matches'](v)
  66. ),
  67. 'contains_any': lambda v: ('$in', [re.compile(x) for x in v]),
  68. 'endswith': lambda v: (None, re.compile('{0}$'.format(v))),
  69. 'equals': lambda v: (None, v),
  70. 'exists': lambda v: ('$exists', v),
  71. 'gt': lambda v: ('$gt', v),
  72. 'gte': lambda v: ('$gte', v),
  73. 'in': lambda v: ('$in', v),
  74. # 'like': lambda a,b: NotImplemented,
  75. # 'like_any': lambda a,b: NotImplemented,
  76. 'lt': lambda v: ('$lt', v),
  77. 'lte': lambda v: ('$lte', v),
  78. 'matches': lambda v: (None, re.compile(v)),
  79. # TODO: implement this lookup in other backends
  80. 'matches_caseless': lambda v: (None, re.compile(v, re.IGNORECASE)),
  81. # 'search': lambda a,b: NotImplemented,
  82. 'startswith': lambda v: (None, re.compile('^{0}'.format(v))),
  83. 'year': lambda v: (None, re.compile(r'^{0}....'.format(v))),
  84. 'month': lambda v: (None, re.compile(r'^....{0:02}..'.format(v))),
  85. 'day': lambda v: (None, re.compile(r'^......{0:02}'.format(v))),
  86. }
  87. meta_lookups = {
  88. 'between': lambda values: [('gte', values[0]),
  89. ('lte', values[1])],
  90. }
  91. inline_negation = {
  92. 'equals': '$ne',
  93. 'in': '$nin',
  94. # XXX be careful with gt/lt/gte/lte: "not < 2" != "> 2"
  95. }
  96. def autonegated_lookup(processor, operation):
  97. "wrapper for lookup processors; handles negation"
  98. @wraps(processor)
  99. def inner(name, value, data_processor, negated):
  100. op, val = processor(value, data_processor)
  101. expr = {op: val} if op else val
  102. if negated:
  103. neg = inline_negation.get(operation)
  104. if neg:
  105. return {name: {neg: val}}
  106. return {name: {'$not': expr}}
  107. return {name: expr}
  108. return inner
  109. def autocoersed_lookup(processor):
  110. "wrapper for lookup processors; handles value coersion"
  111. @wraps(processor)
  112. def inner(value, data_processor): # negation to be handled outside
  113. return processor(data_processor(value))
  114. return inner
  115. def meta_lookup(processor):
  116. """
  117. A wrapper for lookup processors. Delegates the task to multiple simple
  118. lookup processors (e.g. "between 1,3" can generate lookups "gt 1", "lt 3").
  119. """
  120. @wraps(processor)
  121. def inner(name, value, data_processor, negated):
  122. pairs = processor(value)
  123. for _operation, _value in pairs:
  124. p = lookup_manager.get_processor(_operation)
  125. yield p(name, _value, data_processor, negated)
  126. return inner
  127. for operation, processor in lookup_processors.items():
  128. is_default = operation == DEFAULT_OPERATION
  129. processor = autocoersed_lookup(processor)
  130. processor = autonegated_lookup(processor, operation)
  131. lookup_manager.register(operation, default=is_default)(processor)
  132. for operation, mapper in meta_lookups.items():
  133. processor = meta_lookup(mapper)#, operation)
  134. lookup_manager.register(operation)(processor)