utils.py 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. # -*- coding: utf-8 -*-
  2. from __future__ import unicode_literals
  3. import re
  4. from collections import OrderedDict
  5. from copy import deepcopy
  6. from six import iteritems
  7. from ._http import HTTPStatus
  8. FIRST_CAP_RE = re.compile("(.)([A-Z][a-z]+)")
  9. ALL_CAP_RE = re.compile("([a-z0-9])([A-Z])")
  10. __all__ = (
  11. "merge",
  12. "camel_to_dash",
  13. "default_id",
  14. "not_none",
  15. "not_none_sorted",
  16. "unpack",
  17. )
  18. def merge(first, second):
  19. """
  20. Recursively merges two dictionaries.
  21. Second dictionary values will take precedence over those from the first one.
  22. Nested dictionaries are merged too.
  23. :param dict first: The first dictionary
  24. :param dict second: The second dictionary
  25. :return: the resulting merged dictionary
  26. :rtype: dict
  27. """
  28. if not isinstance(second, dict):
  29. return second
  30. result = deepcopy(first)
  31. for key, value in iteritems(second):
  32. if key in result and isinstance(result[key], dict):
  33. result[key] = merge(result[key], value)
  34. else:
  35. result[key] = deepcopy(value)
  36. return result
  37. def camel_to_dash(value):
  38. """
  39. Transform a CamelCase string into a low_dashed one
  40. :param str value: a CamelCase string to transform
  41. :return: the low_dashed string
  42. :rtype: str
  43. """
  44. first_cap = FIRST_CAP_RE.sub(r"\1_\2", value)
  45. return ALL_CAP_RE.sub(r"\1_\2", first_cap).lower()
  46. def default_id(resource, method):
  47. """Default operation ID generator"""
  48. return "{0}_{1}".format(method, camel_to_dash(resource))
  49. def not_none(data):
  50. """
  51. Remove all keys where value is None
  52. :param dict data: A dictionary with potentially some values set to None
  53. :return: The same dictionary without the keys with values to ``None``
  54. :rtype: dict
  55. """
  56. return dict((k, v) for k, v in iteritems(data) if v is not None)
  57. def not_none_sorted(data):
  58. """
  59. Remove all keys where value is None
  60. :param OrderedDict data: A dictionary with potentially some values set to None
  61. :return: The same dictionary without the keys with values to ``None``
  62. :rtype: OrderedDict
  63. """
  64. return OrderedDict((k, v) for k, v in sorted(iteritems(data)) if v is not None)
  65. def unpack(response, default_code=HTTPStatus.OK):
  66. """
  67. Unpack a Flask standard response.
  68. Flask response can be:
  69. - a single value
  70. - a 2-tuple ``(value, code)``
  71. - a 3-tuple ``(value, code, headers)``
  72. .. warning::
  73. When using this function, you must ensure that the tuple is not the response data.
  74. To do so, prefer returning list instead of tuple for listings.
  75. :param response: A Flask style response
  76. :param int default_code: The HTTP code to use as default if none is provided
  77. :return: a 3-tuple ``(data, code, headers)``
  78. :rtype: tuple
  79. :raise ValueError: if the response does not have one of the expected format
  80. """
  81. if not isinstance(response, tuple):
  82. # data only
  83. return response, default_code, {}
  84. elif len(response) == 1:
  85. # data only as tuple
  86. return response[0], default_code, {}
  87. elif len(response) == 2:
  88. # data and code
  89. data, code = response
  90. return data, code, {}
  91. elif len(response) == 3:
  92. # data, code and headers
  93. data, code, headers = response
  94. return data, code or default_code, headers
  95. else:
  96. raise ValueError("Too many response values")