Exemple #1
0
    def execute(self, bug, current_event):
        """See `IEmailCommand`."""
        # Tags are always lowercase.
        string_args = [arg.lower() for arg in self.string_args]
        if bug.tags is None:
            tags = []
        else:
            tags = list(bug.tags)
        for arg in string_args:
            # Are we adding or removing a tag?
            if arg.startswith('-'):
                remove = True
                tag = arg[1:]
            else:
                remove = False
                tag = arg
            # Tag must be a valid name.
            if not valid_name(tag):
                raise EmailProcessingError(
                    get_error_message('invalid-tag.txt',
                                      error_templates=error_templates,
                                      tag=tag))
            if remove:
                try:
                    tags.remove(tag)
                except ValueError:
                    raise EmailProcessingError(
                        get_error_message('unassigned-tag.txt',
                                          error_templates=error_templates,
                                          tag=tag))
            else:
                tags.append(arg)
        bug.tags = tags

        return bug, current_event
Exemple #2
0
    def traverse(self, owner, name, segments):
        """See `IGitTraversable`.

        :raises InvalidNamespace: If 'name' is '+git' and there are no
            further segments.
        :raises InvalidProductName: If 'name' is not '+git' and is not a
            valid name.
        :raises NoSuchGitRepository: If the segment after '+git' doesn't
            match an existing Git repository.
        :raises NoSuchProduct: If 'name' is not '+git' and doesn't match an
            existing pillar.
        :return: A tuple of (`IPerson`, `IHasGitRepositories`,
            `IGitRepository`).
        """
        if name == "+git":
            return super(PersonGitTraversable,
                         self).traverse(owner, name, segments)
        else:
            if not valid_name(name):
                raise InvalidProductName(name)
            pillar = getUtility(IPillarNameSet).getByName(name)
            if pillar is None:
                # Actually, the pillar is no such *anything*.
                raise NoSuchProduct(name)
            return owner, pillar, None
 def test_template_name(self):
     # The name is derived from the file name and must be a valid name.
     translation_domain = u"Invalid-Name_with illegal#Characters"
     template_path = translation_domain + u".pot"
     entry = self._upload_file(template_path)
     self._createApprover(template_path).approve(entry)
     self.assertTrue(valid_name(entry.potemplate.name))
     self.assertEqual(u"invalid-name-withillegalcharacters", entry.potemplate.name)
Exemple #4
0
    def createSubmission(
        self,
        date_created,
        format,
        private,
        contactable,
        submission_key,
        emailaddress,
        distroarchseries,
        raw_submission,
        filename,
        filesize,
        system_fingerprint,
    ):
        """See `IHWSubmissionSet`."""
        assert valid_name(submission_key), "Invalid key %s" % submission_key

        submission_exists = HWSubmission.selectOneBy(submission_key=submission_key)
        if submission_exists is not None:
            raise HWSubmissionKeyNotUnique("A submission with this ID already exists")

        personset = getUtility(IPersonSet)
        if emailaddress is not None:
            owner = personset.getByEmail(emailaddress)
        else:
            owner = None

        fingerprint = HWSystemFingerprint.selectOneBy(fingerprint=system_fingerprint)
        if fingerprint is None:
            fingerprint = HWSystemFingerprint(fingerprint=system_fingerprint)

        libraryfileset = getUtility(ILibraryFileAliasSet)
        libraryfile = libraryfileset.create(
            name=filename,
            size=filesize,
            file=raw_submission,
            # XXX: kiko 2007-09-20: The hwdb client sends us bzipped XML, but
            # arguably other clients could send us other formats. The right
            # way to do this is either to enforce the format in the browser
            # code, allow the client to specify the format, or use a
            # magic module to sniff what it is we got.
            contentType="application/x-bzip2",
            expires=None,
        )

        return HWSubmission(
            date_created=date_created,
            format=format,
            status=HWSubmissionProcessingStatus.SUBMITTED,
            private=private,
            contactable=contactable,
            submission_key=submission_key,
            owner=owner,
            distroarchseries=distroarchseries,
            raw_submission=libraryfile,
            system_fingerprint=fingerprint,
            raw_emailaddress=emailaddress,
        )
Exemple #5
0
 def test_template_name(self):
     # The name is derived from the file name and must be a valid name.
     translation_domain = (u'Invalid-Name_with illegal#Characters')
     template_path = translation_domain + u'.pot'
     entry = self._upload_file(template_path)
     self._createApprover(template_path).approve(entry)
     self.assertTrue(valid_name(entry.potemplate.name))
     self.assertEqual(u'invalid-name-withillegalcharacters',
                      entry.potemplate.name)
Exemple #6
0
    def validate(self, data):
        """Create new team if necessary."""
        personset = getUtility(IPersonSet)
        request = self.request
        owner_name = request.form.get(self.owner_widget.name)
        if not owner_name:
            self.setFieldError(
                'owner',
                "You have to specify the name of the person/team that's "
                "going to be the new %s." % self.ownerOrMaintainerName)
            return None

        if request.form.get('field.existing') == 'existing':
            try:
                # By getting the owner using getInputValue() we make sure
                # it's valid according to the vocabulary of self.schema's
                # owner widget.
                owner = self.owner_widget.getInputValue()
            except WidgetInputError:
                self.setFieldError(
                    'owner',
                    "The person/team named '%s' is not a valid owner for %s." %
                    (owner_name, self.contextName))
                return None
            except ConversionError:
                self.setFieldError(
                    self.ownerOrMaintainerName,
                    "There's no person/team named '%s' in Launchpad." %
                    owner_name)
                return None
        else:
            if personset.getByName(owner_name):
                self.setFieldError(
                    'owner',
                    "There's already a person/team with the name '%s' in "
                    "Launchpad. Please choose a different name or select "
                    "the option to make that person/team the new owner, "
                    "if that's what you want." % owner_name)
                return None

            if not valid_name(owner_name):
                self.setFieldError(
                    'owner',
                    "'%s' is not a valid name for a team. Please make sure "
                    "it contains only the allowed characters and no spaces." %
                    owner_name)
                return None

            owner = personset.newTeam(self.user, owner_name,
                                      owner_name.capitalize())

        self.validateOwner(owner)
    def validate(self, data):
        """Create new team if necessary."""
        personset = getUtility(IPersonSet)
        request = self.request
        owner_name = request.form.get(self.owner_widget.name)
        if not owner_name:
            self.setFieldError(
                'owner',
                "You have to specify the name of the person/team that's "
                "going to be the new %s." % self.ownerOrMaintainerName)
            return None

        if request.form.get('field.existing') == 'existing':
            try:
                # By getting the owner using getInputValue() we make sure
                # it's valid according to the vocabulary of self.schema's
                # owner widget.
                owner = self.owner_widget.getInputValue()
            except WidgetInputError:
                self.setFieldError(
                    'owner',
                    "The person/team named '%s' is not a valid owner for %s."
                    % (owner_name, self.contextName))
                return None
            except ConversionError:
                self.setFieldError(
                    self.ownerOrMaintainerName,
                    "There's no person/team named '%s' in Launchpad."
                    % owner_name)
                return None
        else:
            if personset.getByName(owner_name):
                self.setFieldError(
                    'owner',
                    "There's already a person/team with the name '%s' in "
                    "Launchpad. Please choose a different name or select "
                    "the option to make that person/team the new owner, "
                    "if that's what you want." % owner_name)
                return None

            if not valid_name(owner_name):
                self.setFieldError(
                    'owner',
                    "'%s' is not a valid name for a team. Please make sure "
                    "it contains only the allowed characters and no spaces."
                    % owner_name)
                return None

            owner = personset.newTeam(
                self.user, owner_name, owner_name.capitalize())

        self.validateOwner(owner)
    def mainTask(self):
        """Main function entry point."""
        opts = self.options

        if not specified(opts.from_distribution):
            raise SoyuzScriptError(
                "error: origin distribution not specified.")

        if not specified(opts.to_distribution):
            raise SoyuzScriptError(
                "error: destination distribution not specified.")

        if not specified(opts.to_archive):
            raise SoyuzScriptError(
                "error: destination copy archive not specified.")
        if not valid_name(opts.to_archive):
            raise SoyuzScriptError(
                "Invalid destination archive name: '%s'" % opts.to_archive)
        if opts.include_binaries:
            raise SoyuzScriptError(
                "error: copying of binary packages is not supported yet.")

        if (specified(opts.from_user) and not specified(opts.from_archive)):
            opts.from_archive = 'ppa'

        if specified(opts.from_archive) and not valid_name(opts.from_archive):
            raise SoyuzScriptError(
                "Invalid origin archive name: '%s'" % opts.from_archive)

        # For the love of $DEITY, WTF doesn't this method just accept a
        # single parameter "opts" ...
        self.populateArchive(
            opts.from_archive, opts.from_distribution, opts.from_suite,
            opts.from_user, opts.component, opts.to_distribution,
            opts.to_suite, opts.to_archive, opts.to_user, opts.reason,
            opts.include_binaries, opts.arch_tags, opts.merge_copy_flag,
            opts.packageset_delta_flag, opts.packageset_tags,
            opts.nonvirtualized)
    def mainTask(self):
        """Main function entry point."""
        opts = self.options

        if not specified(opts.from_distribution):
            raise SoyuzScriptError(
                "error: origin distribution not specified.")

        if not specified(opts.to_distribution):
            raise SoyuzScriptError(
                "error: destination distribution not specified.")

        if not specified(opts.to_archive):
            raise SoyuzScriptError(
                "error: destination copy archive not specified.")
        if not valid_name(opts.to_archive):
            raise SoyuzScriptError(
                "Invalid destination archive name: '%s'" % opts.to_archive)
        if opts.include_binaries:
            raise SoyuzScriptError(
                "error: copying of binary packages is not supported yet.")

        if (specified(opts.from_user) and not specified(opts.from_archive)):
            opts.from_archive = 'ppa'

        if specified(opts.from_archive) and not valid_name(opts.from_archive):
            raise SoyuzScriptError(
                "Invalid origin archive name: '%s'" % opts.from_archive)

        # For the love of $DEITY, WTF doesn't this method just accept a
        # single parameter "opts" ...
        self.populateArchive(
            opts.from_archive, opts.from_distribution, opts.from_suite,
            opts.from_user, opts.component, opts.to_distribution,
            opts.to_suite, opts.to_archive, opts.to_user, opts.reason,
            opts.include_binaries, opts.arch_tags, opts.merge_copy_flag,
            opts.packageset_delta_flag, opts.packageset_tags,
            opts.nonvirtualized)
 def _validatePOT(self, data):
     name = data.get("name")
     translation_domain = data.get("translation_domain")
     if name is None:
         self.setFieldError("name", "Please specify a name for " "the template.")
     elif not valid_name(name):
         self.setFieldError(
             "name",
             "Please specify a valid name for "
             "the template. Names must be all lower "
             "case and start with a letter or number.",
         )
     if translation_domain is None:
         self.setFieldError("translation_domain", "Please specify a " "translation domain for the template.")
Exemple #11
0
    def createSubmission(self, date_created, format, private, contactable,
                         submission_key, emailaddress, distroarchseries,
                         raw_submission, filename, filesize,
                         system_fingerprint):
        """See `IHWSubmissionSet`."""
        assert valid_name(submission_key), "Invalid key %s" % submission_key

        submission_exists = HWSubmission.selectOneBy(
            submission_key=submission_key)
        if submission_exists is not None:
            raise HWSubmissionKeyNotUnique(
                'A submission with this ID already exists')

        personset = getUtility(IPersonSet)
        if emailaddress is not None:
            owner = personset.getByEmail(emailaddress)
        else:
            owner = None

        fingerprint = HWSystemFingerprint.selectOneBy(
            fingerprint=system_fingerprint)
        if fingerprint is None:
            fingerprint = HWSystemFingerprint(fingerprint=system_fingerprint)

        libraryfileset = getUtility(ILibraryFileAliasSet)
        libraryfile = libraryfileset.create(
            name=filename,
            size=filesize,
            file=raw_submission,
            # XXX: kiko 2007-09-20: The hwdb client sends us bzipped XML, but
            # arguably other clients could send us other formats. The right
            # way to do this is either to enforce the format in the browser
            # code, allow the client to specify the format, or use a
            # magic module to sniff what it is we got.
            contentType='application/x-bzip2',
            expires=None)

        return HWSubmission(
            date_created=date_created,
            format=format,
            status=HWSubmissionProcessingStatus.SUBMITTED,
            private=private,
            contactable=contactable,
            submission_key=submission_key,
            owner=owner,
            distroarchseries=distroarchseries,
            raw_submission=libraryfile,
            system_fingerprint=fingerprint,
            raw_emailaddress=emailaddress)
 def _validatePOT(self, data):
     name = data.get('name')
     translation_domain = data.get('translation_domain')
     if name is None:
         self.setFieldError('name', 'Please specify a name for '
                            'the template.')
     elif not valid_name(name):
         self.setFieldError(
             'name', 'Please specify a valid name for '
             'the template. Names must be all lower '
             'case and start with a letter or number.')
     if translation_domain is None:
         self.setFieldError(
             'translation_domain', 'Please specify a '
             'translation domain for the template.')
    def traverse(self, name, segments):
        """See `ITraversable`.

        :raise NoSuchProduct: If 'name' doesn't match an existing pillar.
        :return: `IPillar`.
        """
        if not valid_name(name):
            raise InvalidProductName(name)
        pillar = getUtility(IPillarNameSet).getByName(name)
        if pillar is None:
            # Actually, the pillar is no such *anything*. The user might be
            # trying to refer to a project, a distribution or a product. We
            # raise a NoSuchProduct error since that's what we used to raise
            # when we only supported product & junk branches.
            raise NoSuchProduct(name)
        return pillar
    def traverse(self, name, segments):
        """See `ITraversable`.

        :raise NoSuchProduct: If 'name' doesn't match an existing pillar.
        :return: `IPillar`.
        """
        if not valid_name(name):
            raise InvalidProductName(name)
        pillar = getUtility(IPillarNameSet).getByName(name)
        if pillar is None:
            # Actually, the pillar is no such *anything*. The user might be
            # trying to refer to a project, a distribution or a product. We
            # raise a NoSuchProduct error since that's what we used to raise
            # when we only supported product & junk branches.
            raise NoSuchProduct(name)
        return pillar
Exemple #15
0
    def validate(self, data):
        name = data.get('name', None)
        if name is None or not valid_name(name):
            self.setFieldError(
                'name',
                'Template name can only start with lowercase letters a-z '
                'or digits 0-9, and other than those characters, can only '
                'contain "-", "+" and "." characters.')

        distroseries = data.get('distroseries', self.context.distroseries)
        sourcepackagename = data.get('sourcepackagename',
                                     self.context.sourcepackagename)
        if IDistributionSourcePackage.providedBy(sourcepackagename):
            sourcepackagename = sourcepackagename.sourcepackagename
        productseries = data.get('productseries', None)
        sourcepackage_changed = (
            distroseries is not None
            and (distroseries != self.context.distroseries
                 or sourcepackagename != self.context.sourcepackagename))
        productseries_changed = (productseries is not None and
                                 productseries != self.context.productseries)
        similar_templates = self._validateTargetAndGetTemplates(data)
        if similar_templates is not None:
            self.validateName(name, similar_templates, sourcepackage_changed,
                              productseries_changed)
            self.validateDomain(data.get('translation_domain'),
                                similar_templates, sourcepackage_changed,
                                productseries_changed)

        priority = data.get('priority')
        if priority is None:
            return

        if (priority < self.PRIORITY_MIN_VALUE
                or priority > self.PRIORITY_MAX_VALUE):
            self.setFieldError(
                'priority', 'The priority value must be between %s and %s.' %
                (self.PRIORITY_MIN_VALUE, self.PRIORITY_MAX_VALUE))
    def validate(self, data):
        name = data.get('name', None)
        if name is None or not valid_name(name):
            self.setFieldError(
                'name',
                'Template name can only start with lowercase letters a-z '
                'or digits 0-9, and other than those characters, can only '
                'contain "-", "+" and "." characters.')

        distroseries = data.get('distroseries', self.context.distroseries)
        sourcepackagename = data.get(
            'sourcepackagename', self.context.sourcepackagename)
        productseries = data.get('productseries', None)
        sourcepackage_changed = (
            distroseries is not None and
            (distroseries != self.context.distroseries or
             sourcepackagename != self.context.sourcepackagename))
        productseries_changed = (productseries is not None and
                                 productseries != self.context.productseries)
        similar_templates = self._validateTargetAndGetTemplates(data)
        if similar_templates is not None:
            self.validateName(
                name, similar_templates, sourcepackage_changed,
                productseries_changed)
            self.validateDomain(
                data.get('translation_domain'), similar_templates,
                sourcepackage_changed, productseries_changed)

        priority = data.get('priority')
        if priority is None:
            return

        if (priority < self.PRIORITY_MIN_VALUE or
            priority > self.PRIORITY_MAX_VALUE):
            self.setFieldError(
                'priority',
                'The priority value must be between %s and %s.' % (
                self.PRIORITY_MIN_VALUE, self.PRIORITY_MAX_VALUE))
    def execute(self, bug, current_event):
        """See `IEmailCommand`."""
        # Tags are always lowercase.
        string_args = [arg.lower() for arg in self.string_args]
        if bug.tags is None:
            tags = []
        else:
            tags = list(bug.tags)
        for arg in string_args:
            # Are we adding or removing a tag?
            if arg.startswith('-'):
                remove = True
                tag = arg[1:]
            else:
                remove = False
                tag = arg
            # Tag must be a valid name.
            if not valid_name(tag):
                raise EmailProcessingError(
                    get_error_message(
                        'invalid-tag.txt',
                        error_templates=error_templates,
                        tag=tag))
            if remove:
                try:
                    tags.remove(tag)
                except ValueError:
                    raise EmailProcessingError(
                        get_error_message(
                            'unassigned-tag.txt',
                            error_templates=error_templates,
                            tag=tag))
            else:
                tags.append(arg)
        bug.tags = tags

        return bug, current_event
Exemple #18
0
    def traverse(self, owner, name, segments):
        """See `IGitTraversable`.

        :raises InvalidProductName: If 'name' is not a valid name.
        :raises NoSuchPerson: If 'name' begins with a '~', but the remainder
            doesn't match an existing person.
        :raises NoSuchProduct: If 'name' doesn't match an existing pillar.
        :return: A tuple of (`IPerson`, `IPillar`, None).
        """
        assert owner is None
        if name.startswith("~"):
            owner_name = name[1:]
            owner = getUtility(IPersonSet).getByName(owner_name)
            if owner is None:
                raise NoSuchPerson(owner_name)
            return owner, owner, None
        else:
            if not valid_name(name):
                raise InvalidProductName(name)
            pillar = getUtility(IPillarNameSet).getByName(name)
            if pillar is None:
                # Actually, the pillar is no such *anything*.
                raise NoSuchProduct(name)
            return owner, pillar, None
import os.path
import urllib

from lazr.uri import URI
from zope.interface import Interface

from lp.app.validators.name import valid_name
from lp.services.config import config
from lp.services.webapp.interfaces import ILaunchpadApplication

# When LAUNCHPAD_SERVICES is provided as a login ID to XML-RPC methods, they
# bypass the normal security checks and give read-only access to all branches.
# This allows Launchpad services like the puller and branch scanner to access
# private branches.
LAUNCHPAD_SERVICES = '+launchpad-services'
assert not valid_name(LAUNCHPAD_SERVICES), (
    "%r should *not* be a valid name." % (LAUNCHPAD_SERVICES,))

# When LAUNCHPAD_ANONYMOUS is passed, the XML-RPC methods behave as if no user
# was logged in.
LAUNCHPAD_ANONYMOUS = '+launchpad-anonymous'
assert not valid_name(LAUNCHPAD_ANONYMOUS), (
    "%r should *not* be a valid name." % (LAUNCHPAD_ANONYMOUS,))

# These are used as permissions for getBranchInformation.
READ_ONLY = 'r'
WRITABLE = 'w'

# Indicates that a path's real location is on a branch transport.
BRANCH_TRANSPORT = 'BRANCH_TRANSPORT'
# Indicates that a path points to a control directory.
    def _createBranch(self, registrant, branch_path):
        """The guts of the create branch method.

        Raises exceptions on error conditions.
        """
        to_link = None
        if branch_path.startswith(BRANCH_ALIAS_PREFIX + '/'):
            branch_path = branch_path[len(BRANCH_ALIAS_PREFIX) + 1:]
            if branch_path.startswith('~'):
                data, branch_name = self._parseUniqueName(branch_path)
            else:
                tokens = branch_path.split('/')
                data = {
                    'person': registrant.name,
                    'product': tokens[0],
                    }
                branch_name = 'trunk'
                # check the series
                product = self._product_set.getByName(data['product'])
                if product is not None:
                    if len(tokens) > 1:
                        series = product.getSeries(tokens[1])
                        if series is None:
                            raise faults.NotFound(
                                "No such product series: '%s'." % tokens[1])
                        else:
                            to_link = ICanHasLinkedBranch(series)
                    else:
                        to_link = ICanHasLinkedBranch(product)
                # don't forget the link.
        else:
            data, branch_name = self._parseUniqueName(branch_path)

        owner = self._person_set.getByName(data['person'])
        if owner is None:
            raise faults.NotFound(
                "User/team '%s' does not exist." % (data['person'],))
        # The real code consults the branch creation policy of the product. We
        # don't need to do so here, since the tests above this layer never
        # encounter that behaviour. If they *do* change to rely on the branch
        # creation policy, the observed behaviour will be failure to raise
        # exceptions.
        if not registrant.inTeam(owner):
            raise faults.PermissionDenied(
                ('%s cannot create branches owned by %s'
                 % (registrant.displayname, owner.displayname)))
        product = sourcepackage = None
        if data['product'] == '+junk':
            product = None
        elif data['product'] is not None:
            if not valid_name(data['product']):
                raise faults.InvalidProductName(escape(data['product']))
            product = self._product_set.getByName(data['product'])
            if product is None:
                raise faults.NotFound(
                    "Project '%s' does not exist." % (data['product'],))
        elif data['distribution'] is not None:
            distro = self._distribution_set.getByName(data['distribution'])
            if distro is None:
                raise faults.NotFound(
                    "No such distribution: '%s'." % (data['distribution'],))
            distroseries = self._distroseries_set.getByName(
                data['distroseries'])
            if distroseries is None:
                raise faults.NotFound(
                    "No such distribution series: '%s'."
                    % (data['distroseries'],))
            sourcepackagename = self._sourcepackagename_set.getByName(
                data['sourcepackagename'])
            if sourcepackagename is None:
                try:
                    sourcepackagename = self._sourcepackagename_set.new(
                        data['sourcepackagename'])
                except InvalidName:
                    raise faults.InvalidSourcePackageName(
                        data['sourcepackagename'])
            sourcepackage = self._factory.makeSourcePackage(
                distroseries, sourcepackagename)
        else:
            raise faults.PermissionDenied(
                "Cannot create branch at '%s'" % branch_path)
        branch = self._factory.makeBranch(
            owner=owner, name=branch_name, product=product,
            sourcepackage=sourcepackage, registrant=registrant,
            branch_type=BranchType.HOSTED)
        if to_link is not None:
            if registrant.inTeam(to_link.product.owner):
                to_link.branch = branch
            else:
                self._branch_set._delete(branch)
                raise faults.PermissionDenied(
                    "Cannot create linked branch at '%s'." % branch_path)
        return branch.id
 def new(self, name_string):
     if not valid_name(name_string):
         raise InvalidName(name_string)
     return self._add(FakeSourcePackageName(name_string))
Exemple #22
0
def validate_tags(tags):
    """Check that `separator` separated `tags` are valid tag names."""
    return (all(valid_name(tag) for tag in tags)
            and len(set(tags)) == len(tags))
 def new(self, name):
     if not valid_name(name):
         raise InvalidName("%s is not a valid name for a source package." %
                           name)
     return SourcePackageName(name=name)
Exemple #24
0
 def constraint(self, value):
     """Make sure that the value is a valid name."""
     super_constraint = TextLine.constraint(self, value)
     return super_constraint and valid_name(value)
 def new(self, name):
     if not valid_name(name):
         raise InvalidName(
             "%s is not a valid name for a source package." % name)
     return SourcePackageName(name=name)
 def constraint(self, value):
     """Make sure that the value is a valid name."""
     super_constraint = TextLine.constraint(self, value)
     return super_constraint and valid_name(value)
import urllib

from lazr.uri import URI
import six
from zope.interface import Interface

from lp.app.validators.name import valid_name
from lp.services.config import config
from lp.services.webapp.interfaces import ILaunchpadApplication

# When LAUNCHPAD_SERVICES is provided as a login ID to XML-RPC methods, they
# bypass the normal security checks and give read-only access to all branches.
# This allows Launchpad services like the puller and branch scanner to access
# private branches.
LAUNCHPAD_SERVICES = '+launchpad-services'
assert not valid_name(LAUNCHPAD_SERVICES), (
    "%r should *not* be a valid name." % (LAUNCHPAD_SERVICES, ))

# When LAUNCHPAD_ANONYMOUS is passed, the XML-RPC methods behave as if no user
# was logged in.
LAUNCHPAD_ANONYMOUS = '+launchpad-anonymous'
assert not valid_name(LAUNCHPAD_ANONYMOUS), (
    "%r should *not* be a valid name." % (LAUNCHPAD_ANONYMOUS, ))

# These are used as permissions for getBranchInformation.
READ_ONLY = 'r'
WRITABLE = 'w'

# Indicates that a path's real location is on a branch transport.
BRANCH_TRANSPORT = 'BRANCH_TRANSPORT'
# Indicates that a path points to a control directory.
def validate_tags(tags):
    """Check that `separator` separated `tags` are valid tag names."""
    return (
        all(valid_name(tag) for tag in tags) and
        len(set(tags)) == len(tags))