humanize.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. from typing import (
  2. Any,
  3. Iterable,
  4. Iterator,
  5. Tuple,
  6. Union,
  7. )
  8. from urllib import (
  9. parse,
  10. )
  11. from eth_typing import (
  12. URI,
  13. Hash32,
  14. )
  15. from eth_utils.currency import (
  16. denoms,
  17. from_wei,
  18. )
  19. from .toolz import (
  20. sliding_window,
  21. take,
  22. )
  23. def humanize_seconds(seconds: Union[float, int]) -> str:
  24. if int(seconds) == 0:
  25. return "0s"
  26. unit_values = _consume_leading_zero_units(_humanize_seconds(int(seconds)))
  27. return "".join((f"{amount}{unit}" for amount, unit in take(3, unit_values)))
  28. SECOND = 1
  29. MINUTE = 60
  30. HOUR = 60 * 60
  31. DAY = 24 * HOUR
  32. YEAR = 365 * DAY
  33. MONTH = YEAR // 12
  34. WEEK = 7 * DAY
  35. UNITS = (
  36. (YEAR, "y"),
  37. (MONTH, "m"),
  38. (WEEK, "w"),
  39. (DAY, "d"),
  40. (HOUR, "h"),
  41. (MINUTE, "m"),
  42. (SECOND, "s"),
  43. )
  44. def _consume_leading_zero_units(
  45. units_iter: Iterator[Tuple[int, str]]
  46. ) -> Iterator[Tuple[int, str]]:
  47. for amount, unit in units_iter:
  48. if amount == 0:
  49. continue
  50. else:
  51. yield (amount, unit)
  52. break
  53. yield from units_iter
  54. def _humanize_seconds(seconds: int) -> Iterator[Tuple[int, str]]:
  55. remainder = seconds
  56. for duration, unit in UNITS:
  57. if not remainder:
  58. break
  59. num = remainder // duration
  60. yield num, unit
  61. remainder %= duration
  62. DISPLAY_HASH_CHARS = 4
  63. def humanize_bytes(value: bytes) -> str:
  64. if len(value) <= DISPLAY_HASH_CHARS + 1:
  65. return value.hex()
  66. value_as_hex = value.hex()
  67. head = value_as_hex[:DISPLAY_HASH_CHARS]
  68. tail = value_as_hex[-1 * DISPLAY_HASH_CHARS :]
  69. return f"{head}..{tail}"
  70. def humanize_hexstr(value: str) -> str:
  71. tail = value[-1 * DISPLAY_HASH_CHARS :]
  72. if value[:2] == "0x":
  73. if len(value[2:]) <= DISPLAY_HASH_CHARS * 2:
  74. return value
  75. head = value[2 : DISPLAY_HASH_CHARS + 2]
  76. return f"0x{head}..{tail}"
  77. else:
  78. if len(value) <= DISPLAY_HASH_CHARS * 2:
  79. return value
  80. head = value[:DISPLAY_HASH_CHARS]
  81. return f"{head}..{tail}"
  82. def humanize_hash(value: Hash32) -> str:
  83. return humanize_bytes(value)
  84. def humanize_ipfs_uri(uri: URI) -> str:
  85. if not is_ipfs_uri(uri):
  86. raise TypeError(
  87. f"{uri} does not look like a valid IPFS uri. Currently, "
  88. "only CIDv0 hash schemes are supported."
  89. )
  90. parsed = parse.urlparse(uri)
  91. ipfs_hash = parsed.netloc
  92. head = ipfs_hash[:DISPLAY_HASH_CHARS]
  93. tail = ipfs_hash[-1 * DISPLAY_HASH_CHARS :]
  94. return f"ipfs://{head}..{tail}"
  95. def is_ipfs_uri(value: Any) -> bool:
  96. if not isinstance(value, str):
  97. return False
  98. parsed = parse.urlparse(value)
  99. if parsed.scheme != "ipfs" or not parsed.netloc:
  100. return False
  101. return _is_CIDv0_ipfs_hash(parsed.netloc)
  102. def _is_CIDv0_ipfs_hash(ipfs_hash: str) -> bool:
  103. if ipfs_hash.startswith("Qm") and len(ipfs_hash) == 46:
  104. return True
  105. return False
  106. def _find_breakpoints(*values: int) -> Iterator[int]:
  107. yield 0
  108. for index, (left, right) in enumerate(sliding_window(2, values), 1):
  109. if left + 1 == right:
  110. continue
  111. else:
  112. yield index
  113. yield len(values)
  114. def _extract_integer_ranges(*values: int) -> Iterator[Tuple[int, int]]:
  115. """
  116. Return a tuple of consecutive ranges of integers.
  117. :param values: a sequence of ordered integers
  118. - fn(1, 2, 3) -> ((1, 3),)
  119. - fn(1, 2, 3, 7, 8, 9) -> ((1, 3), (7, 9))
  120. - fn(1, 7, 8, 9) -> ((1, 1), (7, 9))
  121. """
  122. for left, right in sliding_window(2, _find_breakpoints(*values)):
  123. chunk = values[left:right]
  124. yield chunk[0], chunk[-1]
  125. def _humanize_range(bounds: Tuple[int, int]) -> str:
  126. left, right = bounds
  127. if left == right:
  128. return str(left)
  129. else:
  130. return f"{left}-{right}"
  131. def humanize_integer_sequence(values_iter: Iterable[int]) -> str:
  132. """
  133. Return a concise, human-readable string representing a sequence of integers.
  134. - fn((1, 2, 3)) -> '1-3'
  135. - fn((1, 2, 3, 7, 8, 9)) -> '1-3|7-9'
  136. - fn((1, 2, 3, 5, 7, 8, 9)) -> '1-3|5|7-9'
  137. - fn((1, 7, 8, 9)) -> '1|7-9'
  138. """
  139. values = tuple(values_iter)
  140. if not values:
  141. return "(empty)"
  142. else:
  143. return "|".join(map(_humanize_range, _extract_integer_ranges(*values)))
  144. def humanize_wei(number: int) -> str:
  145. if number >= denoms.finney:
  146. unit = "ether"
  147. elif number >= denoms.mwei:
  148. unit = "gwei"
  149. else:
  150. unit = "wei"
  151. amount = from_wei(number, unit)
  152. x = f"{str(amount)} {unit}"
  153. return x