diff --git a/djblets/db/query.py b/djblets/db/query.py
index acc34c0fe780b49e92faa5a3816afac603e97e74..b089b09c3d2129a4caaf3078bf7b1b1846b632df 100644
--- a/djblets/db/query.py
+++ b/djblets/db/query.py
@@ -1,6 +1,134 @@
 from __future__ import unicode_literals
 
+from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
 from django.db.models.manager import Manager
+from django.utils import six
+
+
+class LocalDataQuerySet(object):
+    """A QuerySet that operates on generic data provided by the caller.
+
+    This can be used in some circumstances when code requires a QuerySet,
+    but where the data doesn't come from the database. The caller can
+    instantiate one of these and provide it.
+
+    This doesn't perform full support for all of QuerySet's abilities. It
+    does, however, support the following basic functions:
+
+    * all
+    * clone
+    * count
+    * exclude
+    * filter
+    * get
+    * prefetch_related
+    * select_related
+
+    As well as the operators expected by consumers of QuerySet, such as
+    __len__ and __iter__.
+
+    This is particularly handy with WebAPIResource.
+    """
+    def __init__(self, data):
+        self._data = data
+
+    def all(self):
+        """Returns a cloned copy of this queryset."""
+        return self.clone()
+
+    def clone(self):
+        """Returns a cloned copy of this queryset."""
+        return LocalDataQuerySet(list(self._data))
+
+    def count(self):
+        """Returns the number of items in this queryset."""
+        return len(self._data)
+
+    def exclude(self, **kwargs):
+        """Returns a queryset excluding items from this queryset.
+
+        The result will be a LocalDataQuerySet that contains all items from
+        this queryset that do not contain attributes with values matching
+        those that were passed to this function as keyword arguments.
+        """
+        return LocalDataQuerySet(
+            list(self._filter_or_exclude(return_matches=False, **kwargs)))
+
+    def filter(self, **kwargs):
+        """Returns a queryset filtering items from this queryset.
+
+        The result will be a LocalDataQuerySet that contains all items from
+        this queryset that contain attributes with values matching those that
+        were passed to this function as keyword arguments.
+        """
+        return LocalDataQuerySet(
+            list(self._filter_or_exclude(return_matches=True, **kwargs)))
+
+    def get(self, **kwargs):
+        """Returns a single result from this queryset.
+
+        This will return a single result from the list of items in this
+        queryset. If keyword arguments are provided, they will be used
+        to filter the queryset down.
+
+        There must be only one item in the queryset matching the given
+        criteria, or a MultipleObjectsReturned will be raised. If there are
+        no items, then an ObjectDoesNotExist will be raised.
+        """
+        clone = self.filter(**kwargs)
+        count = len(clone)
+
+        if count == 1:
+            return clone[0]
+        elif count == 0:
+            raise ObjectDoesNotExist('%s matching query does not exist.'
+                                     % self._data.__class__.__name__)
+        else:
+            raise MultipleObjectsReturned(
+                'get() returned more than one %s -- it returned %s!'
+                % (self._data.__class__.__name__, count))
+
+    def prefetch_related(self, *args, **kwargs):
+        """Stub for compatibility with QuerySet.prefetch_related.
+
+        This will simply return a clone of this queryset.
+        """
+        return self.clone()
+
+    def select_related(self, *args, **kwargs):
+        """Stub for compatibility with QuerySet.select_related.
+
+        This will simply return a clone of this queryset.
+        """
+        return self.clone()
+
+    def __contains__(self, i):
+        return i in self._data
+
+    def __getitem__(self, i):
+        return self._data[i]
+
+    def __getslice__(self, i, j):
+        return self._data[i:j]
+
+    def __iter__(self):
+        for i in self._data:
+            yield i
+
+    def __len__(self):
+        return len(self._data)
+
+    def _filter_or_exclude(self, return_matches=True, **kwargs):
+        for item in self:
+            match = True
+
+            for key, value in six.iteritems(kwargs):
+                if getattr(item, key) != value:
+                    match = False
+                    break
+
+            if match == return_matches:
+                yield item
 
 
 def get_object_or_none(klass, *args, **kwargs):
diff --git a/djblets/db/tests.py b/djblets/db/tests.py
new file mode 100644
index 0000000000000000000000000000000000000000..e96f4822c324dd49b7c96fe8fee4540e597b94e7
--- /dev/null
+++ b/djblets/db/tests.py
@@ -0,0 +1,180 @@
+from __future__ import unicode_literals
+
+from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
+from mock import Mock
+
+from djblets.db.query import LocalDataQuerySet
+from djblets.testing.testcases import TestCase
+
+
+class LocalDataQuerySetTests(TestCase):
+    """Tests for djblets.db.query.LocalDataQuerySet."""
+    def test_clone(self):
+        """Testing LocalDataQuerySet.clone"""
+        values = [1, 2, 3]
+        queryset = LocalDataQuerySet(values)
+        clone = queryset.clone()
+
+        self.assertEqual(list(clone), values)
+        values.append(4)
+        self.assertNotEqual(list(clone), values)
+
+    def test_count(self):
+        """Testing LocalDataQuerySet.count"""
+        values = [1, 2, 3]
+        queryset = LocalDataQuerySet(values)
+
+        self.assertEqual(queryset.count(), 3)
+
+    def test_exclude(self):
+        """Testing LocalDataQuerySet.exclude"""
+        obj1 = Mock()
+        obj1.a = 1
+        obj1.b = 2
+
+        obj2 = Mock()
+        obj2.a = 10
+        obj2.b = 20
+
+        queryset = LocalDataQuerySet([obj1, obj2])
+        queryset = queryset.exclude(a=1)
+
+        self.assertEqual(len(queryset), 1)
+        self.assertEqual(queryset[0], obj2)
+
+    def test_exclude_with_multiple_args(self):
+        """Testing LocalDataQuerySet.exclude with multiple arguments"""
+        obj1 = Mock()
+        obj1.a = 1
+        obj1.b = 2
+
+        obj2 = Mock()
+        obj2.a = 1
+        obj2.b = 20
+
+        obj3 = Mock()
+        obj3.a = 1
+        obj3.b = 40
+
+        queryset = LocalDataQuerySet([obj1, obj2, obj3])
+        queryset = queryset.exclude(a=1, b=20)
+
+        self.assertEqual(len(queryset), 2)
+        self.assertEqual(list(queryset), [obj1, obj3])
+
+    def test_filter(self):
+        """Testing LocalDataQuerySet.filter"""
+        obj1 = Mock()
+        obj1.a = 1
+        obj1.b = 2
+
+        obj2 = Mock()
+        obj2.a = 10
+        obj2.b = 20
+
+        obj3 = Mock()
+        obj3.a = 1
+        obj3.b = 40
+
+        queryset = LocalDataQuerySet([obj1, obj2, obj3])
+        queryset = queryset.filter(a=1)
+
+        self.assertEqual(len(queryset), 2)
+        self.assertEqual(list(queryset), [obj1, obj3])
+
+    def test_filter_with_multiple_args(self):
+        """Testing LocalDataQuerySet.filter with multiple arguments"""
+        obj1 = Mock()
+        obj1.a = 1
+        obj1.b = 2
+
+        obj2 = Mock()
+        obj2.a = 1
+        obj2.b = 20
+
+        obj3 = Mock()
+        obj3.a = 2
+        obj3.b = 20
+
+        queryset = LocalDataQuerySet([obj1, obj2, obj3])
+        queryset = queryset.filter(a=1, b=20)
+
+        self.assertEqual(len(queryset), 1)
+        self.assertEqual(queryset[0], obj2)
+
+    def test_get(self):
+        """Testing LocalDataQuerySet.get"""
+        obj1 = Mock()
+        queryset = LocalDataQuerySet([obj1])
+
+        self.assertEqual(queryset.get(), obj1)
+
+    def test_get_with_filters(self):
+        """Testing LocalDataQuerySet.get with filters"""
+        obj1 = Mock()
+        obj1.a = 1
+
+        obj2 = Mock()
+        obj2.a = 2
+
+        queryset = LocalDataQuerySet([obj1, obj2])
+
+        self.assertEqual(queryset.get(a=1), obj1)
+
+    def test_get_with_no_results(self):
+        """Testing LocalDataQuerySet.get with no results"""
+        obj1 = Mock()
+        obj1.a = 1
+
+        obj2 = Mock()
+        obj2.a = 1
+
+        queryset = LocalDataQuerySet([obj1, obj2])
+
+        self.assertRaises(ObjectDoesNotExist, queryset.get, a=2)
+
+    def test_get_with_multiple_results(self):
+        """Testing LocalDataQuerySet.get with multiple results"""
+        obj1 = Mock()
+        obj2 = Mock()
+
+        queryset = LocalDataQuerySet([obj1, obj2])
+
+        self.assertRaises(MultipleObjectsReturned, queryset.get)
+
+    def test_contains(self):
+        """Testing LocalDataQuerySet.__contains__"""
+        values = [1, 2, 3]
+        queryset = LocalDataQuerySet(values)
+
+        self.assertIn(2, queryset)
+
+    def test_getitem(self):
+        """Testing LocalDataQuerySet.__getitem__"""
+        values = [1, 2, 3]
+        queryset = LocalDataQuerySet(values)
+
+        self.assertEqual(queryset[1], 2)
+
+    def test_getslice(self):
+        """Testing LocalDataQuerySet.__getitem__"""
+        values = [1, 2, 3]
+        queryset = LocalDataQuerySet(values)
+
+        self.assertEqual(queryset[1:3], [2, 3])
+
+    def test_iter(self):
+        """Testing LocalDataQuerySet.__iter__"""
+        values = [1, 2]
+        queryset = LocalDataQuerySet(values)
+        gen = iter(queryset)
+
+        self.assertEqual(gen.next(), 1)
+        self.assertEqual(gen.next(), 2)
+
+    def test_len(self):
+        """Testing LocalDataQuerySet.__len__"""
+        values = [1, 2, 3]
+        queryset = LocalDataQuerySet(values)
+
+        self.assertEqual(len(queryset), 3)
diff --git a/djblets/webapi/resources.py b/djblets/webapi/resources.py
index 1e834674a4466af1d725b9ef0dc6187d45f6096f..23b337a652de18b371d28730da1c0c574289e933 100644
--- a/djblets/webapi/resources.py
+++ b/djblets/webapi/resources.py
@@ -58,7 +58,8 @@ class WebAPIResource(object):
     -------------------
 
     Most resources will have ``model`` set to a Model subclass, and
-    ``fields`` set to list the fields that would be shown when
+    ``fields`` set to a dictionary defining the fields to return in the
+    resource payloads.
 
     Each resource will also include a ``link`` dictionary that maps
     a key (resource name or action) to a dictionary containing the URL
@@ -88,6 +89,23 @@ class WebAPIResource(object):
     can be overridden by setting ``name`` and ``name_plural``.
 
 
+    Non-Database Models
+    -------------------
+
+    Resources are not always backed by a database model. It's often useful to
+    work with lists of objects or data computed within the request.
+
+    In these cases, most resources will still want to set ``model`` to some
+    sort of class and provide a ``fields`` dictionary. It's expected that
+    the fields will all exist as attributes on an instance of the model, or
+    that a serializer function will exist for the field.
+
+    These resources will then to define a ``get_queryset`` that returns a
+    :py:class:`djblets.db.query.LocalDataQuerySet` containing the list of
+    items to return in the resource. This will allow standard resource
+    functionality like pagination to work.
+
+
     Matching Objects
     ----------------
 
@@ -701,7 +719,7 @@ class WebAPIResource(object):
 
         try:
             obj = self.get_object(request, *args, **kwargs)
-        except self.model.DoesNotExist:
+        except ObjectDoesNotExist:
             return DOES_NOT_EXIST
 
         if not self.has_access_permissions(request, obj, *args, **kwargs):
@@ -831,7 +849,7 @@ class WebAPIResource(object):
 
         try:
             obj = self.get_object(request, *args, **kwargs)
-        except self.model.DoesNotExist:
+        except ObjectDoesNotExist:
             return DOES_NOT_EXIST
 
         if not self.has_delete_permissions(request, obj, *args, **kwargs):
