195 lines
5.8 KiB
Python
195 lines
5.8 KiB
Python
# Copyright 2016 The LUCI Authors. All rights reserved.
|
|
# Use of this source code is governed under the Apache License, Version 2.0
|
|
# that can be found in the LICENSE file.
|
|
|
|
import collections
|
|
import re
|
|
import string
|
|
|
|
# third_party/
|
|
from six.moves import urllib
|
|
|
|
_STREAM_SEP = '/'
|
|
_ALNUM_CHARS = string.ascii_letters + string.digits
|
|
_VALID_SEG_CHARS = _ALNUM_CHARS + ':_-.'
|
|
_SEGMENT_RE_BASE = r'[a-zA-Z0-9][a-zA-Z0-9:_\-.]*'
|
|
_SEGMENT_RE = re.compile('^' + _SEGMENT_RE_BASE + '$')
|
|
_STREAM_NAME_RE = re.compile('^(' + _SEGMENT_RE_BASE + ')(/' + _SEGMENT_RE_BASE + ')*$')
|
|
_MAX_STREAM_NAME_LENGTH = 4096
|
|
|
|
_MAX_TAG_KEY_LENGTH = 64
|
|
_MAX_TAG_VALUE_LENGTH = 4096
|
|
|
|
|
|
def validate_stream_name(v, maxlen=None):
|
|
"""Verifies that a given stream name is valid.
|
|
|
|
Args:
|
|
v (str): The stream name string.
|
|
|
|
|
|
Raises:
|
|
ValueError if the stream name is invalid.
|
|
"""
|
|
maxlen = maxlen or _MAX_STREAM_NAME_LENGTH
|
|
if len(v) > maxlen:
|
|
raise ValueError('Maximum length exceeded (%d > %d)' % (len(v), maxlen))
|
|
if _STREAM_NAME_RE.match(v) is None:
|
|
raise ValueError('Invalid stream name: %r' % v)
|
|
|
|
|
|
def validate_tag(key, value):
|
|
"""Verifies that a given tag key/value is valid.
|
|
|
|
Args:
|
|
k (str): The tag key.
|
|
v (str): The tag value.
|
|
|
|
Raises:
|
|
ValueError if the tag is not valid.
|
|
"""
|
|
validate_stream_name(key, maxlen=_MAX_TAG_KEY_LENGTH)
|
|
validate_stream_name(value, maxlen=_MAX_TAG_VALUE_LENGTH)
|
|
|
|
|
|
def normalize_segment(seg, prefix=None):
|
|
"""Given a string, mutate it into a valid segment name.
|
|
|
|
This operates by replacing invalid segment name characters with underscores
|
|
(_) when encountered.
|
|
|
|
A special case is when "seg" begins with non-alphanumeric character. In this
|
|
case, we will prefix it with the "prefix", if one is supplied. Otherwise,
|
|
raises ValueError.
|
|
|
|
See _VALID_SEG_CHARS for all valid characters for a segment.
|
|
|
|
Raises:
|
|
ValueError: If normalization could not be successfully performed.
|
|
"""
|
|
if not seg:
|
|
if prefix is None:
|
|
raise ValueError('Cannot normalize empty segment with no prefix.')
|
|
seg = prefix
|
|
else:
|
|
|
|
def replace_if_invalid(ch, first=False):
|
|
ret = ch if ch in _VALID_SEG_CHARS else '_'
|
|
if first and ch not in _ALNUM_CHARS:
|
|
if prefix is None:
|
|
raise ValueError('Segment has invalid beginning, and no prefix was '
|
|
'provided.')
|
|
return prefix + ret
|
|
return ret
|
|
|
|
seg = ''.join(replace_if_invalid(ch, i == 0) for i, ch in enumerate(seg))
|
|
|
|
if _SEGMENT_RE.match(seg) is None:
|
|
raise AssertionError('Normalized segment is still invalid: %r' % seg)
|
|
|
|
return seg
|
|
|
|
|
|
def normalize(v, prefix=None):
|
|
"""Given a string, mutate it into a valid stream name.
|
|
|
|
This operates by replacing invalid stream name characters with underscores (_)
|
|
when encountered.
|
|
|
|
A special case is when any segment of "v" begins with an non-alphanumeric
|
|
character. In this case, we will prefix the segment with the "prefix", if one
|
|
is supplied. Otherwise, raises ValueError.
|
|
|
|
See _STREAM_NAME_RE for a description of a valid stream name.
|
|
|
|
Raises:
|
|
ValueError: If normalization could not be successfully performed.
|
|
"""
|
|
normalized = _STREAM_SEP.join(
|
|
normalize_segment(seg, prefix=prefix) for seg in v.split(_STREAM_SEP))
|
|
# Validate the resulting string.
|
|
validate_stream_name(normalized)
|
|
return normalized
|
|
|
|
|
|
class StreamPath(collections.namedtuple('_StreamPath', ('prefix', 'name'))):
|
|
"""StreamPath is a full stream path.
|
|
|
|
This consists of both a stream prefix and a stream name.
|
|
|
|
When constructed with parse or make, the stream path must be completely valid.
|
|
However, invalid stream paths may be constructed by manually instantiation.
|
|
This can be useful for wildcard query values (e.g., "prefix='foo/*/bar/**'").
|
|
"""
|
|
|
|
@classmethod
|
|
def make(cls, prefix, name):
|
|
"""Returns (StreamPath): The validated StreamPath instance.
|
|
|
|
Args:
|
|
prefix (str): the prefix component
|
|
name (str): the name component
|
|
|
|
Raises:
|
|
ValueError: If path is not a full, valid stream path string.
|
|
"""
|
|
inst = cls(prefix=prefix, name=name)
|
|
inst.validate()
|
|
return inst
|
|
|
|
@classmethod
|
|
def parse(cls, path):
|
|
"""Returns (StreamPath): The parsed StreamPath instance.
|
|
|
|
Args:
|
|
path (str): the full stream path to parse.
|
|
|
|
Raises:
|
|
ValueError: If path is not a full, valid stream path string.
|
|
"""
|
|
parts = path.split('/+/', 1)
|
|
if len(parts) != 2:
|
|
raise ValueError('Not a full stream path: [%s]' % (path,))
|
|
return cls.make(*parts)
|
|
|
|
def validate(self):
|
|
"""Raises: ValueError if this is not a valid stream name."""
|
|
try:
|
|
validate_stream_name(self.prefix)
|
|
except ValueError as e:
|
|
raise ValueError('Invalid prefix component [%s]: %s' % (
|
|
self.prefix,
|
|
e,
|
|
))
|
|
|
|
try:
|
|
validate_stream_name(self.name)
|
|
except ValueError as e:
|
|
raise ValueError('Invalid name component [%s]: %s' % (
|
|
self.name,
|
|
e,
|
|
))
|
|
|
|
def __str__(self):
|
|
return '%s/+/%s' % (self.prefix, self.name)
|
|
|
|
|
|
def get_logdog_viewer_url(host, project, *stream_paths):
|
|
"""Returns (str): The LogDog viewer URL for the named stream(s).
|
|
|
|
Args:
|
|
host (str): The name of the Coordiantor host.
|
|
project (str): The project name.
|
|
stream_paths: A set of StreamPath instances for the stream paths to
|
|
generate the URL for.
|
|
"""
|
|
return urllib.parse.urlunparse((
|
|
'https', # Scheme
|
|
host, # netloc
|
|
'v/', # path
|
|
'', # params
|
|
'&'.join(('s=%s' % (urllib.parse.quote('%s/%s' % (project, path), ''))
|
|
for path in stream_paths)), # query
|
|
'', # fragment
|
|
))
|