We have clarified our Privacy Statement. Please have a look at our changes.
Push send, receive post.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

143 lines
6.0KB

  1. """
  2. humanhash: Human-readable representations of digests.
  3. The simplest ways to use this module are the :func:`humanize` and :func:`uuid`
  4. functions. For tighter control over the output, see :class:`HumanHasher`.
  5. """
  6. import operator
  7. import uuid as uuidlib
  8. DEFAULT_WORDLIST = (
  9. 'ack', 'alabama', 'alanine', 'alaska', 'alpha', 'angel', 'apart', 'april',
  10. 'arizona', 'arkansas', 'artist', 'asparagus', 'aspen', 'august', 'autumn',
  11. 'avocado', 'bacon', 'bakerloo', 'batman', 'beer', 'berlin', 'beryllium',
  12. 'black', 'blossom', 'blue', 'bluebird', 'bravo', 'bulldog', 'burger',
  13. 'butter', 'california', 'carbon', 'cardinal', 'carolina', 'carpet', 'cat',
  14. 'ceiling', 'charlie', 'chicken', 'coffee', 'cola', 'cold', 'colorado',
  15. 'comet', 'connecticut', 'crazy', 'cup', 'dakota', 'december', 'delaware',
  16. 'delta', 'diet', 'don', 'double', 'early', 'earth', 'east', 'echo',
  17. 'edward', 'eight', 'eighteen', 'eleven', 'emma', 'enemy', 'equal',
  18. 'failed', 'fanta', 'fifteen', 'fillet', 'finch', 'fish', 'five', 'fix',
  19. 'floor', 'florida', 'football', 'four', 'fourteen', 'foxtrot', 'freddie',
  20. 'friend', 'fruit', 'gee', 'georgia', 'glucose', 'golf', 'green', 'grey',
  21. 'hamper', 'happy', 'harry', 'hawaii', 'helium', 'high', 'hot', 'hotel',
  22. 'hydrogen', 'idaho', 'illinois', 'india', 'indigo', 'ink', 'iowa',
  23. 'island', 'item', 'jersey', 'jig', 'johnny', 'juliet', 'july', 'jupiter',
  24. 'kansas', 'kentucky', 'kilo', 'king', 'kitten', 'lactose', 'lake', 'lamp',
  25. 'lemon', 'leopard', 'lima', 'lion', 'lithium', 'london', 'louisiana',
  26. 'low', 'magazine', 'magnesium', 'maine', 'mango', 'march', 'mars',
  27. 'maryland', 'massachusetts', 'may', 'mexico', 'michigan', 'mike',
  28. 'minnesota', 'mirror', 'mississippi', 'missouri', 'mobile', 'mockingbird',
  29. 'monkey', 'montana', 'moon', 'mountain', 'muppet', 'music', 'nebraska',
  30. 'neptune', 'network', 'nevada', 'nine', 'nineteen', 'nitrogen', 'north',
  31. 'november', 'nuts', 'october', 'ohio', 'oklahoma', 'one', 'orange',
  32. 'oranges', 'oregon', 'oscar', 'oven', 'oxygen', 'papa', 'paris', 'pasta',
  33. 'pennsylvania', 'pip', 'pizza', 'pluto', 'potato', 'princess', 'purple',
  34. 'quebec', 'queen', 'quiet', 'red', 'river', 'robert', 'robin', 'romeo',
  35. 'rugby', 'sad', 'salami', 'saturn', 'september', 'seven', 'seventeen',
  36. 'shade', 'sierra', 'single', 'sink', 'six', 'sixteen', 'skylark', 'snake',
  37. 'social', 'sodium', 'solar', 'south', 'spaghetti', 'speaker', 'spring',
  38. 'stairway', 'steak', 'stream', 'summer', 'sweet', 'table', 'tango', 'ten',
  39. 'tennessee', 'tennis', 'texas', 'thirteen', 'three', 'timing', 'triple',
  40. 'twelve', 'twenty', 'two', 'uncle', 'undress', 'uniform', 'uranus', 'utah',
  41. 'vegan', 'venus', 'vermont', 'victor', 'video', 'violet', 'virginia',
  42. 'washington', 'west', 'whiskey', 'white', 'william', 'winner', 'winter',
  43. 'wisconsin', 'wolfram', 'wyoming', 'xray', 'yankee', 'yellow', 'zebra',
  44. 'zulu')
  45. class HumanHasher(object):
  46. """
  47. Transforms hex digests to human-readable strings.
  48. The format of these strings will look something like:
  49. `victor-bacon-zulu-lima`. The output is obtained by compressing the input
  50. digest to a fixed number of bytes, then mapping those bytes to one of 256
  51. words. A default wordlist is provided, but you can override this if you
  52. prefer.
  53. As long as you use the same wordlist, the output will be consistent (i.e.
  54. the same digest will always render the same representation).
  55. """
  56. def __init__(self, wordlist=DEFAULT_WORDLIST):
  57. if len(wordlist) != 256:
  58. raise ArgumentError("Wordlist must have exactly 256 items")
  59. self.wordlist = wordlist
  60. def humanize(self, hexdigest, words=4, separator='-'):
  61. """
  62. Humanize a given hexadecimal digest.
  63. Change the number of words output by specifying `words`. Change the
  64. word separator with `separator`.
  65. >>> digest = '60ad8d0d871b6095808297'
  66. >>> HumanHasher().humanize(digest)
  67. 'sodium-magnesium-nineteen-hydrogen'
  68. """
  69. # Gets a list of byte values between 0-255.
  70. bytes = map(lambda x: int(x, 16),
  71. map(''.join, zip(hexdigest[::2], hexdigest[1::2])))
  72. # Compress an arbitrary number of bytes to `words`.
  73. compressed = self.compress(bytes, words)
  74. # Map the compressed byte values through the word list.
  75. return separator.join(self.wordlist[byte] for byte in compressed)
  76. @staticmethod
  77. def compress(bytes, target):
  78. """
  79. Compress a list of byte values to a fixed target length.
  80. >>> bytes = [96, 173, 141, 13, 135, 27, 96, 149, 128, 130, 151]
  81. >>> HumanHasher.compress(bytes, 4)
  82. [205, 128, 156, 96]
  83. Attempting to compress a smaller number of bytes to a larger number is
  84. an error:
  85. >>> HumanHasher.compress(bytes, 15) # doctest: +ELLIPSIS
  86. Traceback (most recent call last):
  87. ...
  88. ValueError: Fewer input bytes than requested output
  89. """
  90. length = len(bytes)
  91. if target > length:
  92. raise ValueError("Fewer input bytes than requested output")
  93. # Split `bytes` into `target` segments.
  94. seg_size = length // target
  95. segments = [bytes[i * seg_size:(i + 1) * seg_size]
  96. for i in xrange(target)]
  97. # Catch any left-over bytes in the last segment.
  98. segments[-1].extend(bytes[target * seg_size:])
  99. # Use a simple XOR checksum-like function for compression.
  100. checksum = lambda bytes: reduce(operator.xor, bytes, 0)
  101. checksums = map(checksum, segments)
  102. return checksums
  103. def uuid(self, **params):
  104. """
  105. Generate a UUID with a human-readable representation.
  106. Returns `(human_repr, full_digest)`. Accepts the same keyword arguments
  107. as :meth:`humanize` (they'll be passed straight through).
  108. """
  109. digest = str(uuidlib.uuid4()).replace('-', '')
  110. return self.humanize(digest, **params), digest
  111. DEFAULT_HASHER = HumanHasher()
  112. uuid = DEFAULT_HASHER.uuid
  113. humanize = DEFAULT_HASHER.humanize