streamname.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. # Copyright 2016 The LUCI Authors. All rights reserved.
  2. # Use of this source code is governed under the Apache License, Version 2.0
  3. # that can be found in the LICENSE file.
  4. import collections
  5. import re
  6. import string
  7. # third_party/
  8. from six.moves import urllib
  9. _STREAM_SEP = '/'
  10. _ALNUM_CHARS = string.ascii_letters + string.digits
  11. _VALID_SEG_CHARS = _ALNUM_CHARS + ':_-.'
  12. _SEGMENT_RE_BASE = r'[a-zA-Z0-9][a-zA-Z0-9:_\-.]*'
  13. _SEGMENT_RE = re.compile('^' + _SEGMENT_RE_BASE + '$')
  14. _STREAM_NAME_RE = re.compile('^(' + _SEGMENT_RE_BASE + ')(/' +
  15. _SEGMENT_RE_BASE + ')*$')
  16. _MAX_STREAM_NAME_LENGTH = 4096
  17. _MAX_TAG_KEY_LENGTH = 64
  18. _MAX_TAG_VALUE_LENGTH = 4096
  19. def validate_stream_name(v, maxlen=None):
  20. """Verifies that a given stream name is valid.
  21. Args:
  22. v (str): The stream name string.
  23. Raises:
  24. ValueError if the stream name is invalid.
  25. """
  26. maxlen = maxlen or _MAX_STREAM_NAME_LENGTH
  27. if len(v) > maxlen:
  28. raise ValueError('Maximum length exceeded (%d > %d)' % (len(v), maxlen))
  29. if _STREAM_NAME_RE.match(v) is None:
  30. raise ValueError('Invalid stream name: %r' % v)
  31. def validate_tag(key, value):
  32. """Verifies that a given tag key/value is valid.
  33. Args:
  34. k (str): The tag key.
  35. v (str): The tag value.
  36. Raises:
  37. ValueError if the tag is not valid.
  38. """
  39. validate_stream_name(key, maxlen=_MAX_TAG_KEY_LENGTH)
  40. validate_stream_name(value, maxlen=_MAX_TAG_VALUE_LENGTH)
  41. def normalize_segment(seg, prefix=None):
  42. """Given a string, mutate it into a valid segment name.
  43. This operates by replacing invalid segment name characters with underscores
  44. (_) when encountered.
  45. A special case is when "seg" begins with non-alphanumeric character. In this
  46. case, we will prefix it with the "prefix", if one is supplied. Otherwise,
  47. raises ValueError.
  48. See _VALID_SEG_CHARS for all valid characters for a segment.
  49. Raises:
  50. ValueError: If normalization could not be successfully performed.
  51. """
  52. if not seg:
  53. if prefix is None:
  54. raise ValueError('Cannot normalize empty segment with no prefix.')
  55. seg = prefix
  56. else:
  57. def replace_if_invalid(ch, first=False):
  58. ret = ch if ch in _VALID_SEG_CHARS else '_'
  59. if first and ch not in _ALNUM_CHARS:
  60. if prefix is None:
  61. raise ValueError('Segment has invalid beginning, and no prefix was '
  62. 'provided.')
  63. return prefix + ret
  64. return ret
  65. seg = ''.join(replace_if_invalid(ch, i == 0) for i, ch in enumerate(seg))
  66. if _SEGMENT_RE.match(seg) is None:
  67. raise AssertionError('Normalized segment is still invalid: %r' % seg)
  68. return seg
  69. def normalize(v, prefix=None):
  70. """Given a string, mutate it into a valid stream name.
  71. This operates by replacing invalid stream name characters with underscores (_)
  72. when encountered.
  73. A special case is when any segment of "v" begins with an non-alphanumeric
  74. character. In this case, we will prefix the segment with the "prefix", if one
  75. is supplied. Otherwise, raises ValueError.
  76. See _STREAM_NAME_RE for a description of a valid stream name.
  77. Raises:
  78. ValueError: If normalization could not be successfully performed.
  79. """
  80. normalized = _STREAM_SEP.join(
  81. normalize_segment(seg, prefix=prefix) for seg in v.split(_STREAM_SEP))
  82. # Validate the resulting string.
  83. validate_stream_name(normalized)
  84. return normalized
  85. class StreamPath(collections.namedtuple('_StreamPath', ('prefix', 'name'))):
  86. """StreamPath is a full stream path.
  87. This consists of both a stream prefix and a stream name.
  88. When constructed with parse or make, the stream path must be completely valid.
  89. However, invalid stream paths may be constructed by manually instantiation.
  90. This can be useful for wildcard query values (e.g., "prefix='foo/*/bar/**'").
  91. """
  92. @classmethod
  93. def make(cls, prefix, name):
  94. """Returns (StreamPath): The validated StreamPath instance.
  95. Args:
  96. prefix (str): the prefix component
  97. name (str): the name component
  98. Raises:
  99. ValueError: If path is not a full, valid stream path string.
  100. """
  101. inst = cls(prefix=prefix, name=name)
  102. inst.validate()
  103. return inst
  104. @classmethod
  105. def parse(cls, path):
  106. """Returns (StreamPath): The parsed StreamPath instance.
  107. Args:
  108. path (str): the full stream path to parse.
  109. Raises:
  110. ValueError: If path is not a full, valid stream path string.
  111. """
  112. parts = path.split('/+/', 1)
  113. if len(parts) != 2:
  114. raise ValueError('Not a full stream path: [%s]' % (path,))
  115. return cls.make(*parts)
  116. def validate(self):
  117. """Raises: ValueError if this is not a valid stream name."""
  118. try:
  119. validate_stream_name(self.prefix)
  120. except ValueError as e:
  121. raise ValueError('Invalid prefix component [%s]: %s' % (self.prefix, e,))
  122. try:
  123. validate_stream_name(self.name)
  124. except ValueError as e:
  125. raise ValueError('Invalid name component [%s]: %s' % (self.name, e,))
  126. def __str__(self):
  127. return '%s/+/%s' % (self.prefix, self.name)
  128. def get_logdog_viewer_url(host, project, *stream_paths):
  129. """Returns (str): The LogDog viewer URL for the named stream(s).
  130. Args:
  131. host (str): The name of the Coordiantor host.
  132. project (str): The project name.
  133. stream_paths: A set of StreamPath instances for the stream paths to
  134. generate the URL for.
  135. """
  136. return urllib.parse.urlunparse((
  137. 'https', # Scheme
  138. host, # netloc
  139. 'v/', # path
  140. '', # params
  141. '&'.join(('s=%s' % (urllib.parse.quote('%s/%s' % (project, path), ''))
  142. for path in stream_paths)), # query
  143. '', # fragment
  144. ))