Exemple #1
0
    def contains(self, query):
        """Determine if C{query} is contained within this query.

        Note that this method only matches simple queries that represent a
        single expression.

        @param query: The L{Query} to match against this one.
        @raises FeatureError: Raised if C{query} is too complex to match.
        @return: C{True} if C{query} is present, otherwise C{False}.
        """
        if query.rootNode.left is not None:
            if (query.rootNode.left.left is not None
                    or query.rootNode.left.right is not None):
                raise FeatureError("Query is too complex to match.")
        if query.rootNode.right is not None:
            if (query.rootNode.right.left is not None
                    or query.rootNode.right.right is not None):
                raise FeatureError("Query is too complex to match.")

        def traverse(node):
            if node is None:
                return False
            elif (node == query.rootNode and node.left == query.rootNode.left
                  and node.right == query.rootNode.right):
                return True
            else:
                return traverse(node.left) or traverse(node.right)

        return traverse(self.rootNode)
Exemple #2
0
    def search(self, queries, implicitCreate=True):
        """Find object IDs matching specified L{Query}s.

        @param queries: The sequence of L{Query}s to resolve.
        @param implicitCreate: Optionally a flag indicating if nonexistent
            objects should be created for special tags like C{fluiddb/about}.
            Default is L{True}.
        @return: A L{SearchResult} configured to resolve the specified
            L{Query}s.
        """
        if not queries:
            raise FeatureError('Queries must be provided.')

        idQueries = []
        aboutQueries = []
        hasQueries = []
        solrQueries = []
        for query in queries:
            if isEqualsQuery(query, u'fluiddb/id'):
                idQueries.append(query)
            elif isEqualsQuery(query, u'fluiddb/about'):
                aboutQueries.append(query)
            elif isHasQuery(query):
                hasQueries.append(query)
            else:
                solrQueries.append(query)

        index = getObjectIndex()
        specialResults = self._resolveAboutQueries(aboutQueries,
                                                   implicitCreate)
        specialResults.update(self._resolveFluiddbIDQueries(idQueries))
        specialResults.update(self._resolveHasQueries(hasQueries))
        return SearchResult(index, solrQueries, specialResults)
Exemple #3
0
def getQueryParser():
    """Get a L{QueryParser} to parse Fluidinfo queries.

    The process of building a L{QueryParser} is quite expensive.  PLY
    generates parse tables and writes them to a file on disk.  As a result, a
    single parser instance is generated and cached in memory.  The same
    L{QueryParser} instance is returned each time this function is called.

    As a result, you must be especially careful about thread-safety.  The
    L{QueryParser} must only be used to parse one input at a time and never
    shared among threads.

    @raise FeatureError: Raised if this function is invoked outside the main
        thread.
    @return: The global L{QueryParser} instance.
    """
    if currentThread().getName() != 'MainThread':
        raise FeatureError(
            'A query parser may only be used in the main thread.')

    global _parser
    if _parser is None:
        lexer = getQueryLexer()
        parser = QueryParser(lexer.tokens)
        parser.build(module=parser,
                     debug=False,
                     outputdir=getConfig().get('service', 'temp-path'))
        _parser = parser
        # Setup illegal queries to ensure we do it in a safe way and avoid
        # races.
        getIllegalQueries()
    return _parser
Exemple #4
0
    def get(self, paths, withDescriptions=None):
        """Get information about L{Tag}s matching C{paths}.

        @param paths: A sequence of L{Tag.path}s.
        @param withDescriptions: Optionally, a C{bool} indicating whether or
            not to include L{Tag} descriptions in the result.  Default is
            C{False}.
        @return: A C{dict} that maps L{Tag.path}s to C{dict}s with information
            about matching L{Tag}s, matching the following format::

              {<path>: {'id': <object-id>,
                        'description': <description>}}
        """
        if not paths:
            raise FeatureError("Can't retrieve an empty list of tags.")

        result = getTags(paths=paths)
        values = list(result.values(Tag.path, Tag.objectID))
        descriptions = (
            self._getDescriptions(objectID for path, objectID in values)
            if withDescriptions else None)

        tags = {}
        for path, objectID in values:
            value = {'id': objectID}
            if withDescriptions:
                value['description'] = descriptions.get(objectID, u'')
            tags[path] = value
        return tags
Exemple #5
0
    def delete(self, usernames):
        """Delete L{User}s matching C{username}s.

        @param usernames: A sequence of L{User.username}s.
        @raise FeatureError: Raised if no L{User.username}s are provided.
        @raise UnknownUserError: Raised if one or more usernames don't match
            existing L{User}s.
        @return: A  C{list} of C{(objectID, User.username)} 2-tuples
            representing the L{User}s that that were removed.
        """
        if isgenerator(usernames):
            usernames = list(usernames)
        if not usernames:
            raise FeatureError('At least one username must be provided.')

        usernames = set(usernames)
        result = getUsers(usernames=usernames)
        existingUsernames = set(result.values(User.username))
        unknownUsernames = usernames - existingUsernames
        if unknownUsernames:
            raise UnknownUserError(list(unknownUsernames))

        admin = getUser(u'fluiddb')
        deletedUsers = list(result.values(User.objectID, User.username))
        # FIXME: Deleting a user will leave the permission exception lists
        # containing the user in a corrupt state.
        result.remove()
        self._factory.tagValues(admin).delete([
            (objectID, systemTag) for objectID, _ in deletedUsers
            for systemTag in [
                u'fluiddb/users/username', u'fluiddb/users/name',
                u'fluiddb/users/email', u'fluiddb/users/role'
            ]
        ])
        return deletedUsers
Exemple #6
0
    def get(self, objectIDs, paths=None):
        """Get L{TagValue}s matching filtering criteria.

        @param objectIDs: A sequence of object IDs to retrieve values for.
        @param paths: Optionally, a sequence of L{Tag.path}s to return.  The
            default is to return values for all available L{Tag.path}s.
        @raise FeatureError: Raised if any of the arguments is empty or
            C{None}.
        @return: A C{dict} mapping object IDs to tags and values, matching the
            following format::

              {<object-id>: {<path>: <L{TagValue}>}}
        """
        if not objectIDs:
            raise FeatureError("Can't get tag values for an empty list of "
                               'object IDs.')
        if not paths:
            objects = self._factory.objects(self._user)
            paths = objects.getTagsForObjects(objectIDs)

        result = {}

        if u'fluiddb/id' in paths:
            for objectID in objectIDs:
                tagValue = FluidinfoTagValue.fromObjectID(objectID)
                result[objectID] = {u'fluiddb/id': tagValue}

        # Avoid querying the database if only the 'fluiddb/id' tag has
        # been requested, since we already have the results.
        if [u'fluiddb/id'] == paths:
            return result

        collection = TagValueCollection(objectIDs=objectIDs, paths=paths)
        for tag, tagValue in collection.values():
            if tagValue.objectID not in result:
                result[tagValue.objectID] = {}
            tagValue = FluidinfoTagValue.fromTagValue(tagValue)
            if isinstance(tagValue.value, dict):
                # We have to make a copy of the value because we don't want
                # storm to try to add the 'contents' binary value to the
                # database.
                tagValue.value = dict(tagValue.value)
                opaque = getOpaqueValues([tagValue.id]).one()
                if opaque is None:
                    raise RuntimeError('Opaque value not found.')
                tagValue.value['contents'] = opaque.content
            result[tagValue.objectID][tag.path] = tagValue

        return result
Exemple #7
0
    def getUnknownPaths(self, values):
        """Check if the paths in a sequence of path-operation exist.

        @param values: A sequence of C{(path, Operation)} 2-tuples.
        @raise FeatureError: Raised if an invalid path or L{Operation} is
            given.
        @return: A C{set} with the unknown paths.
        """
        tagPaths = set()
        namespacePaths = set()
        for path, operation in values:
            if path is None:
                raise FeatureError('A path must be provided.')
            elif operation in Operation.TAG_OPERATIONS:
                tagPaths.add(path)
            elif operation in Operation.NAMESPACE_OPERATIONS:
                namespacePaths.add(path)
            else:
                raise FeatureError('Invalid operation %s for the path %r' %
                                   (operation, path))

        if tagPaths:
            existingTags = set(getTags(paths=tagPaths).values(Tag.path))
            unknownTags = tagPaths - existingTags
            unknownTags.discard(u'fluiddb/id')
        else:
            unknownTags = set()

        if namespacePaths:
            result = getNamespaces(paths=namespacePaths).values(Namespace.path)
            existingNamespaces = set(result)
            unknownNamespaces = namespacePaths - existingNamespaces
        else:
            unknownNamespaces = set()

        return unknownTags.union(unknownNamespaces)
Exemple #8
0
    def set(self, values):
        """Update information about L{User}s.

        If an incoming field is C{None} the appropriate instance field will not
        be modified.

        @param values: A sequence of C{(username, password, fullname, email,
            role)} 5-tuples.
        @raise FeatureError: Raised if C{values} is empty.
        @raise UnknownUserError: Raised if a specified L{User} does not exist.
        @return: A 2-tuples representing the L{User}s that were updated.
        """
        if not values:
            raise FeatureError('Information about at least one user must be '
                               'provided.')

        usernames = set(username for username, _, _, _, _ in values)
        users = dict(
            (user.username, user) for user in getUsers(usernames=usernames))
        existingUsernames = set(users.iterkeys())
        unknownUsernames = usernames - existingUsernames
        if unknownUsernames:
            raise UnknownUserError(list(unknownUsernames))

        result = []
        systemValues = {}
        for username, password, fullname, email, role in values:
            user = users[username]
            valuesToUpdate = {}
            if password is not None:
                user.passwordHash = hashPassword(password)
            if fullname is not None:
                user.fullname = fullname
                valuesToUpdate[u'fluiddb/users/name'] = user.fullname
            if email is not None:
                user.email = email
                valuesToUpdate[u'fluiddb/users/email'] = user.email
            if role is not None:
                user.role = role
                valuesToUpdate[u'fluiddb/users/role'] = unicode(user.role)
            if valuesToUpdate:
                systemValues[user.objectID] = valuesToUpdate
            result.append((user.objectID, user.username))
        if systemValues:
            admin = getUser(u'fluiddb')
            self._factory.tagValues(admin).set(systemValues)
        return result
Exemple #9
0
    def create(self, values):
        """Create new L{Tag}s.

        L{Namespace}s that don't exist are created automatically before
        L{Tag}s are created.  Associated L{NamespacePermission} and
        L{TagPermission}s are created automatically with the system-wide
        default permissions.

        @param values: A sequence of C{(Tag.path, description)} 2-tuples.
        @raise DuplicatePathError: Raised if the path for a new L{Tag}
            collides with an existing one.
        @raise FeatureError: Raised if the given list of values is empty.
        @raise UnknownParentPathError: Raised if the parent for a new L{Tag}
            can't be found.
        @raise MalformedPathError: Raised if one of the given paths is empty
            or has unacceptable characters.
        @return: A C{list} of C{(objectID, path)} 2-tuples for the new L{Tag}s.
        """
        if not values:
            raise FeatureError("Can't create an empty list of tags.")

        # Make sure tag paths don't exist before trying to create new tags.
        paths = [path for path, _ in values]
        existingPaths = list(getTags(paths=paths).values(Tag.path))
        if existingPaths:
            raise DuplicatePathError(
                'Paths already exist: %s' % ', '.join(existingPaths))

        # Get intermediate namespaces.  If they don't exist, create them
        # automatically.
        paths = [path for (path, _) in values]
        parentPaths = getParentPaths(paths)
        missingParentPaths = self._getMissingNamespaces(parentPaths)
        self._factory.namespaces(self._user).create(
            [(path, u'Object for the namespace %s' % path)
             for path in missingParentPaths])

        # Create the new tags.
        result = getNamespaces(paths=parentPaths)
        parentNamespaces = dict((namespace.path, namespace)
                                for namespace in result)
        return self._createTags(values, parentNamespaces)
Exemple #10
0
    def delete(self, values):
        """Delete L{TagValue}s.

        @param values: A sequence of C{(objectID, Tag.path)} 2-tuples to
            delete values for.
        @raise FeatureError: Raised if the given list of values is empty.
        @return: The number of values deleted.
        """
        if isgenerator(values):
            values = list(values)
        if not values:
            raise FeatureError("Can't delete an empty list of tag values.")

        paths = set([path for objectID, path in values])
        objectIDs = set([objectID for objectID, path in values])
        tagIDs = dict(getTags(paths).values(Tag.path, Tag.id))
        values = [(objectID, tagIDs[path]) for objectID, path in values]
        result = getTagValues(values).remove()
        if result:
            touchObjects(objectIDs)
        return result
Exemple #11
0
    def get(self, usernames):
        """Get information about L{User}s matching C{usernames}.

        @param usernames: A sequence of L{User.username}s.
        @raise FeatureError: Raised if no L{User.username}s are provided.
        @return: A C{dict} that maps L{User.username}s to C{dict}s with
            information about matching L{User}s, matching the following
            format::

              {<username>: {'id': <object-id>,
                            'name': <full-name>,
                            'role': <role>}}
        """
        if not usernames:
            raise FeatureError('At least one username must be provided.')

        result = getUsers(usernames=usernames)
        result = result.values(User.objectID, User.username, User.fullname,
                               User.role)
        users = {}
        for objectID, username, name, role in result:
            users[username] = {'id': objectID, 'name': name, 'role': role}
        return users
Exemple #12
0
    def get(self, values):
        """Get permissions matching pairs of paths and L{Operation}s.

        @param values: A sequence of C{(path, Operation)} tuples.
        @raise FeatureError: Raised if the given list of values is empty.
        @return: A C{dict} that maps C{(path, Operation)} tuples to C{(Policy,
            exceptions)} tuples.  Example::

              {(<path>, <operation>): (<policy>, ['user1', 'user2', ...]), ...}
        """
        if not values:
            raise FeatureError("Can't get an empty list of permissions.")

        # Get the requested permission data.
        permissions = {}
        namespaceValues = []
        tagValues = []
        for path, operation in values:
            if operation in Operation.NAMESPACE_OPERATIONS:
                namespaceValues.append((path, operation))
            else:
                tagValues.append((path, operation))
        if namespaceValues:
            permissions.update(self._getNamespacePermissions(namespaceValues))
        if tagValues:
            permissions.update(self._getTagPermissions(tagValues))

        # Translate User.id's in the exception lists to User.username's.
        userIDs = set()
        for key, (policy, exceptions) in permissions.iteritems():
            userIDs.update(exceptions)
        usernames = dict(getUsers(ids=userIDs).values(User.id, User.username))
        for key, (policy, exceptions) in permissions.items():
            permissions[key] = (policy,
                                [usernames[userID] for userID in exceptions])

        return permissions
Exemple #13
0
    def set(self, values):
        """Set or update L{TagValue}s.

        L{Tag}s that don't exist are created automatically before L{TagValue}s
        are stored.  Associated L{TagPermission}s are created automatically
        with the system-wide default permissions.

        @param values: A C{dict} mapping object IDs to tags and values,
            matching the following format::

              {<object-id>: {<path>: <value>,
                             <path>: {'mime-type': <mime-type>,
                                      'contents': <contents>}}}

            A binary L{TagValue} is represented using a different layout than
            other values types, as shown for the second value.
        @raise FeatureError: Raised if the given list of values is empty.
        @raise MalformedPathError: Raised if one of the given paths for a
            nonexistent tag is empty or has unacceptable characters.
        """
        if not values:
            raise FeatureError("Can't set an empty list of tag values.")

        objectIDs = set(values.keys())

        # Implicitly create missing tags, if there are any.
        paths = set()
        for tagValues in values.itervalues():
            paths.update(tagValues.iterkeys())
        tagIDs = dict(getTags(paths=paths).values(Tag.path, Tag.id))
        existingPaths = set(tagIDs.iterkeys())
        unknownPaths = paths - existingPaths
        if unknownPaths:
            tags = [(path, u'Object for the attribute %s' % path)
                    for path in unknownPaths]
            self._factory.tags(self._user).create(tags)
            tagIDs = dict(getTags(paths=paths).values(Tag.path, Tag.id))

        # Delete all existing tag values for the specified object IDs and
        # paths.
        deleteValues = []
        for objectID in values:
            for path in values[objectID].iterkeys():
                deleteValues.append((objectID, tagIDs[path]))
        getTagValues(deleteValues).remove()

        # Set new tag values for the specified object IDs and paths.
        for objectID in values:
            tagValues = values[objectID]
            for path, value in tagValues.iteritems():
                tagID = tagIDs[path]

                if isinstance(value, dict):
                    content = value['contents']
                    value = createTagValue(self._user.id, tagID, objectID, {
                        'mime-type': value['mime-type'],
                        'size': len(content)
                    })

                    # This is necessary to tell PostgreSQL that generates a
                    # `value.id` immediately.
                    value.id = AutoReload
                    createOpaqueValue(value.id, content)
                else:
                    createTagValue(self._user.id, tagID, objectID, value)
        touchObjects(objectIDs)
Exemple #14
0
    def getFollowedObjects(self,
                           username,
                           limit=20,
                           olderThan=None,
                           objectType=None):
        """Get the objects followed by the specified user.

        @param username: The user to get the followed objects for.
        @param limit: Optionally, The maximum number of objects to return.
        @param olderThan: Optionally a C{datetime} indicating to return only
            objects followed before the specified time.
        @param objectType: Optionally, a C{str} representing the object type
            to filter from the objects. The allowed values are C{url},
            C{user} and C{hashtag}.
        @return: A C{list} of objects followed by C{username} represented by
            a C{dict} with the following format::

              [
                {
                  'about': '<about>',
                  'creationTime': <float_timestamp>,
                  'following': True
                },
              ...
              ]
        """
        store = getMainStore()
        where = [
            TagValue.tagID == Tag.id,
            TagValue.objectID == AboutTagValue.objectID,
            Tag.path == username + u'/follows'
        ]
        if olderThan is not None:
            where.append(TagValue.creationTime < olderThan)
        if objectType is not None:
            if objectType == 'user':
                where.append(Like(AboutTagValue.value, u'@%'))
            elif objectType == 'url':
                where.append(Like(AboutTagValue.value, u'http%'))
            elif objectType == 'hashtag':
                where.append(Like(AboutTagValue.value, u'#%'))
            else:
                raise FeatureError('Unknown object type.')

        result = store.find(
            (TagValue.objectID, AboutTagValue.value, TagValue.creationTime),
            where)
        result = result.order_by(Desc(TagValue.creationTime))
        result = list(result.config(limit=limit))

        objectIDs = [objectID for objectID, _, _ in result]

        if self._user.username != username:
            callerObjectIDs = set(
                store.find(TagValue.objectID, Tag.id == TagValue.tagID,
                           Tag.path == self._user.username + u'/follows',
                           TagValue.objectID.is_in(objectIDs)))
        else:
            callerObjectIDs = None

        return [{
            u'about':
            about,
            u'following': (objectID in callerObjectIDs
                           if callerObjectIDs is not None else True),
            u'creationTime': (timegm(creationTime.utctimetuple()) +
                              float(creationTime.strftime('0.%f')))
        } for objectID, about, creationTime in result]
Exemple #15
0
    def create(self,
               text,
               username,
               about=None,
               importer=None,
               when=None,
               url=None):
        """Create a new comment.

        @param text: The C{unicode} comment text.
        @param username: the C{unicode} username of the commenter.
        @param about: Optionally, a C{list} of C{unicode} values the comment is
            about.
        @param importer: A C{unicode} string giving the name of the importer.
        @param when: A C{datetime.datetime} instance or C{None} if the
            current time should be used.
        @param url: A C{str} URL or C{None} if there is no URL associated with
            this comment.
        @raise L{FeatureError}: if (1) the comment text is C{None} or is all
            whitespace, or (2) if the importer name contains the separator
            (space) that we use in the about value for comment objects.
        @return: A C{dict} as follows:
            {
                fluidinfo.com/info/about: A C{list} of all the about values
                    (i.e., URLs and hashtags) in the comment text, including
                    the thing the comment was about (if anything). The hashtags
                    are in lowercase.
                fluidinfo.com/info/timestamp: The C{int} UTC timestamp (seconds
                    since the epoch) the comment was created at.
                fluidinfo.com/info/url: The C{url}, as received.
                fluidinfo.com/info/username: The C{username}, as received.
            }
        """
        if not text or text.strip() == '':
            raise FeatureError('Comment text non-existent or just whitespace.')

        if importer:
            if ' ' in importer:
                raise FeatureError('Comment importer name contains a space.')
        else:
            importer = u'fluidinfo.com'

        when = when or datetime.utcnow()
        floatTime = timegm(when.utctimetuple()) + float(when.strftime('0.%f'))
        isoTime = when.isoformat()

        if not url:
            url = 'https://fluidinfo.com/comment/%s/%s/%s' % (
                importer, username, isoTime)

        # Put all the explicit about values into a list called abouts. Items
        # are stripped and those that are not URLs are lowercased.
        abouts = []
        if about:
            for item in map(unicode.strip, about):
                abouts.append(item if URL_REGEX.match(item) else item.lower())
        abouts.extend(self._extractAbouts(text))
        abouts = uniqueList(abouts)

        commentObjectAbout = u'%s %s %s' % (importer, username, isoTime)
        commentID = self._objects.create(commentObjectAbout)

        values = {
            u'fluidinfo.com/info/about': abouts,
            u'fluidinfo.com/info/username': username,
            u'fluidinfo.com/info/text': text,
            u'fluidinfo.com/info/url': url,
            u'fluidinfo.com/info/timestamp': floatTime
        }
        self._tagValues.set({commentID: values})

        if abouts:
            # Get all the object IDs of the target objects. If an object does
            # not exist, create it.
            result = getAboutTagValues(values=abouts)
            existingObjects = dict(
                result.values(AboutTagValue.value, AboutTagValue.objectID))
            missingAbouts = set(abouts) - set(existingObjects.iterkeys())
            for aboutValue in missingAbouts:
                existingObjects[aboutValue] = self._objects.create(aboutValue)
            createComment(commentID, existingObjects.values(), username, when)
        return values
Exemple #16
0
    def create(self, values, createPrivateNamespace=None):
        """Create new L{User}s.

        @param values: A sequence of C{(username, password, fullname, email)}
            4-tuples.
        @param createPrivateNamespace: Optionally, a flag to specify whether
            or not the C{<username>/private} L{Namespace} should be created.
            Default is C{True}.
        @raise DuplicateUserError: Raised if the username for a new L{User}
            collides with an existing one.
        @raise FeatureError: Raised if C{values} is empty.
        @return: A C{list} of C{(objectID, username)} 2-tuples for the new
            L{User}s.
        """
        if not values:
            raise FeatureError('Information about at least one user must be '
                               'provided.')

        # Make sure usernames don't exist before trying to create
        # new users.
        usernames = [username for username, _, _, _ in values]
        result = getUsers(usernames=usernames)
        existingUsernames = set(result.values(User.username))
        if existingUsernames:
            raise DuplicateUserError(existingUsernames)

        # Create the users.
        systemValues = {}
        result = []
        privateUpdateResults = []
        admin = getUser(u'fluiddb')
        objects = self._factory.objects(admin)
        for username, password, fullname, email in values:
            user = createUser(username, password, fullname, email)
            about = u'@%s' % username
            user.objectID = objects.create(about)
            namespaces = self._factory.namespaces(user)

            # Create the user's root namespace.
            namespaces.create([(username, u'Namespace for user %s' % username)
                               ])
            namespace = getNamespaces(paths=[username]).one()
            user.namespaceID = namespace.id

            # Create the user's private namespace.
            if createPrivateNamespace is None or createPrivateNamespace:
                privateNamespaceName = '%s/private' % username
                privateUpdateResults.append(
                    namespaces.create([
                        (privateNamespaceName,
                         u'Private namespace for user %s' % username)
                    ]))
                namespace = getNamespaces(paths=[privateNamespaceName]).one()
                permission = namespace.permission
                permission.set(Operation.LIST_NAMESPACE, Policy.CLOSED,
                               [user.id])

            # Create system tags
            systemValues[user.objectID] = {
                u'fluiddb/users/username': username,
                u'fluiddb/users/name': fullname,
                u'fluiddb/users/email': email,
                u'fluiddb/users/role': unicode(user.role)
            }
            result.append((user.objectID, user.username))
        self._factory.tagValues(admin).set(systemValues)
        return result