helpers.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  1. from datetime import datetime, timedelta, tzinfo
  2. from mongomock import InvalidURI
  3. import re
  4. from six.moves.urllib_parse import unquote_plus
  5. from six import iteritems, PY2
  6. import warnings
  7. try:
  8. from bson import ObjectId
  9. except ImportError:
  10. from mongomock.object_id import ObjectId # noqa
  11. try:
  12. from bson import RE_TYPE
  13. except ImportError:
  14. RE_TYPE = re._pattern_type
  15. try:
  16. from bson.tz_util import utc
  17. except ImportError:
  18. class FixedOffset(tzinfo):
  19. def __init__(self, offset, name):
  20. if isinstance(offset, timedelta):
  21. self.__offset = offset
  22. else:
  23. self.__offset = timedelta(minutes=offset)
  24. self.__name = name
  25. def __getinitargs__(self):
  26. return self.__offset, self.__name
  27. def utcoffset(self, dt):
  28. return self.__offset
  29. def tzname(self, dt):
  30. return self.__name
  31. def dst(self, dt):
  32. return timedelta(0)
  33. utc = FixedOffset(0, "UTC")
  34. # for Python 3 compatibility
  35. if PY2:
  36. from __builtin__ import basestring
  37. else:
  38. basestring = (str, bytes)
  39. ASCENDING = 1
  40. def print_deprecation_warning(old_param_name, new_param_name):
  41. warnings.warn(
  42. "'%s' has been deprecated to be in line with pymongo implementation, a new parameter '%s' "
  43. "should be used instead. the old parameter will be kept for backward compatibility "
  44. "purposes." % (old_param_name, new_param_name), DeprecationWarning)
  45. def index_list(key_or_list, direction=None):
  46. """Helper to generate a list of (key, direction) pairs.
  47. It takes such a list, or a single key, or a single key and direction.
  48. """
  49. if direction is not None:
  50. return [(key_or_list, direction)]
  51. else:
  52. if isinstance(key_or_list, basestring):
  53. return [(key_or_list, ASCENDING)]
  54. elif not isinstance(key_or_list, (list, tuple)):
  55. raise TypeError("if no direction is specified, "
  56. "key_or_list must be an instance of list")
  57. return key_or_list
  58. class hashdict(dict):
  59. """hashable dict implementation, suitable for use as a key into other dicts.
  60. >>> h1 = hashdict({"apples": 1, "bananas":2})
  61. >>> h2 = hashdict({"bananas": 3, "mangoes": 5})
  62. >>> h1+h2
  63. hashdict(apples=1, bananas=3, mangoes=5)
  64. >>> d1 = {}
  65. >>> d1[h1] = "salad"
  66. >>> d1[h1]
  67. 'salad'
  68. >>> d1[h2]
  69. Traceback (most recent call last):
  70. ...
  71. KeyError: hashdict(bananas=3, mangoes=5)
  72. based on answers from
  73. http://stackoverflow.com/questions/1151658/python-hashable-dicts
  74. """
  75. def __key(self):
  76. return frozenset((k,
  77. hashdict(v) if type(v) == dict else
  78. tuple(v) if type(v) == list else
  79. v)
  80. for k, v in iteritems(self))
  81. def __repr__(self):
  82. return "{0}({1})".format(
  83. self.__class__.__name__,
  84. ", ".join("{0}={1}".format(str(i[0]), repr(i[1])) for i in self.__key()))
  85. def __hash__(self):
  86. return hash(self.__key())
  87. def __setitem__(self, key, value):
  88. raise TypeError("{0} does not support item assignment"
  89. .format(self.__class__.__name__))
  90. def __delitem__(self, key):
  91. raise TypeError("{0} does not support item assignment"
  92. .format(self.__class__.__name__))
  93. def clear(self):
  94. raise TypeError("{0} does not support item assignment"
  95. .format(self.__class__.__name__))
  96. def pop(self, *args, **kwargs):
  97. raise TypeError("{0} does not support item assignment"
  98. .format(self.__class__.__name__))
  99. def popitem(self, *args, **kwargs):
  100. raise TypeError("{0} does not support item assignment"
  101. .format(self.__class__.__name__))
  102. def setdefault(self, *args, **kwargs):
  103. raise TypeError("{0} does not support item assignment"
  104. .format(self.__class__.__name__))
  105. def update(self, *args, **kwargs):
  106. raise TypeError("{0} does not support item assignment"
  107. .format(self.__class__.__name__))
  108. def __add__(self, right):
  109. result = hashdict(self)
  110. dict.update(result, right)
  111. return result
  112. def _fields_list_to_dict(fields):
  113. """Takes a list of field names and returns a matching dictionary.
  114. ["a", "b"] becomes {"a": 1, "b": 1}
  115. and
  116. ["a.b.c", "d", "a.c"] becomes {"a.b.c": 1, "d": 1, "a.c": 1}
  117. """
  118. as_dict = {}
  119. for field in fields:
  120. if not isinstance(field, basestring):
  121. raise TypeError("fields must be a list of key names, "
  122. "each an instance of %s" % (basestring.__name__,))
  123. as_dict[field] = 1
  124. return as_dict
  125. def parse_dbase_from_uri(uri):
  126. """A simplified version of pymongo.uri_parser.parse_uri to get the dbase.
  127. Returns a string representing the database provided in the URI or None if
  128. no database is provided in the URI.
  129. An invalid MongoDB connection URI may raise an InvalidURI exception,
  130. however, the URI is not fully parsed and some invalid URIs may not result
  131. in an exception.
  132. "mongodb://host1/database" becomes "database"
  133. and
  134. "mongodb://host1" becomes None
  135. """
  136. SCHEME = "mongodb://"
  137. if not uri.startswith(SCHEME):
  138. raise InvalidURI("Invalid URI scheme: URI "
  139. "must begin with '%s'" % (SCHEME,))
  140. scheme_free = uri[len(SCHEME):]
  141. if not scheme_free:
  142. raise InvalidURI("Must provide at least one hostname or IP.")
  143. dbase = None
  144. # Check for unix domain sockets in the uri
  145. if '.sock' in scheme_free:
  146. host_part, _, path_part = scheme_free.rpartition('/')
  147. if not host_part:
  148. host_part = path_part
  149. path_part = ""
  150. if '/' in host_part:
  151. raise InvalidURI("Any '/' in a unix domain socket must be"
  152. " URL encoded: %s" % host_part)
  153. path_part = unquote_plus(path_part)
  154. else:
  155. host_part, _, path_part = scheme_free.partition('/')
  156. if not path_part and '?' in host_part:
  157. raise InvalidURI("A '/' is required between "
  158. "the host list and any options.")
  159. if path_part:
  160. if path_part[0] == '?':
  161. opts = path_part[1:]
  162. else:
  163. dbase, _, opts = path_part.partition('?')
  164. if '.' in dbase:
  165. dbase, _ = dbase.split('.', 1)
  166. if dbase is not None:
  167. dbase = unquote_plus(dbase)
  168. return dbase
  169. def embedded_item_getter(*keys):
  170. """Get items from embedded dictionaries.
  171. use case:
  172. d = {"a": {"b": 1}}
  173. embedded_item_getter("a.b")(d) == 1
  174. :param keys: keys to get
  175. embedded keys are separated with dot in string
  176. :return: itemgetter function
  177. """
  178. def recurse_embedded(obj, key):
  179. ret = obj
  180. for k in key.split('.'):
  181. ret = ret[k]
  182. return ret
  183. if len(keys) == 1:
  184. item = keys[0]
  185. def g(obj):
  186. return recurse_embedded(obj, item)
  187. else:
  188. def g(obj):
  189. return tuple(recurse_embedded(obj, item) for item in keys)
  190. return g
  191. def inplace_patch_datetime_awareness_in_document(doc):
  192. for key in doc.keys():
  193. doc[key] = patch_datetime_awareness_in_document(doc)
  194. def patch_datetime_awareness_in_document(value):
  195. # MongoDB is supposed to stock everything as timezone naive utc date
  196. # Hence we have to convert incoming datetimes to avoid errors while
  197. # mixing tz aware and naive.
  198. # On top of that, MongoDB date precision is up to millisecond, where Python
  199. # datetime use microsecond, so we must lower the precision to mimic mongo.
  200. if isinstance(value, dict):
  201. return {k: patch_datetime_awareness_in_document(v) for k, v in value.items()}
  202. elif isinstance(value, (tuple, list)):
  203. return [patch_datetime_awareness_in_document(item) for item in value]
  204. elif isinstance(value, datetime):
  205. mongo_us = (value.microsecond // 1000) * 1000
  206. if value.tzinfo:
  207. return (value - value.utcoffset()).replace(tzinfo=None, microsecond=mongo_us)
  208. else:
  209. return value.replace(microsecond=mongo_us)
  210. else:
  211. return value
  212. def make_datetime_timezone_aware_in_document(value):
  213. # MongoClient support tz_aware=True parameter to return timezone-aware
  214. # datetime objects. Given the date is stored internally without timezone
  215. # information, all returned datetime have utc as timezone.
  216. if isinstance(value, dict):
  217. return {k: make_datetime_timezone_aware_in_document(v) for k, v in value.items()}
  218. elif isinstance(value, (tuple, list)):
  219. return [make_datetime_timezone_aware_in_document(item) for item in value]
  220. elif isinstance(value, datetime):
  221. return value.replace(tzinfo=utc)
  222. else:
  223. return value