prompt: We are currently solving the following issue within our repository. Here is the issue text:
  --- BEGIN ISSUE ---
  Support different seeds for random.hourly sort
  These carousels are all sorted by random.hourly, but we want them to have a different random subset!
  
  ![image](https://github.com/internetarchive/openlibrary/assets/6251786/408dcd86-f96a-4534-bacf-46f437f9f444)
  
  
  ### Proposal & Constraints
  Expand `random.hourly` sorting to support a custom seed like `random`
  
  ### Additional context
  <!-- Add any other context or screenshots about the feature request here. -->
  
  ### Stakeholders
  @RayBB 
  
  --- END ISSUE ---
  
  Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
  --- BEGIN FILES ---
  Path: `openlibrary/plugins/worksearch/schemes/__init__.py`
  Content:
  ```
  1 import logging
  2 from collections.abc import Callable
  3 
  4 import luqum.tree
  5 from luqum.exceptions import ParseError
  6 from openlibrary.solr.query_utils import (
  7     escape_unknown_fields,
  8     fully_escape_query,
  9     luqum_parser,
  10 )
  11 
  12 logger = logging.getLogger("openlibrary.worksearch")
  13 
  14 
  15 class SearchScheme:
  16     # Set of queries that define the universe of this scheme
  17     universe: list[str]
  18     # All actual solr fields that can be in a user query
  19     all_fields: set[str]
  20     # These fields are fetched for facets and can also be url params
  21     facet_fields: set[str]
  22     # Mapping of user-only fields to solr fields
  23     field_name_map: dict[str, str]
  24     # Mapping of user sort to solr sort
  25     sorts: dict[str, str | Callable[[], str]]
  26     # Default
  27     default_fetched_fields: set[str]
  28     # Fields that should be rewritten
  29     facet_rewrites: dict[tuple[str, str], str | Callable[[], str]]
  30 
  31     def is_search_field(self, field: str):
  32         return field in self.all_fields or field in self.field_name_map
  33 
  34     def process_user_sort(self, user_sort: str) -> str:
  35         """
  36         Convert a user-provided sort to a solr sort
  37 
  38         >>> from openlibrary.plugins.worksearch.schemes.works import WorkSearchScheme
  39         >>> scheme = WorkSearchScheme()
  40         >>> scheme.process_user_sort('editions')
  41         'edition_count desc'
  42         >>> scheme.process_user_sort('editions, new')
  43         'edition_count desc,first_publish_year desc'
  44         >>> scheme.process_user_sort('random')
  45         'random_1 asc'
  46         >>> scheme.process_user_sort('random_custom_seed')
  47         'random_custom_seed asc'
  48         >>> scheme.process_user_sort('random_custom_seed desc')
  49         'random_custom_seed desc'
  50         >>> scheme.process_user_sort('random_custom_seed asc')
  51         'random_custom_seed asc'
  52         """
  53 
  54         def process_individual_sort(sort: str):
  55             if sort.startswith('random_'):
  56                 # Allow custom randoms; so anything random_* is allowed
  57                 return sort if ' ' in sort else f'{sort} asc'
  58             else:
  59                 solr_sort = self.sorts[sort]
  60                 return solr_sort() if callable(solr_sort) else solr_sort
  61 
  62         return ','.join(
  63             process_individual_sort(s.strip()) for s in user_sort.split(',')
  64         )
  65 
  66     def process_user_query(self, q_param: str) -> str:
  67         if q_param == '*:*':
  68             # This is a special solr syntax; don't process
  69             return q_param
  70 
  71         try:
  72             q_param = escape_unknown_fields(
  73                 (
  74                     # Solr 4+ has support for regexes (eg `key:/foo.*/`)! But for now,
  75                     # let's not expose that and escape all '/'. Otherwise
  76                     # `key:/works/OL1W` is interpreted as a regex.
  77                     q_param.strip()
  78                     .replace('/', '\\/')
  79                     # Also escape unexposed lucene features
  80                     .replace('?', '\\?')
  81                     .replace('~', '\\~')
  82                 ),
  83                 self.is_search_field,
  84                 lower=True,
  85             )
  86             q_tree = luqum_parser(q_param)
  87         except ParseError:
  88             # This isn't a syntactically valid lucene query
  89             logger.warning("Invalid lucene query", exc_info=True)
  90             # Escape everything we can
  91             q_tree = luqum_parser(fully_escape_query(q_param))
  92 
  93         q_tree = self.transform_user_query(q_param, q_tree)
  94         return str(q_tree)
  95 
  96     def transform_user_query(
  97         self,
  98         user_query: str,
  99         q_tree: luqum.tree.Item,
  100     ) -> luqum.tree.Item:
  101         return q_tree
  102 
  103     def build_q_from_params(self, params: dict) -> str | None:
  104         return None
  105 
  106     def q_to_solr_params(
  107         self,
  108         q: str,
  109         solr_fields: set[str],
  110         cur_solr_params: list[tuple[str, str]],
  111     ) -> list[tuple[str, str]]:
  112         return [('q', q)]
  113
  ```
  --- END FILES ---
  
  Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.
  
  Here is an example:
  
  ```diff
  diff --git a/examples/server_async.py b/examples/server_async.py
  --- a/examples/server_async.py
  +++ b/examples/server_async.py
  @@ -313,4 +313,4 @@
   
   
   if __name__ == "__main__":
  -    asyncio.run(run_async_server("."), debug=True)
  +    asyncio.run(run_async_server(), debug=True)
  diff --git a/examples/server_sync.py b/examples/server_sync.py
  --- a/examples/server_sync.py
  +++ b/examples/server_sync.py
  @@ -313,5 +313,5 @@
   
   
   if __name__ == "__main__":
  -    server = run_sync_server(".")
  +    server = run_sync_server()
       server.shutdown()
  
  ```
  
golden_diff: diff --git a/openlibrary/plugins/worksearch/schemes/__init__.py b/openlibrary/plugins/worksearch/schemes/__init__.py
  --- a/openlibrary/plugins/worksearch/schemes/__init__.py
  +++ b/openlibrary/plugins/worksearch/schemes/__init__.py
  @@ -44,17 +44,27 @@
           >>> scheme.process_user_sort('random')
           'random_1 asc'
           >>> scheme.process_user_sort('random_custom_seed')
  -        'random_custom_seed asc'
  +        'random_1_custom_seed asc'
           >>> scheme.process_user_sort('random_custom_seed desc')
  -        'random_custom_seed desc'
  +        'random_1_custom_seed desc'
           >>> scheme.process_user_sort('random_custom_seed asc')
  -        'random_custom_seed asc'
  +        'random_1_custom_seed asc'
           """
   
  -        def process_individual_sort(sort: str):
  -            if sort.startswith('random_'):
  +        def process_individual_sort(sort: str) -> str:
  +            if sort.startswith(('random_', 'random.hourly_', 'random.daily_')):
                   # Allow custom randoms; so anything random_* is allowed
  -                return sort if ' ' in sort else f'{sort} asc'
  +                # Also Allow custom time randoms to allow carousels with overlapping
  +                # books to have a fresh ordering when on the same collection
  +                sort_order: str | None = None
  +                if ' ' in sort:
  +                    sort, sort_order = sort.split(' ', 1)
  +                random_type, random_seed = sort.split('_', 1)
  +                solr_sort = self.sorts[random_type]
  +                solr_sort_str = solr_sort() if callable(solr_sort) else solr_sort
  +                solr_sort_field, solr_sort_order = solr_sort_str.split(' ', 1)
  +                sort_order = sort_order or solr_sort_order
  +                return f'{solr_sort_field}_{random_seed} {sort_order}'
               else:
                   solr_sort = self.sorts[sort]
                   return solr_sort() if callable(solr_sort) else solr_sort
  
verification_info: {"golden_diff": "diff --git a/openlibrary/plugins/worksearch/schemes/__init__.py b/openlibrary/plugins/worksearch/schemes/__init__.py\n--- a/openlibrary/plugins/worksearch/schemes/__init__.py\n+++ b/openlibrary/plugins/worksearch/schemes/__init__.py\n@@ -44,17 +44,27 @@\n         >>> scheme.process_user_sort('random')\n         'random_1 asc'\n         >>> scheme.process_user_sort('random_custom_seed')\n-        'random_custom_seed asc'\n+        'random_1_custom_seed asc'\n         >>> scheme.process_user_sort('random_custom_seed desc')\n-        'random_custom_seed desc'\n+        'random_1_custom_seed desc'\n         >>> scheme.process_user_sort('random_custom_seed asc')\n-        'random_custom_seed asc'\n+        'random_1_custom_seed asc'\n         \"\"\"\n \n-        def process_individual_sort(sort: str):\n-            if sort.startswith('random_'):\n+        def process_individual_sort(sort: str) -> str:\n+            if sort.startswith(('random_', 'random.hourly_', 'random.daily_')):\n                 # Allow custom randoms; so anything random_* is allowed\n-                return sort if ' ' in sort else f'{sort} asc'\n+                # Also Allow custom time randoms to allow carousels with overlapping\n+                # books to have a fresh ordering when on the same collection\n+                sort_order: str | None = None\n+                if ' ' in sort:\n+                    sort, sort_order = sort.split(' ', 1)\n+                random_type, random_seed = sort.split('_', 1)\n+                solr_sort = self.sorts[random_type]\n+                solr_sort_str = solr_sort() if callable(solr_sort) else solr_sort\n+                solr_sort_field, solr_sort_order = solr_sort_str.split(' ', 1)\n+                sort_order = sort_order or solr_sort_order\n+                return f'{solr_sort_field}_{random_seed} {sort_order}'\n             else:\n                 solr_sort = self.sorts[sort]\n                 return solr_sort() if callable(solr_sort) else solr_sort\n", "issue": "Support different seeds for random.hourly sort\nThese carousels are all sorted by random.hourly, but we want them to have a different random subset!\r\n\r\n![image](https://github.com/internetarchive/openlibrary/assets/6251786/408dcd86-f96a-4534-bacf-46f437f9f444)\r\n\r\n\r\n### Proposal & Constraints\r\nExpand `random.hourly` sorting to support a custom seed like `random`\r\n\r\n### Additional context\r\n<!-- Add any other context or screenshots about the feature request here. -->\r\n\r\n### Stakeholders\r\n@RayBB \n", "before_files": [{"content": "import logging\nfrom collections.abc import Callable\n\nimport luqum.tree\nfrom luqum.exceptions import ParseError\nfrom openlibrary.solr.query_utils import (\n    escape_unknown_fields,\n    fully_escape_query,\n    luqum_parser,\n)\n\nlogger = logging.getLogger(\"openlibrary.worksearch\")\n\n\nclass SearchScheme:\n    # Set of queries that define the universe of this scheme\n    universe: list[str]\n    # All actual solr fields that can be in a user query\n    all_fields: set[str]\n    # These fields are fetched for facets and can also be url params\n    facet_fields: set[str]\n    # Mapping of user-only fields to solr fields\n    field_name_map: dict[str, str]\n    # Mapping of user sort to solr sort\n    sorts: dict[str, str | Callable[[], str]]\n    # Default\n    default_fetched_fields: set[str]\n    # Fields that should be rewritten\n    facet_rewrites: dict[tuple[str, str], str | Callable[[], str]]\n\n    def is_search_field(self, field: str):\n        return field in self.all_fields or field in self.field_name_map\n\n    def process_user_sort(self, user_sort: str) -> str:\n        \"\"\"\n        Convert a user-provided sort to a solr sort\n\n        >>> from openlibrary.plugins.worksearch.schemes.works import WorkSearchScheme\n        >>> scheme = WorkSearchScheme()\n        >>> scheme.process_user_sort('editions')\n        'edition_count desc'\n        >>> scheme.process_user_sort('editions, new')\n        'edition_count desc,first_publish_year desc'\n        >>> scheme.process_user_sort('random')\n        'random_1 asc'\n        >>> scheme.process_user_sort('random_custom_seed')\n        'random_custom_seed asc'\n        >>> scheme.process_user_sort('random_custom_seed desc')\n        'random_custom_seed desc'\n        >>> scheme.process_user_sort('random_custom_seed asc')\n        'random_custom_seed asc'\n        \"\"\"\n\n        def process_individual_sort(sort: str):\n            if sort.startswith('random_'):\n                # Allow custom randoms; so anything random_* is allowed\n                return sort if ' ' in sort else f'{sort} asc'\n            else:\n                solr_sort = self.sorts[sort]\n                return solr_sort() if callable(solr_sort) else solr_sort\n\n        return ','.join(\n            process_individual_sort(s.strip()) for s in user_sort.split(',')\n        )\n\n    def process_user_query(self, q_param: str) -> str:\n        if q_param == '*:*':\n            # This is a special solr syntax; don't process\n            return q_param\n\n        try:\n            q_param = escape_unknown_fields(\n                (\n                    # Solr 4+ has support for regexes (eg `key:/foo.*/`)! But for now,\n                    # let's not expose that and escape all '/'. Otherwise\n                    # `key:/works/OL1W` is interpreted as a regex.\n                    q_param.strip()\n                    .replace('/', '\\\\/')\n                    # Also escape unexposed lucene features\n                    .replace('?', '\\\\?')\n                    .replace('~', '\\\\~')\n                ),\n                self.is_search_field,\n                lower=True,\n            )\n            q_tree = luqum_parser(q_param)\n        except ParseError:\n            # This isn't a syntactically valid lucene query\n            logger.warning(\"Invalid lucene query\", exc_info=True)\n            # Escape everything we can\n            q_tree = luqum_parser(fully_escape_query(q_param))\n\n        q_tree = self.transform_user_query(q_param, q_tree)\n        return str(q_tree)\n\n    def transform_user_query(\n        self,\n        user_query: str,\n        q_tree: luqum.tree.Item,\n    ) -> luqum.tree.Item:\n        return q_tree\n\n    def build_q_from_params(self, params: dict) -> str | None:\n        return None\n\n    def q_to_solr_params(\n        self,\n        q: str,\n        solr_fields: set[str],\n        cur_solr_params: list[tuple[str, str]],\n    ) -> list[tuple[str, str]]:\n        return [('q', q)]\n", "path": "openlibrary/plugins/worksearch/schemes/__init__.py"}], "after_files": [{"content": "import logging\nfrom collections.abc import Callable\n\nimport luqum.tree\nfrom luqum.exceptions import ParseError\nfrom openlibrary.solr.query_utils import (\n    escape_unknown_fields,\n    fully_escape_query,\n    luqum_parser,\n)\n\nlogger = logging.getLogger(\"openlibrary.worksearch\")\n\n\nclass SearchScheme:\n    # Set of queries that define the universe of this scheme\n    universe: list[str]\n    # All actual solr fields that can be in a user query\n    all_fields: set[str]\n    # These fields are fetched for facets and can also be url params\n    facet_fields: set[str]\n    # Mapping of user-only fields to solr fields\n    field_name_map: dict[str, str]\n    # Mapping of user sort to solr sort\n    sorts: dict[str, str | Callable[[], str]]\n    # Default\n    default_fetched_fields: set[str]\n    # Fields that should be rewritten\n    facet_rewrites: dict[tuple[str, str], str | Callable[[], str]]\n\n    def is_search_field(self, field: str):\n        return field in self.all_fields or field in self.field_name_map\n\n    def process_user_sort(self, user_sort: str) -> str:\n        \"\"\"\n        Convert a user-provided sort to a solr sort\n\n        >>> from openlibrary.plugins.worksearch.schemes.works import WorkSearchScheme\n        >>> scheme = WorkSearchScheme()\n        >>> scheme.process_user_sort('editions')\n        'edition_count desc'\n        >>> scheme.process_user_sort('editions, new')\n        'edition_count desc,first_publish_year desc'\n        >>> scheme.process_user_sort('random')\n        'random_1 asc'\n        >>> scheme.process_user_sort('random_custom_seed')\n        'random_1_custom_seed asc'\n        >>> scheme.process_user_sort('random_custom_seed desc')\n        'random_1_custom_seed desc'\n        >>> scheme.process_user_sort('random_custom_seed asc')\n        'random_1_custom_seed asc'\n        \"\"\"\n\n        def process_individual_sort(sort: str) -> str:\n            if sort.startswith(('random_', 'random.hourly_', 'random.daily_')):\n                # Allow custom randoms; so anything random_* is allowed\n                # Also Allow custom time randoms to allow carousels with overlapping\n                # books to have a fresh ordering when on the same collection\n                sort_order: str | None = None\n                if ' ' in sort:\n                    sort, sort_order = sort.split(' ', 1)\n                random_type, random_seed = sort.split('_', 1)\n                solr_sort = self.sorts[random_type]\n                solr_sort_str = solr_sort() if callable(solr_sort) else solr_sort\n                solr_sort_field, solr_sort_order = solr_sort_str.split(' ', 1)\n                sort_order = sort_order or solr_sort_order\n                return f'{solr_sort_field}_{random_seed} {sort_order}'\n            else:\n                solr_sort = self.sorts[sort]\n                return solr_sort() if callable(solr_sort) else solr_sort\n\n        return ','.join(\n            process_individual_sort(s.strip()) for s in user_sort.split(',')\n        )\n\n    def process_user_query(self, q_param: str) -> str:\n        if q_param == '*:*':\n            # This is a special solr syntax; don't process\n            return q_param\n\n        try:\n            q_param = escape_unknown_fields(\n                (\n                    # Solr 4+ has support for regexes (eg `key:/foo.*/`)! But for now,\n                    # let's not expose that and escape all '/'. Otherwise\n                    # `key:/works/OL1W` is interpreted as a regex.\n                    q_param.strip()\n                    .replace('/', '\\\\/')\n                    # Also escape unexposed lucene features\n                    .replace('?', '\\\\?')\n                    .replace('~', '\\\\~')\n                ),\n                self.is_search_field,\n                lower=True,\n            )\n            q_tree = luqum_parser(q_param)\n        except ParseError:\n            # This isn't a syntactically valid lucene query\n            logger.warning(\"Invalid lucene query\", exc_info=True)\n            # Escape everything we can\n            q_tree = luqum_parser(fully_escape_query(q_param))\n\n        q_tree = self.transform_user_query(q_param, q_tree)\n        return str(q_tree)\n\n    def transform_user_query(\n        self,\n        user_query: str,\n        q_tree: luqum.tree.Item,\n    ) -> luqum.tree.Item:\n        return q_tree\n\n    def build_q_from_params(self, params: dict) -> str | None:\n        return None\n\n    def q_to_solr_params(\n        self,\n        q: str,\n        solr_fields: set[str],\n        cur_solr_params: list[tuple[str, str]],\n    ) -> list[tuple[str, str]]:\n        return [('q', q)]\n", "path": "openlibrary/plugins/worksearch/schemes/__init__.py"}]}