diff --git a/news/17.feature b/news/17.feature
new file mode 100644
index 0000000..1691e05
--- /dev/null
+++ b/news/17.feature
@@ -0,0 +1,3 @@
+Store date information in the paths.
+Can be used as basis for removing for example all redirects that are older than a year.
+[maurits]
diff --git a/news/18.feature b/news/18.feature
new file mode 100644
index 0000000..d9886fa
--- /dev/null
+++ b/news/18.feature
@@ -0,0 +1,2 @@
+Keep track if a redirect was added manually or automatically.
+[maurits]
diff --git a/plone/app/redirector/storage.py b/plone/app/redirector/storage.py
index 91c12c3..dbb2d97 100644
--- a/plone/app/redirector/storage.py
+++ b/plone/app/redirector/storage.py
@@ -3,7 +3,7 @@ from zope.interface import implementer
 
 from persistent import Persistent
 from BTrees.OOBTree import OOBTree, OOSet
-
+from DateTime import DateTime
 from plone.app.redirector.interfaces import IRedirectionStorage
 
 
@@ -33,7 +33,7 @@ class RedirectionStorage(Persistent):
         self._paths = OOBTree()
         self._rpaths = OOBTree()
 
-    def add(self, old_path, new_path):
+    def add(self, old_path, new_path, now=None, manual=False):
         old_path = self._canonical(old_path)
         new_path = self._canonical(new_path)
 
@@ -41,38 +41,68 @@ class RedirectionStorage(Persistent):
             return
 
         # Forget any existing reverse paths to old_path
-        existing_target = self._paths.get(old_path, None)
-        if (existing_target is not None) and (
-            existing_target in self._rpaths):
+        existing_target = self.get(old_path)
+        if (existing_target is not None) and (existing_target in self._rpaths):
+            # old_path was pointing to existing_target, but now we want it to
+            # point to new_path.  So remove the existing reverse path.
             if len(self._rpaths[existing_target]) == 1:
                 del self._rpaths[existing_target]
             else:
                 self._rpaths[existing_target].remove(old_path)
 
+        if now is None:
+            now = DateTime()
+        full_value = (new_path, now, manual)
+
         # Update any references that pointed to old_path
         for p in self.redirects(old_path):
+            # p points to old_path, but old_path will point to new_path,
+            # so we update p to point to new_path directly.
             if p != new_path:
-                self._paths[p] = new_path
+                old_full_value = self._paths[p]
+                if isinstance(old_full_value, tuple):
+                    # keep date and manual
+                    new_full_value = (
+                        new_path,
+                        old_full_value[1],
+                        old_full_value[2],
+                    )
+                else:
+                    new_full_value = full_value
+                self._paths[p] = new_full_value
                 self._rpaths.setdefault(new_path, OOSet()).insert(p)
             else:
+                # There is an existing redirect from new_path to old_path.
+                # We now want to update new_path to point to new_path.
+                # This is not useful, so we delete it.
                 del self._paths[new_path]
 
-        # Remove reverse paths for old_path
+        # Remove reverse paths for old_path.  If old_path was being
+        # redirected to, the above code will have updated those redirects,
+        # so this reverse redirect info is no longer needed.
         if old_path in self._rpaths:
             del self._rpaths[old_path]
 
-        self._paths[old_path] = new_path
+        self._paths[old_path] = full_value
         self._rpaths.setdefault(new_path, OOSet()).insert(old_path)
 
     __setitem__ = add
 
-    def update(self, info):
+    def update(self, info, manual=True):
+        # Bulk update information.
+        # Calling update will usually be done for manual additions (csv upload).
+        now = DateTime()
         for key, value in info.items():
-            self.add(key, value)
+            if isinstance(value, tuple):
+                # This is (new path, datetime, manual),
+                # where datetime may be None.
+                self.add(key, value[0], now=value[1] or now, manual=value[2])
+            else:
+                self.add(key, value, now=now, manual=manual)
 
     def remove(self, old_path):
         old_path = self._canonical(old_path)
-        new_path = self._paths.get(old_path, None)
+        new_path = self.get(old_path)
         if new_path is not None and new_path in self._rpaths:
             if len(self._rpaths[new_path]) == 1:
                 del self._rpaths[new_path]
@@ -82,6 +112,50 @@ class RedirectionStorage(Persistent):
 
     __delitem__ = remove
 
+    def _rebuild(self):
+        """Rebuild the information.
+
+        Can be used in migration to initialize the date and manual information.
+
+        For good measure, this also rebuild the _rpaths structure:
+        the _paths structure is leading.  For one million paths,
+        the _paths rebuilding takes 1 second,
+        and the _rpaths an extra 3 seconds.  Seems fine, as this should
+        rarely be used.
+        """
+        now = DateTime()
+        self._rpaths = OOBTree()
+        for old_path in self._paths:
+            new_info = self._paths[old_path]
+            if isinstance(new_info, tuple):
+                new_path = new_info[0]
+            else:
+                # Store as tuple: (new_path, date, manual).
+                # We cannot know if this was a manual redirect or not.
+                # For safety we register this as a manual one.
+                new_path = new_info
+                new_info = (new_path, now, True)
+                self._paths[old_path] = new_info
+            self._rpaths.setdefault(new_path, OOSet()).insert(old_path)
+
+        # Look for inconsistenties and fix them:
+        # paths that are both in paths and in rpaths.
+        bads = [
+            new_path for new_path in self._rpaths if new_path in self._paths
+        ]
+        for new_path in bads:
+            newer_path = self._paths[new_path][0]
+            for old_path in self._rpaths[new_path]:
+                # old_path points to new_path,
+                # but new_path points to newer_path.
+                # So update old_path to point to newer_path.
+                info = self._paths[old_path]
+                info = (newer_path, info[1], info[2])
+                self._paths[old_path] = info
+                self._rpaths[newer_path].insert(old_path)
+            # self._rpaths[new_path] is empty now
+            del self._rpaths[new_path]
+
     def destroy(self, new_path):
         new_path = self._canonical(new_path)
         for p in self._rpaths.get(new_path, []):
@@ -99,7 +173,19 @@ class RedirectionStorage(Persistent):
 
     def get(self, old_path, default=None):
         old_path = self._canonical(old_path)
-        return self._paths.get(old_path, default)
+        new_path = self._paths.get(old_path, default)
+        if isinstance(new_path, tuple):
+            # (new_path, date, manual)
+            return new_path[0]
+        return new_path
+
+    def get_full(self, old_path, default=None):
+        old_path = self._canonical(old_path)
+        new_path = self._paths.get(old_path, default)
+        if isinstance(new_path, tuple):
+            # (new_path, date, manual)
+            return new_path
+        return (new_path, None, True)
 
     def __getitem__(self, old_path):
         result = self.get(old_path, default=_marker)

Store date on which a redirect has been added
This would be a basis for a control panel change where you can for example remove all redirects that have been there for over a year.
See https://github.com/plone/Products.CMFPlone/issues/1486#issuecomment-462974939
diff --git a/plone/app/redirector/tests/test_performance.py b/plone/app/redirector/tests/test_performance.py
index c8821db..d238206 100644
--- a/plone/app/redirector/tests/test_performance.py
+++ b/plone/app/redirector/tests/test_performance.py
@@ -136,3 +136,9 @@ class TestStoragePerformance(unittest.TestCase):
         ):
             for key in st:
                 st[key]
+
+        # Can take long.  But 10.000 per second should be no problem.
+        with self.timeit(
+            'Rebuilding the structure for migration', NUMBER / 100000.0
+        ):
+            st._rebuild()
diff --git a/plone/app/redirector/tests/test_storage.py b/plone/app/redirector/tests/test_storage.py
index 1967798..7000d9b 100644
--- a/plone/app/redirector/tests/test_storage.py
+++ b/plone/app/redirector/tests/test_storage.py
@@ -1,4 +1,5 @@
 # -*- coding: utf-8 -*-
+from DateTime import DateTime
 from plone.app.redirector.storage import RedirectionStorage
 
 import unittest
@@ -25,6 +26,31 @@ class TestStorage(unittest.TestCase):
         with self.assertRaises(KeyError):
             st['/bar']
 
+    def test_storage_get_full_standard(self):
+        # get_full gets the full tuple instead of only the path
+        st = RedirectionStorage()
+        time1 = DateTime()
+        st.add('/foo', '/bar', now=time1, manual=False)
+        full = st.get_full('/foo')
+        self.assertIsInstance(full, tuple)
+        self.assertEqual(full, st._paths['/foo'])
+        self.assertEqual(full[0], '/bar')
+        self.assertEqual(full[1], time1)
+        self.assertFalse(full[2])  # manual
+
+    def test_storage_get_full_fallback(self):
+        # get_full gets the full tuple,
+        # even if the unmigrated data only has the path
+        st = RedirectionStorage()
+        st._paths['/foo'] = '/bar'
+        self.assertEqual(st._paths['/foo'], '/bar')
+        full = st.get_full('/foo')
+        self.assertIsInstance(full, tuple)
+        self.assertEqual(full[0], '/bar')
+        # Instead of a DateTime, we get None in the fallback
+        self.assertIsNone(full[1])
+        self.assertTrue(full[2])  # manual
+
     def test_storage_no_slash(self):
         # Standard Plone will created redirects with key
         # /plone-site-id/some/path.
@@ -51,7 +77,9 @@ class TestStorage(unittest.TestCase):
         self.assertTrue(st.has_path('/plone/some/path'))
         self.assertEqual(st.get('/plone/some/path'), '/plone/a/different/path')
         self.assertFalse(st.has_path('/plone/a/different/path'))
-        self.assertListEqual(st.redirects('/plone/a/different/path'), ['/plone/some/path'])
+        self.assertListEqual(
+            st.redirects('/plone/a/different/path'), ['/plone/some/path']
+        )
         self.assertIn('/plone/some/path', st)
         self.assertNotIn('/plone/a/different/path', st)
         self.assertEqual(st['/plone/some/path'], '/plone/a/different/path')
@@ -84,6 +112,63 @@ class TestStorage(unittest.TestCase):
         with self.assertRaises(KeyError):
             st['/baaz']
 
+    def test_storage_date(self):
+        # Add one redirect
+        st = RedirectionStorage()
+        time1 = DateTime()
+        st.add('/foo', '/bar')
+        time2 = DateTime()
+        # Check the internals: we now store a date (and manual True/False).
+        info = st._paths['/foo']
+        self.assertIsInstance(info, tuple)
+        self.assertTrue(time1 < info[1] < time2)
+        # Use an explicit date.
+        now = DateTime(2000, 12, 31)
+        st.add('/exp', '/bar', now=now)
+        info = st._paths['/exp']
+        self.assertIsInstance(info, tuple)
+        self.assertEqual(info[1], now)
+        # Update with a different date.
+        st.add('/exp', '/bar', now=time1)
+        info = st._paths['/exp']
+        self.assertIsInstance(info, tuple)
+        self.assertEqual(info[1], time1)
+        # Update with an implicit date.
+        st.add('/exp', '/bar')
+        time3 = DateTime()
+        info = st._paths['/exp']
+        self.assertIsInstance(info, tuple)
+        self.assertTrue(time2 < info[1] < time3)
+
+    def test_storage_manual(self):
+        # Add one redirect
+        st = RedirectionStorage()
+        st.add('/foo', '/bar')
+        # Check the internals: we now store manual True/False (and a date).
+        info = st._paths['/foo']
+        self.assertIsInstance(info, tuple)
+        self.assertIsInstance(info[2], bool)
+        self.assertFalse(info[2])
+        # Store a manual one.
+        st.add('/exp', '/bar', manual=True)
+        info = st._paths['/exp']
+        self.assertIsInstance(info, tuple)
+        self.assertIsInstance(info[2], bool)
+        self.assertTrue(info[2])
+        # Update to non-manual (the default).
+        st.add('/exp', '/bar')
+        info = st._paths['/exp']
+        self.assertIsInstance(info, tuple)
+        self.assertIsInstance(info[2], bool)
+        self.assertFalse(info[2])
+        # Make the original non-manual one manual.
+        st.add('/foo', '/bar', manual=True)
+        # Check the internals: we now store manual True/False (and a date).
+        info = st._paths['/foo']
+        self.assertIsInstance(info, tuple)
+        self.assertIsInstance(info[2], bool)
+        self.assertTrue(info[2])
+
     def test_storage_two_redirects_plain(self):
         # Add multiple redirects.
         st = RedirectionStorage()
@@ -291,3 +376,183 @@ class TestStorage(unittest.TestCase):
             st[0] = '/bar'
         with self.assertRaises(AttributeError):
             st['/foo'] = 0
+
+    def test_storage_update_paths(self):
+        st = RedirectionStorage()
+        info = {}
+        time1 = DateTime()
+        for i in range(10):
+            info['/old/{0}'.format(i)] = '/new/{0}'.format(i)
+        st.update(info)
+        time2 = DateTime()
+        self.assertEqual(len(st), 10)
+        self.assertEqual(st.get('/old/0'), '/new/0')
+        self.assertEqual(st.get('/old/1'), '/new/1')
+        self.assertTrue(time1 < st.get_full('/old/0')[1] < time2)
+        self.assertTrue(st.get_full('/old/0')[2])
+
+    def test_storage_update_tuple(self):
+        st = RedirectionStorage()
+        info = {}
+        for i in range(10):
+            info['/old/{0}'.format(i)] = (
+                '/new/{0}'.format(i),
+                DateTime(),
+                False,
+            )
+        st.update(info)
+        self.assertEqual(st.get('/old/0'), '/new/0')
+        self.assertEqual(st.get('/old/1'), '/new/1')
+        self.assertEqual(st.get_full('/old/0'), info['/old/0'])
+
+    def test_storage_update_keeps_info(self):
+        st = RedirectionStorage()
+        info = {}
+        time1 = DateTime()
+        time2 = DateTime()
+        info['/old/1'] = ('/new', time1, False)
+        info['/old/2'] = ('/new', time2, True)
+        st.update(info)
+        self.assertEqual(len(st), 2)
+        self.assertEqual(st.get('/old/1'), '/new')
+        self.assertEqual(st.get('/old/2'), '/new')
+        self.assertListEqual(
+            sorted(st.redirects('/new')), ['/old/1', '/old/2']
+        )
+        self.assertEqual(st.get_full('/old/1'), info['/old/1'])
+        self.assertEqual(st.get_full('/old/2'), info['/old/2'])
+        # New info
+        del info['/old/1']
+        time3 = DateTime()
+        info['/old/2'] = ('/new/2', time2, False)
+        info['/old/3'] = ('/new', time3, True)
+        st.update(info)
+        self.assertEqual(len(st), 3)
+        self.assertEqual(st.get('/old/1'), '/new')
+        self.assertEqual(st.get('/old/2'), '/new/2')
+        self.assertEqual(st.get('/old/3'), '/new')
+        self.assertListEqual(
+            sorted(st.redirects('/new')), ['/old/1', '/old/3']
+        )
+        self.assertListEqual(sorted(st.redirects('/new/2')), ['/old/2'])
+        self.assertEqual(st.get_full('/old/2'), info['/old/2'])
+        self.assertEqual(st.get_full('/old/3'), info['/old/3'])
+
+    def test_storage_update_mixed(self):
+        st = RedirectionStorage()
+        info = {}
+        time1 = DateTime()
+        for i in range(10):
+            info['/old/{0}'.format(i)] = '/new/{0}'.format(i)
+        for i in range(10, 20):
+            info['/old/{0}'.format(i)] = (
+                '/new/{0}'.format(i),
+                DateTime(),
+                False,
+            )
+        for i in range(20, 30):
+            info['/old/{0}'.format(i)] = ('/new/{0}'.format(i), None, True)
+        st.update(info)
+        time2 = DateTime()
+        self.assertEqual(len(st), 30)
+        self.assertEqual(st.get('/old/0'), '/new/0')
+        self.assertEqual(st.get('/old/1'), '/new/1')
+        self.assertTrue(time1 < st.get_full('/old/0')[1] < time2)
+        self.assertTrue(st.get_full('/old/0')[2])
+        self.assertEqual(st.get('/old/10'), '/new/10')
+        self.assertEqual(st.get('/old/11'), '/new/11')
+        self.assertEqual(st.get_full('/old/10'), info['/old/10'])
+        self.assertEqual(st.get('/old/20'), '/new/20')
+        self.assertEqual(st.get('/old/21'), '/new/21')
+        self.assertTrue(time1 < st.get_full('/old/20')[1] < time2)
+        self.assertTrue(st.get_full('/old/20')[2])
+        # Update again with the same info.
+        # This may set new dates.
+        st.update(info)
+        time3 = DateTime()
+        self.assertEqual(len(st), 30)
+        self.assertEqual(st.get('/old/0'), '/new/0')
+        self.assertTrue(time2 < st.get_full('/old/0')[1] < time3)
+        self.assertTrue(st.get_full('/old/0')[2])
+        self.assertEqual(st.get('/old/10'), '/new/10')
+        self.assertEqual(st.get_full('/old/10'), info['/old/10'])
+        self.assertEqual(st.get('/old/20'), '/new/20')
+        self.assertTrue(time2 < st.get_full('/old/20')[1] < time3)
+        self.assertTrue(st.get_full('/old/20')[2])
+
+    def test_rebuild(self):
+        # Rebuild the internal information.
+        # This is mostly meant to be used in migration
+        # to initialize the date and manual information.
+        st = RedirectionStorage()
+        # Should run fine on an empty storage.
+        st._rebuild()
+        # Set internals directly.
+        st._paths['/old'] = '/new'
+        st._paths['/older'] = '/new'
+        st._paths['/first'] = '/second'
+        st._rpaths['/unused'] = '/unknown'
+        # Add some bad redirects, which should have been updated to point to /new.
+        st._paths['/bad'] = '/old'
+        st._paths['/worse'] = '/old'
+        st._paths['/worst'] = '/worse'
+        self.assertIsInstance(st._paths['/old'], str)
+        self.assertEqual(st._paths['/old'], '/new')
+        self.assertEqual(len(st._paths), 6)
+        self.assertEqual(len(st._rpaths), 1)
+
+        # Rebuild
+        time1 = DateTime()
+        st._rebuild()
+        time2 = DateTime()
+        # The _paths should be tuples now.
+        self.assertEqual(
+            sorted(list(st._paths)),
+            ['/bad', '/first', '/old', '/older', '/worse', '/worst'],
+        )
+        info = st._paths['/old']
+        self.assertIsInstance(info, tuple)
+        # The good ones were pointing to /new or /second, which should stay the same,
+        # but the bad ones have been updated to point to new as well.
+        self.assertSetEqual(
+            set([path[0] for path in st._paths.values()]),
+            set(['/new', '/second']),
+        )
+        # Date should be set to the same for all.
+        self.assertIsInstance(info[1], DateTime)
+        new_time = info[1]
+        self.assertTrue(time1 < new_time < time2)
+        self.assertSetEqual(
+            set([path[1] for path in st._paths.values()]), set([new_time])
+        )
+        # manual is set to True when migrating to tuples:
+        self.assertEqual(info[2], True)
+        # _rpaths should be filled now with only the new one.
+        self.assertEqual(len(st._rpaths), 2)
+        self.assertNotIn('/unused', st._rpaths)
+        self.assertEqual(sorted(list(st._rpaths['/second'])), ['/first'])
+        self.assertEqual(
+            sorted(list(st._rpaths['/new'])),
+            ['/bad', '/old', '/older', '/worse', '/worst'],
+        )
+
+        # Rebuild again.  Nothing fundamentally should have changed,
+        # except that _rpaths have been recreated.
+        old_paths = st._paths
+        old_rpaths = st._rpaths
+        st._rebuild()
+        self.assertIs(old_paths, st._paths)
+        self.assertIsNot(old_rpaths, st._rpaths)
+        self.assertListEqual(
+            sorted(list(old_rpaths)), sorted(list(st._rpaths))
+        )
+        self.assertSetEqual(
+            set([path[0] for path in st._paths.values()]),
+            set(['/new', '/second']),
+        )
+        self.assertSetEqual(
+            set([path[1] for path in st._paths.values()]), set([new_time])
+        )
+        self.assertSetEqual(
+            set([path[2] for path in st._paths.values()]), set([True])
+        )

pip install -e .[test]
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
AccessControl==6.3
Acquisition==5.2
attrs @ file:///croot/attrs_1668696182826/work
AuthEncoding==5.0
backports.zoneinfo==0.2.1
beautifulsoup4==4.13.3
borg.localrole==3.1.9
BTrees==5.2
calmjs.parse==1.3.3
certifi @ file:///croot/certifi_1671487769961/work/certifi
cffi==1.15.1
Chameleon==4.2.0
collective.monkeypatcher==1.2.2
cssselect==1.2.0
DateTime==5.5
decorator==5.1.1
diazo==1.5.0
DocumentTemplate==4.6
docutils==0.20.1
ExtensionClass==5.1
feedparser==6.0.11
five.customerize==3.0.0
five.intid==1.2.6
five.localsitemanager==4.0
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
future==1.0.0
icalendar==5.0.13
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
importlib-resources==5.12.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
jsonschema==4.17.3
lxml==5.3.1
Markdown==3.4.4
Missing==5.0
mockup==4.0.2
MultiMapping==5.0
multipart==1.1.0
packaging @ file:///croot/packaging_1671697413597/work
PasteDeploy==3.1.0
Persistence==4.1
persistent==5.2
piexif==1.1.3
Pillow==9.5.0
pkgutil_resolve_name==1.3.10
plone.alterego==1.1.7
plone.api==1.11.2
plone.app.content==4.0.2
plone.app.contentlisting==2.0.8
plone.app.contentmenu==2.3.4
plone.app.contentrules==4.1.6
plone.app.contenttypes==2.2.3
plone.app.customerize==1.3.12
plone.app.dexterity==2.6.11
plone.app.discussion==4.0.0
plone.app.event==5.0.1
plone.app.i18n==3.0.6
plone.app.intid==1.1.4
plone.app.layout==4.0.1
plone.app.linkintegrity==3.6.2
plone.app.locales==6.0.11
plone.app.lockingbehavior==1.0.7
plone.app.multilingual==5.6.6
plone.app.portlets==4.4.9
plone.app.querystring==2.0.0
-e git+https://github.com/plone/plone.app.redirector.git@7c9583542612ff60f0096dde4789f413dddaf1f0#egg=plone.app.redirector
plone.app.registry==1.7.9
plone.app.relationfield==2.0.3
plone.app.testing==7.1.1
plone.app.textfield==1.3.7
plone.app.theming==4.1.8
plone.app.users==3.0.0
plone.app.uuid==2.2.1
plone.app.versioningbehavior==1.4.6
plone.app.viewletmanager==4.0.0
plone.app.vocabularies==4.3.0
plone.app.widgets==4.0.0b1
plone.app.workflow==4.0.4
plone.app.z3cform==3.2.5
plone.autoform==2.0.0
plone.base==1.0.0b5
plone.batching==2.0.0
plone.behavior==1.4.0
plone.browserlayer==3.0.0
plone.caching==1.2.2
plone.contentrules==2.1.3
plone.dexterity==2.11.0
plone.event==1.4.2
plone.folder==3.1.0
plone.formwidget.namedfile==3.0.1
plone.formwidget.recurrence==2.1.5
plone.i18n==4.0.7
plone.indexer==1.0.7
plone.intelligenttext==3.1.0
plone.keyring==3.2.1
plone.locking==2.3.0
plone.memoize==3.0.0
plone.namedfile==6.4.0
plone.outputfilters==5.0.0
plone.portlet.collection==4.0.0
plone.portlet.static==3.1.6
plone.portlets==2.3.3
plone.protect==4.1.8
plone.registry==1.2.1
plone.resource==2.1.4
plone.resourceeditor==3.0.4
plone.rfc822==2.0.2
plone.scale==4.0.1
plone.schema==1.4.0
plone.schemaeditor==4.0.0
plone.session==4.0.0
plone.staticresources==2.0.1
plone.stringinterp==2.0.0
plone.subrequest==2.0.0
plone.supermodel==2.0.0
plone.synchronize==1.0.4
plone.testing==8.0.4
plone.theme==3.0.7
plone.transformchain==2.0.2
plone.uuid==1.0.6
plone.z3cform==2.0.0
plonetheme.barceloneta==2.1.11
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
ply==3.11
Products.BTreeFolder2==5.1
Products.CMFCore==3.5
Products.CMFDiffTool==3.3.3
Products.CMFDynamicViewFTI==6.0.3
Products.CMFEditions==4.0.0
Products.CMFFormController==4.1.4
Products.CMFPlone==5.2.15
Products.CMFQuickInstallerTool==4.0.4
Products.CMFUid==4.2
Products.DateRecurringIndex==3.0.1
Products.DCWorkflow==3.0
Products.ExtendedPathIndex==4.0.1
Products.ExternalMethod==5.0
Products.GenericSetup==3.0.2
Products.isurlinportal==1.2.1
Products.MailHost==5.2
Products.MimetypesRegistry==2.1.9
Products.PlonePAS==7.0.0
Products.PluggableAuthService==3.0
Products.PluginRegistry==2.0
Products.PortalTransforms==3.2.2
Products.PythonScripts==5.0
Products.Sessions==5.0
Products.SiteErrorLog==6.0
Products.StandardCacheManagers==5.0
Products.statusmessages==5.0.5
Products.TemporaryFolder==7.0
Products.ZCatalog==7.1
Products.ZODBMountPoint==2.0
Products.ZopeVersionControl==4.1
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pycparser==2.21
pyrsistent==0.19.3
pyScss==1.4.0
pytest==7.1.2
python-dateutil==2.9.0.post0
python-gettext==5.0
pytz==2025.2
Record==4.1
repoze.xmliter==0.6.1
RestrictedPython==7.3
roman==4.2
sgmllib3k==1.0.0
simplejson==3.20.1
six==1.17.0
soupsieve==2.4.1
tempstorage==6.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
transaction==4.0
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
Unidecode==1.3.8
waitress==2.1.2
WebOb==1.8.9
WebTest==3.0.1
WSGIProxy2==0.5.1
z3c.autoinclude==1.0
z3c.caching==3.0
z3c.form==5.1
z3c.formwidget.query==2.0.0
z3c.objpath==2.0
z3c.pt==4.4
z3c.relationfield==1.1
z3c.zcmlhook==2.0
zc.lockfile==3.0.post1
zc.relation==2.0
ZConfig==4.1
zdaemon==5.1
ZEO==6.0.0
zExceptions==5.0
zipp @ file:///croot/zipp_1672387121353/work
ZODB==6.0
ZODB3==3.11.0
zodbpickle==3.3
Zope==5.10
zope.annotation==5.0
zope.app.locales==5.0
zope.browser==3.0
zope.browsermenu==5.0
zope.browserpage==5.0
zope.browserresource==5.1
zope.cachedescriptors==5.0
zope.component==6.0
zope.componentvocabulary==2.3.0
zope.configuration==5.0.1
zope.container==5.2
zope.contentprovider==5.0
zope.contenttype==5.1
zope.copy==4.3
zope.datetime==5.0.0
zope.deferredimport==5.0
zope.deprecation==5.0
zope.dottedname==6.0
zope.event==5.0
zope.exceptions==5.1
zope.filerepresentation==6.0
zope.globalrequest==2.0
zope.hookable==6.0
zope.i18n==5.1
zope.i18nmessageid==6.1.0
zope.interface==6.4.post2
zope.intid==5.0
zope.keyreference==6.0
zope.lifecycleevent==5.0
zope.location==5.0
zope.pagetemplate==5.1
zope.processlifetime==3.0
zope.proxy==5.3
zope.ptresource==5.0
zope.publisher==7.0
zope.ramcache==3.0
zope.schema==7.0.1
zope.security==6.2
zope.sendmail==6.2
zope.sequencesort==5.0
zope.site==5.0
zope.size==5.0
zope.structuredtext==5.0
zope.tal==5.0.1
zope.tales==6.0
zope.testbrowser==6.0
zope.testing==5.0.1
zope.traversing==5.0
zope.viewlet==5.0
Zope2==4.0

name: plone.app.redirector
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - accesscontrol==6.3
      - acquisition==5.2
      - authencoding==5.0
      - backports-zoneinfo==0.2.1
      - beautifulsoup4==4.13.3
      - borg-localrole==3.1.9
      - btrees==5.2
      - calmjs-parse==1.3.3
      - cffi==1.15.1
      - chameleon==4.2.0
      - collective-monkeypatcher==1.2.2
      - cssselect==1.2.0
      - datetime==5.5
      - decorator==5.1.1
      - diazo==1.5.0
      - documenttemplate==4.6
      - docutils==0.20.1
      - extensionclass==5.1
      - feedparser==6.0.11
      - five-customerize==3.0.0
      - five-intid==1.2.6
      - five-localsitemanager==4.0
      - future==1.0.0
      - icalendar==5.0.13
      - importlib-resources==5.12.0
      - jsonschema==4.17.3
      - lxml==5.3.1
      - markdown==3.4.4
      - missing==5.0
      - mockup==4.0.2
      - multimapping==5.0
      - multipart==1.1.0
      - pastedeploy==3.1.0
      - persistence==4.1
      - persistent==5.2
      - piexif==1.1.3
      - pillow==9.5.0
      - pkgutil-resolve-name==1.3.10
      - plone-alterego==1.1.7
      - plone-api==1.11.2
      - plone-app-content==4.0.2
      - plone-app-contentlisting==2.0.8
      - plone-app-contentmenu==2.3.4
      - plone-app-contentrules==4.1.6
      - plone-app-contenttypes==2.2.3
      - plone-app-customerize==1.3.12
      - plone-app-dexterity==2.6.11
      - plone-app-discussion==4.0.0
      - plone-app-event==5.0.1
      - plone-app-i18n==3.0.6
      - plone-app-intid==1.1.4
      - plone-app-layout==4.0.1
      - plone-app-linkintegrity==3.6.2
      - plone-app-locales==6.0.11
      - plone-app-lockingbehavior==1.0.7
      - plone-app-multilingual==5.6.6
      - plone-app-portlets==4.4.9
      - plone-app-querystring==2.0.0
      - plone-app-redirector==2.0.2.dev0
      - plone-app-registry==1.7.9
      - plone-app-relationfield==2.0.3
      - plone-app-testing==7.1.1
      - plone-app-textfield==1.3.7
      - plone-app-theming==4.1.8
      - plone-app-users==3.0.0
      - plone-app-uuid==2.2.1
      - plone-app-versioningbehavior==1.4.6
      - plone-app-viewletmanager==4.0.0
      - plone-app-vocabularies==4.3.0
      - plone-app-widgets==4.0.0b1
      - plone-app-workflow==4.0.4
      - plone-app-z3cform==3.2.5
      - plone-autoform==2.0.0
      - plone-base==1.0.0b5
      - plone-batching==2.0.0
      - plone-behavior==1.4.0
      - plone-browserlayer==3.0.0
      - plone-caching==1.2.2
      - plone-contentrules==2.1.3
      - plone-dexterity==2.11.0
      - plone-event==1.4.2
      - plone-folder==3.1.0
      - plone-formwidget-namedfile==3.0.1
      - plone-formwidget-recurrence==2.1.5
      - plone-i18n==4.0.7
      - plone-indexer==1.0.7
      - plone-intelligenttext==3.1.0
      - plone-keyring==3.2.1
      - plone-locking==2.3.0
      - plone-memoize==3.0.0
      - plone-namedfile==6.4.0
      - plone-outputfilters==5.0.0
      - plone-portlet-collection==4.0.0
      - plone-portlet-static==3.1.6
      - plone-portlets==2.3.3
      - plone-protect==4.1.8
      - plone-registry==1.2.1
      - plone-resource==2.1.4
      - plone-resourceeditor==3.0.4
      - plone-rfc822==2.0.2
      - plone-scale==4.0.1
      - plone-schema==1.4.0
      - plone-schemaeditor==4.0.0
      - plone-session==4.0.0
      - plone-staticresources==2.0.1
      - plone-stringinterp==2.0.0
      - plone-subrequest==2.0.0
      - plone-supermodel==2.0.0
      - plone-synchronize==1.0.4
      - plone-testing==8.0.4
      - plone-theme==3.0.7
      - plone-transformchain==2.0.2
      - plone-uuid==1.0.6
      - plone-z3cform==2.0.0
      - plonetheme-barceloneta==2.1.11
      - ply==3.11
      - products-btreefolder2==5.1
      - products-cmfcore==3.5
      - products-cmfdifftool==3.3.3
      - products-cmfdynamicviewfti==6.0.3
      - products-cmfeditions==4.0.0
      - products-cmfformcontroller==4.1.4
      - products-cmfplone==5.2.15
      - products-cmfquickinstallertool==4.0.4
      - products-cmfuid==4.2
      - products-daterecurringindex==3.0.1
      - products-dcworkflow==3.0
      - products-extendedpathindex==4.0.1
      - products-externalmethod==5.0
      - products-genericsetup==3.0.2
      - products-isurlinportal==1.2.1
      - products-mailhost==5.2
      - products-mimetypesregistry==2.1.9
      - products-plonepas==7.0.0
      - products-pluggableauthservice==3.0
      - products-pluginregistry==2.0
      - products-portaltransforms==3.2.2
      - products-pythonscripts==5.0
      - products-sessions==5.0
      - products-siteerrorlog==6.0
      - products-standardcachemanagers==5.0
      - products-statusmessages==5.0.5
      - products-temporaryfolder==7.0
      - products-zcatalog==7.1
      - products-zodbmountpoint==2.0
      - products-zopeversioncontrol==4.1
      - pycparser==2.21
      - pyrsistent==0.19.3
      - pyscss==1.4.0
      - python-dateutil==2.9.0.post0
      - python-gettext==5.0
      - pytz==2025.2
      - record==4.1
      - repoze-xmliter==0.6.1
      - restrictedpython==7.3
      - roman==4.2
      - sgmllib3k==1.0.0
      - simplejson==3.20.1
      - six==1.17.0
      - soupsieve==2.4.1
      - tempstorage==6.0
      - transaction==4.0
      - unidecode==1.3.8
      - waitress==2.1.2
      - webob==1.8.9
      - webtest==3.0.1
      - wsgiproxy2==0.5.1
      - z3c-autoinclude==1.0
      - z3c-caching==3.0
      - z3c-form==5.1
      - z3c-formwidget-query==2.0.0
      - z3c-objpath==2.0
      - z3c-pt==4.4
      - z3c-relationfield==1.1
      - z3c-zcmlhook==2.0
      - zc-lockfile==3.0.post1
      - zc-relation==2.0
      - zconfig==4.1
      - zdaemon==5.1
      - zeo==6.0.0
      - zexceptions==5.0
      - zodb==6.0
      - zodb3==3.11.0
      - zodbpickle==3.3
      - zope==5.10
      - zope-annotation==5.0
      - zope-app-locales==5.0
      - zope-browser==3.0
      - zope-browsermenu==5.0
      - zope-browserpage==5.0
      - zope-browserresource==5.1
      - zope-cachedescriptors==5.0
      - zope-component==6.0
      - zope-componentvocabulary==2.3.0
      - zope-configuration==5.0.1
      - zope-container==5.2
      - zope-contentprovider==5.0
      - zope-contenttype==5.1
      - zope-copy==4.3
      - zope-datetime==5.0.0
      - zope-deferredimport==5.0
      - zope-deprecation==5.0
      - zope-dottedname==6.0
      - zope-event==5.0
      - zope-exceptions==5.1
      - zope-filerepresentation==6.0
      - zope-globalrequest==2.0
      - zope-hookable==6.0
      - zope-i18n==5.1
      - zope-i18nmessageid==6.1.0
      - zope-interface==6.4.post2
      - zope-intid==5.0
      - zope-keyreference==6.0
      - zope-lifecycleevent==5.0
      - zope-location==5.0
      - zope-pagetemplate==5.1
      - zope-processlifetime==3.0
      - zope-proxy==5.3
      - zope-ptresource==5.0
      - zope-publisher==7.0
      - zope-ramcache==3.0
      - zope-schema==7.0.1
      - zope-security==6.2
      - zope-sendmail==6.2
      - zope-sequencesort==5.0
      - zope-site==5.0
      - zope-size==5.0
      - zope-structuredtext==5.0
      - zope-tal==5.0.1
      - zope-tales==6.0
      - zope-testbrowser==6.0
      - zope-testing==5.0.1
      - zope-traversing==5.0
      - zope-viewlet==5.0
      - zope2==4.0
prefix: /opt/conda/envs/plone.app.redirector

plone/app/redirector/tests/test_performance.py::TestStoragePerformance::test_storage_performance plone/app/redirector/tests/test_storage.py::TestStorage::test_rebuild plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_date plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_get_full_fallback plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_get_full_standard plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_manual plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_update_keeps_info plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_update_mixed plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_update_paths plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_update_tuple
plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_chain plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_clear plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_destroy_target plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_iterator plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_len plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_nested plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_no_circular plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_no_slash plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_non_string_path_fails plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_one_redirect plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_remove_redirect_plain plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_remove_redirect_pythonic plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_three_step_circular_rename plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_trailing_slash plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_two_redirects_plain plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_two_redirects_pythonic plone/app/redirector/tests/test_storage.py::TestStorage::test_storage_update_redirect
plone/app/redirector/storage.py news/17.feature news/18.feature
plone/app/redirector/storage.py news/17.feature news/18.feature