We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Display author date(s) with name for librarians
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
Author disambiguation is difficult and even more difficult when the author names all look the same for different identities. To see precisely which author is associated with a work, one must either click the author to view the profile or go into edit mode and use the author selection box.

### Describe the problem that you'd like solved
<!-- A clear and concise description of what you want to happen. -->
Authors should display their associated years to make this task less cumbersome. 

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->

<!-- Which suggestions or requirements should be considered for how feature needs to appear or be implemented? -->
Only the year is necessary, any other date information does not need to be displayed.
This should be a librarian-feature only.

### Additional context
<!-- Add any other context or screenshots about the feature request here. -->

For example, these are two different Stephen Kings:
<img width="454" alt="Screenshot 2023-04-19 at 09 02 16" src="https://user-images.githubusercontent.com/17739465/232992930-9b99cdaf-2e84-491f-bea3-ec4457b18a32.png">
<img width="484" alt="Screenshot 2023-04-19 at 09 02 38" src="https://user-images.githubusercontent.com/17739465/232993031-8c846762-0dee-4d60-9c83-b10100d4be65.png">

A display like this would make disambiguation much easier and less time-consuming:
<img width="314" alt="Screenshot 2023-04-19 at 09 04 35" src="https://user-images.githubusercontent.com/17739465/232993643-55f2d88f-ff8f-487b-8931-aaa447f54acf.png">
<img width="466" alt="Screenshot 2023-04-19 at 09 04 47" src="https://user-images.githubusercontent.com/17739465/232993680-717724ab-907f-473c-9c4a-048575328ed1.png">

### Stakeholders
<!-- @ tag stakeholders of this bug -->




--- 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/core/helpers.py`
Content:
```
1 """Generic helper functions to use in the templates and the webapp.
2 """
3 
4 import json
5 import re
6 from datetime import datetime, date
7 from urllib.parse import urlsplit
8 
9 import web
10 
11 import babel
12 import babel.core
13 import babel.dates
14 import babel.numbers
15 
16 try:
17     import genshi
18     import genshi.filters
19 except ImportError:
20     genshi = None
21 
22 try:
23     from bs4 import BeautifulSoup
24 except ImportError:
25     BeautifulSoup = None
26 
27 from infogami import config
28 from infogami.infobase.client import Nothing
29 
30 # handy utility to parse ISO date strings
31 from infogami.infobase.utils import parse_datetime
32 from infogami.utils.view import safeint
33 
34 __all__ = [
35     "sanitize",
36     "json_encode",
37     "safesort",
38     "days_since",
39     "datestr",
40     "format_date",
41     "sprintf",
42     "cond",
43     "commify",
44     "truncate",
45     "datetimestr_utc",
46     "urlsafe",
47     "texsafe",
48     "percentage",
49     "affiliate_id",
50     "bookreader_host",
51     "private_collections",
52     "private_collection_in",
53     # functions imported from elsewhere
54     "parse_datetime",
55     "safeint",
56 ]
57 __docformat__ = "restructuredtext en"
58 
59 
60 def sanitize(html, encoding='utf8'):
61     """Removes unsafe tags and attributes from html and adds
62     ``rel="nofollow"`` attribute to all external links.
63     Using encoding=None if passing Unicode strings.
64     encoding="utf8" matches default format for earlier versions of Genshi
65     https://genshi.readthedocs.io/en/latest/upgrade/#upgrading-from-genshi-0-6-x-to-the-development-version
66     """
67 
68     # Can't sanitize unless genshi module is available
69     if genshi is None:
70         return html
71 
72     def get_nofollow(name, event):
73         attrs = event[1][1]
74 
75         if href := attrs.get('href', ''):
76             # add rel=nofollow to all absolute links
77             _, host, _, _, _ = urlsplit(href)
78             if host:
79                 return 'nofollow'
80 
81     try:
82         html = genshi.HTML(html, encoding=encoding)
83 
84     # except (genshi.ParseError, UnicodeDecodeError, UnicodeError) as e:
85     # don't catch Unicode errors so we can tell if we're getting bytes
86     except genshi.ParseError:
87         if BeautifulSoup:
88             # Bad html. Tidy it up using BeautifulSoup
89             html = str(BeautifulSoup(html, "lxml"))
90             try:
91                 html = genshi.HTML(html)
92             except Exception:
93                 # Failed to sanitize.
94                 # We can't do any better than returning the original HTML, without sanitizing.
95                 return html
96         else:
97             raise
98 
99     stream = (
100         html
101         | genshi.filters.HTMLSanitizer()
102         | genshi.filters.Transformer("//a").attr("rel", get_nofollow)
103     )
104     return stream.render()
105 
106 
107 class NothingEncoder(json.JSONEncoder):
108     def default(self, obj):
109         """Custom JSON encoder for handling Nothing values.
110 
111         Returns None if a value is a Nothing object. Otherwise,
112         encode values using the default JSON encoder.
113         """
114         if isinstance(obj, Nothing):
115             return None
116         if isinstance(obj, date):
117             return obj.isoformat()
118         return super().default(obj)
119 
120 
121 def json_encode(d, **kw):
122     """Calls json.dumps on the given data d.
123     If d is a Nothing object, passes an empty list to json.dumps.
124 
125     Returns the json.dumps results.
126     """
127     return json.dumps([] if isinstance(d, Nothing) else d, **kw)
128 
129 
130 def safesort(iterable, key=None, reverse=False):
131     """Sorts heterogeneous of objects without raising errors.
132 
133     Sorting heterogeneous objects sometimes causes error. For example,
134     datetime and Nones don't go well together. This function takes special
135     care to make that work.
136     """
137     key = key or (lambda x: x)
138 
139     def safekey(x):
140         k = key(x)
141         return (k.__class__.__name__, k)
142 
143     return sorted(iterable, key=safekey, reverse=reverse)
144 
145 
146 def days_since(then, now=None):
147     delta = then - (now or datetime.now())
148     return abs(delta.days)
149 
150 
151 def datestr(then, now=None, lang=None, relative=True):
152     """Internationalized version of web.datestr."""
153     lang = lang or web.ctx.lang
154     if relative:
155         if now is None:
156             now = datetime.now()
157         delta = then - now
158         if abs(delta.days) < 4:  # Threshold from web.py
159             return babel.dates.format_timedelta(
160                 delta, add_direction=True, locale=_get_babel_locale(lang)
161             )
162     return format_date(then, lang=lang)
163 
164 
165 def datetimestr_utc(then):
166     return then.strftime("%Y-%m-%dT%H:%M:%SZ")
167 
168 
169 def format_date(date, lang=None):
170     lang = lang or web.ctx.lang
171     locale = _get_babel_locale(lang)
172     return babel.dates.format_date(date, format="long", locale=locale)
173 
174 
175 def _get_babel_locale(lang):
176     try:
177         return babel.Locale(lang)
178     except babel.core.UnknownLocaleError:
179         return babel.Locale("en")
180 
181 
182 def sprintf(s, *a, **kw):
183     """Handy utility for string replacements.
184 
185     >>> sprintf('hello %s', 'python')
186     'hello python'
187     >>> sprintf('hello %(name)s', name='python')
188     'hello python'
189     """
190     if args := kw or a:
191         return s % args
192     else:
193         return s
194 
195 
196 def cond(pred, true_value, false_value=""):
197     """Lisp style cond function.
198 
199     Hanly to use instead of if-else expression.
200     """
201     return true_value if pred else false_value
202 
203 
204 def commify(number, lang=None):
205     """localized version of web.commify"""
206     try:
207         lang = lang or web.ctx.get("lang") or "en"
208         return babel.numbers.format_decimal(int(number), locale=lang)
209     except Exception:
210         return str(number)
211 
212 
213 def truncate(text, limit):
214     """Truncate text and add ellipses if it longer than specified limit."""
215     if not text:
216         return ''
217     if len(text) <= limit:
218         return text
219     return text[:limit] + "..."
220 
221 
222 def urlsafe(path: str) -> str:
223     """Replaces the unsafe chars from path with underscores."""
224     return _get_safepath_re().sub('_', path).strip('_')[:100]
225 
226 
227 @web.memoize
228 def _get_safepath_re():
229     """Make regular expression that matches all unsafe chars."""
230     # unsafe chars according to RFC 2396
231     reserved = ";/?:@&=+$,"
232     delims = '<>#%"'
233     unwise = "{}|\\^[]`"
234     space = ' \n\r'
235 
236     unsafe = reserved + delims + unwise + space
237     pattern = '[%s]+' % "".join(re.escape(c) for c in unsafe)
238     return re.compile(pattern)
239 
240 
241 _texsafe_map = {
242     '"': r'\textquotedbl{}',
243     '#': r'\#',
244     '$': r'\$',
245     '%': r'\%',
246     '&': r'\&',
247     '<': r'\textless{}',
248     '>': r'\textgreater{}',
249     '\\': r'\textbackslash{}',
250     '^': r'\^{}',
251     '_': r'\_{}',
252     '{': r'\{',
253     '}': r'\}',
254     '|': r'\textbar{}',
255     '~': r'\~{}',
256 }
257 
258 _texsafe_re = None
259 
260 
261 def texsafe(text):
262     """Escapes the special characters in the given text for using it in tex type setting.
263 
264     Tex (or Latex) uses some characters in the ascii character range for
265     special notations. These characters must be escaped when occur in the
266     regular text. This function escapes those special characters.
267 
268     The list of special characters and the latex command to typeset them can
269     be found in `The Comprehensive LaTeX Symbol List`_.
270 
271     .. _The Comprehensive LaTeX Symbol List: http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf
272     """
273     global _texsafe_re
274     if _texsafe_re is None:
275         pattern = "[%s]" % re.escape("".join(list(_texsafe_map)))
276         _texsafe_re = re.compile(pattern)
277 
278     return _texsafe_re.sub(lambda m: _texsafe_map[m.group(0)], text)
279 
280 
281 def percentage(value, total):
282     """Computes percentage.
283 
284     >>> percentage(1, 10)
285     10.0
286     >>> percentage(0, 0)
287     0.0
288     """
289     return (value * 100.0) / total if total else 0.0
290 
291 
292 def uniq(values, key=None):
293     """Returns the unique entries from the given values in the original order.
294 
295     The value of the optional `key` parameter should be a function that takes
296     a single argument and returns a key to test the uniqueness.
297     """
298     key = key or (lambda x: x)
299     s = set()
300     result = []
301     for v in values:
302         k = key(v)
303         if k not in s:
304             s.add(k)
305             result.append(v)
306     return result
307 
308 
309 def affiliate_id(affiliate):
310     return config.get('affiliate_ids', {}).get(affiliate, '')
311 
312 
313 def bookreader_host():
314     return config.get('bookreader_host', '')
315 
316 
317 def private_collections():
318     """Collections which are lendable but should not be linked from OL
319     TODO: Remove when we can handle institutional books"""
320     return ['georgetown-university-law-library-rr']
321 
322 
323 def private_collection_in(collections):
324     return any(x in private_collections() for x in collections)
325 
326 
327 def _get_helpers():
328     _globals = globals()
329     return web.storage((k, _globals[k]) for k in __all__)
330 
331 
332 # This must be at the end of this module
333 helpers = _get_helpers()
334
```
--- 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()

```

diff --git a/openlibrary/core/helpers.py b/openlibrary/core/helpers.py
--- a/openlibrary/core/helpers.py
+++ b/openlibrary/core/helpers.py
@@ -50,6 +50,7 @@
     "bookreader_host",
     "private_collections",
     "private_collection_in",
+    "extract_year",
     # functions imported from elsewhere
     "parse_datetime",
     "safeint",
@@ -324,6 +325,14 @@
     return any(x in private_collections() for x in collections)
 
 
+def extract_year(input):
+    """Extracts the year from an author's birth or death date."""
+    if result := re.search(r'\d{4}', input):
+        return result.group()
+    else:
+        return ''
+
+
 def _get_helpers():
     _globals = globals()
     return web.storage((k, _globals[k]) for k in __all__)

{"golden_diff": "diff --git a/openlibrary/core/helpers.py b/openlibrary/core/helpers.py\n--- a/openlibrary/core/helpers.py\n+++ b/openlibrary/core/helpers.py\n@@ -50,6 +50,7 @@\n     \"bookreader_host\",\n     \"private_collections\",\n     \"private_collection_in\",\n+    \"extract_year\",\n     # functions imported from elsewhere\n     \"parse_datetime\",\n     \"safeint\",\n@@ -324,6 +325,14 @@\n     return any(x in private_collections() for x in collections)\n \n \n+def extract_year(input):\n+    \"\"\"Extracts the year from an author's birth or death date.\"\"\"\n+    if result := re.search(r'\\d{4}', input):\n+        return result.group()\n+    else:\n+        return ''\n+\n+\n def _get_helpers():\n     _globals = globals()\n     return web.storage((k, _globals[k]) for k in __all__)\n", "issue": "Display author date(s) with name for librarians\n<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->\r\nAuthor disambiguation is difficult and even more difficult when the author names all look the same for different identities. To see precisely which author is associated with a work, one must either click the author to view the profile or go into edit mode and use the author selection box.\r\n\r\n### Describe the problem that you'd like solved\r\n<!-- A clear and concise description of what you want to happen. -->\r\nAuthors should display their associated years to make this task less cumbersome. \r\n\r\n### Proposal & Constraints\r\n<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->\r\n\r\n<!-- Which suggestions or requirements should be considered for how feature needs to appear or be implemented? -->\r\nOnly the year is necessary, any other date information does not need to be displayed.\r\nThis should be a librarian-feature only.\r\n\r\n### Additional context\r\n<!-- Add any other context or screenshots about the feature request here. -->\r\n\r\nFor example, these are two different Stephen Kings:\r\n<img width=\"454\" alt=\"Screenshot 2023-04-19 at 09 02 16\" src=\"https://user-images.githubusercontent.com/17739465/232992930-9b99cdaf-2e84-491f-bea3-ec4457b18a32.png\">\r\n<img width=\"484\" alt=\"Screenshot 2023-04-19 at 09 02 38\" src=\"https://user-images.githubusercontent.com/17739465/232993031-8c846762-0dee-4d60-9c83-b10100d4be65.png\">\r\n\r\nA display like this would make disambiguation much easier and less time-consuming:\r\n<img width=\"314\" alt=\"Screenshot 2023-04-19 at 09 04 35\" src=\"https://user-images.githubusercontent.com/17739465/232993643-55f2d88f-ff8f-487b-8931-aaa447f54acf.png\">\r\n<img width=\"466\" alt=\"Screenshot 2023-04-19 at 09 04 47\" src=\"https://user-images.githubusercontent.com/17739465/232993680-717724ab-907f-473c-9c4a-048575328ed1.png\">\r\n\r\n### Stakeholders\r\n<!-- @ tag stakeholders of this bug -->\r\n\r\n\r\n\n", "before_files": [{"content": "\"\"\"Generic helper functions to use in the templates and the webapp.\n\"\"\"\n\nimport json\nimport re\nfrom datetime import datetime, date\nfrom urllib.parse import urlsplit\n\nimport web\n\nimport babel\nimport babel.core\nimport babel.dates\nimport babel.numbers\n\ntry:\n    import genshi\n    import genshi.filters\nexcept ImportError:\n    genshi = None\n\ntry:\n    from bs4 import BeautifulSoup\nexcept ImportError:\n    BeautifulSoup = None\n\nfrom infogami import config\nfrom infogami.infobase.client import Nothing\n\n# handy utility to parse ISO date strings\nfrom infogami.infobase.utils import parse_datetime\nfrom infogami.utils.view import safeint\n\n__all__ = [\n    \"sanitize\",\n    \"json_encode\",\n    \"safesort\",\n    \"days_since\",\n    \"datestr\",\n    \"format_date\",\n    \"sprintf\",\n    \"cond\",\n    \"commify\",\n    \"truncate\",\n    \"datetimestr_utc\",\n    \"urlsafe\",\n    \"texsafe\",\n    \"percentage\",\n    \"affiliate_id\",\n    \"bookreader_host\",\n    \"private_collections\",\n    \"private_collection_in\",\n    # functions imported from elsewhere\n    \"parse_datetime\",\n    \"safeint\",\n]\n__docformat__ = \"restructuredtext en\"\n\n\ndef sanitize(html, encoding='utf8'):\n    \"\"\"Removes unsafe tags and attributes from html and adds\n    ``rel=\"nofollow\"`` attribute to all external links.\n    Using encoding=None if passing Unicode strings.\n    encoding=\"utf8\" matches default format for earlier versions of Genshi\n    https://genshi.readthedocs.io/en/latest/upgrade/#upgrading-from-genshi-0-6-x-to-the-development-version\n    \"\"\"\n\n    # Can't sanitize unless genshi module is available\n    if genshi is None:\n        return html\n\n    def get_nofollow(name, event):\n        attrs = event[1][1]\n\n        if href := attrs.get('href', ''):\n            # add rel=nofollow to all absolute links\n            _, host, _, _, _ = urlsplit(href)\n            if host:\n                return 'nofollow'\n\n    try:\n        html = genshi.HTML(html, encoding=encoding)\n\n    # except (genshi.ParseError, UnicodeDecodeError, UnicodeError) as e:\n    # don't catch Unicode errors so we can tell if we're getting bytes\n    except genshi.ParseError:\n        if BeautifulSoup:\n            # Bad html. Tidy it up using BeautifulSoup\n            html = str(BeautifulSoup(html, \"lxml\"))\n            try:\n                html = genshi.HTML(html)\n            except Exception:\n                # Failed to sanitize.\n                # We can't do any better than returning the original HTML, without sanitizing.\n                return html\n        else:\n            raise\n\n    stream = (\n        html\n        | genshi.filters.HTMLSanitizer()\n        | genshi.filters.Transformer(\"//a\").attr(\"rel\", get_nofollow)\n    )\n    return stream.render()\n\n\nclass NothingEncoder(json.JSONEncoder):\n    def default(self, obj):\n        \"\"\"Custom JSON encoder for handling Nothing values.\n\n        Returns None if a value is a Nothing object. Otherwise,\n        encode values using the default JSON encoder.\n        \"\"\"\n        if isinstance(obj, Nothing):\n            return None\n        if isinstance(obj, date):\n            return obj.isoformat()\n        return super().default(obj)\n\n\ndef json_encode(d, **kw):\n    \"\"\"Calls json.dumps on the given data d.\n    If d is a Nothing object, passes an empty list to json.dumps.\n\n    Returns the json.dumps results.\n    \"\"\"\n    return json.dumps([] if isinstance(d, Nothing) else d, **kw)\n\n\ndef safesort(iterable, key=None, reverse=False):\n    \"\"\"Sorts heterogeneous of objects without raising errors.\n\n    Sorting heterogeneous objects sometimes causes error. For example,\n    datetime and Nones don't go well together. This function takes special\n    care to make that work.\n    \"\"\"\n    key = key or (lambda x: x)\n\n    def safekey(x):\n        k = key(x)\n        return (k.__class__.__name__, k)\n\n    return sorted(iterable, key=safekey, reverse=reverse)\n\n\ndef days_since(then, now=None):\n    delta = then - (now or datetime.now())\n    return abs(delta.days)\n\n\ndef datestr(then, now=None, lang=None, relative=True):\n    \"\"\"Internationalized version of web.datestr.\"\"\"\n    lang = lang or web.ctx.lang\n    if relative:\n        if now is None:\n            now = datetime.now()\n        delta = then - now\n        if abs(delta.days) < 4:  # Threshold from web.py\n            return babel.dates.format_timedelta(\n                delta, add_direction=True, locale=_get_babel_locale(lang)\n            )\n    return format_date(then, lang=lang)\n\n\ndef datetimestr_utc(then):\n    return then.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n\n\ndef format_date(date, lang=None):\n    lang = lang or web.ctx.lang\n    locale = _get_babel_locale(lang)\n    return babel.dates.format_date(date, format=\"long\", locale=locale)\n\n\ndef _get_babel_locale(lang):\n    try:\n        return babel.Locale(lang)\n    except babel.core.UnknownLocaleError:\n        return babel.Locale(\"en\")\n\n\ndef sprintf(s, *a, **kw):\n    \"\"\"Handy utility for string replacements.\n\n    >>> sprintf('hello %s', 'python')\n    'hello python'\n    >>> sprintf('hello %(name)s', name='python')\n    'hello python'\n    \"\"\"\n    if args := kw or a:\n        return s % args\n    else:\n        return s\n\n\ndef cond(pred, true_value, false_value=\"\"):\n    \"\"\"Lisp style cond function.\n\n    Hanly to use instead of if-else expression.\n    \"\"\"\n    return true_value if pred else false_value\n\n\ndef commify(number, lang=None):\n    \"\"\"localized version of web.commify\"\"\"\n    try:\n        lang = lang or web.ctx.get(\"lang\") or \"en\"\n        return babel.numbers.format_decimal(int(number), locale=lang)\n    except Exception:\n        return str(number)\n\n\ndef truncate(text, limit):\n    \"\"\"Truncate text and add ellipses if it longer than specified limit.\"\"\"\n    if not text:\n        return ''\n    if len(text) <= limit:\n        return text\n    return text[:limit] + \"...\"\n\n\ndef urlsafe(path: str) -> str:\n    \"\"\"Replaces the unsafe chars from path with underscores.\"\"\"\n    return _get_safepath_re().sub('_', path).strip('_')[:100]\n\n\n@web.memoize\ndef _get_safepath_re():\n    \"\"\"Make regular expression that matches all unsafe chars.\"\"\"\n    # unsafe chars according to RFC 2396\n    reserved = \";/?:@&=+$,\"\n    delims = '<>#%\"'\n    unwise = \"{}|\\\\^[]`\"\n    space = ' \\n\\r'\n\n    unsafe = reserved + delims + unwise + space\n    pattern = '[%s]+' % \"\".join(re.escape(c) for c in unsafe)\n    return re.compile(pattern)\n\n\n_texsafe_map = {\n    '\"': r'\\textquotedbl{}',\n    '#': r'\\#',\n    '$': r'\\$',\n    '%': r'\\%',\n    '&': r'\\&',\n    '<': r'\\textless{}',\n    '>': r'\\textgreater{}',\n    '\\\\': r'\\textbackslash{}',\n    '^': r'\\^{}',\n    '_': r'\\_{}',\n    '{': r'\\{',\n    '}': r'\\}',\n    '|': r'\\textbar{}',\n    '~': r'\\~{}',\n}\n\n_texsafe_re = None\n\n\ndef texsafe(text):\n    \"\"\"Escapes the special characters in the given text for using it in tex type setting.\n\n    Tex (or Latex) uses some characters in the ascii character range for\n    special notations. These characters must be escaped when occur in the\n    regular text. This function escapes those special characters.\n\n    The list of special characters and the latex command to typeset them can\n    be found in `The Comprehensive LaTeX Symbol List`_.\n\n    .. _The Comprehensive LaTeX Symbol List: http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf\n    \"\"\"\n    global _texsafe_re\n    if _texsafe_re is None:\n        pattern = \"[%s]\" % re.escape(\"\".join(list(_texsafe_map)))\n        _texsafe_re = re.compile(pattern)\n\n    return _texsafe_re.sub(lambda m: _texsafe_map[m.group(0)], text)\n\n\ndef percentage(value, total):\n    \"\"\"Computes percentage.\n\n    >>> percentage(1, 10)\n    10.0\n    >>> percentage(0, 0)\n    0.0\n    \"\"\"\n    return (value * 100.0) / total if total else 0.0\n\n\ndef uniq(values, key=None):\n    \"\"\"Returns the unique entries from the given values in the original order.\n\n    The value of the optional `key` parameter should be a function that takes\n    a single argument and returns a key to test the uniqueness.\n    \"\"\"\n    key = key or (lambda x: x)\n    s = set()\n    result = []\n    for v in values:\n        k = key(v)\n        if k not in s:\n            s.add(k)\n            result.append(v)\n    return result\n\n\ndef affiliate_id(affiliate):\n    return config.get('affiliate_ids', {}).get(affiliate, '')\n\n\ndef bookreader_host():\n    return config.get('bookreader_host', '')\n\n\ndef private_collections():\n    \"\"\"Collections which are lendable but should not be linked from OL\n    TODO: Remove when we can handle institutional books\"\"\"\n    return ['georgetown-university-law-library-rr']\n\n\ndef private_collection_in(collections):\n    return any(x in private_collections() for x in collections)\n\n\ndef _get_helpers():\n    _globals = globals()\n    return web.storage((k, _globals[k]) for k in __all__)\n\n\n# This must be at the end of this module\nhelpers = _get_helpers()\n", "path": "openlibrary/core/helpers.py"}], "after_files": [{"content": "\"\"\"Generic helper functions to use in the templates and the webapp.\n\"\"\"\n\nimport json\nimport re\nfrom datetime import datetime, date\nfrom urllib.parse import urlsplit\n\nimport web\n\nimport babel\nimport babel.core\nimport babel.dates\nimport babel.numbers\n\ntry:\n    import genshi\n    import genshi.filters\nexcept ImportError:\n    genshi = None\n\ntry:\n    from bs4 import BeautifulSoup\nexcept ImportError:\n    BeautifulSoup = None\n\nfrom infogami import config\nfrom infogami.infobase.client import Nothing\n\n# handy utility to parse ISO date strings\nfrom infogami.infobase.utils import parse_datetime\nfrom infogami.utils.view import safeint\n\n__all__ = [\n    \"sanitize\",\n    \"json_encode\",\n    \"safesort\",\n    \"days_since\",\n    \"datestr\",\n    \"format_date\",\n    \"sprintf\",\n    \"cond\",\n    \"commify\",\n    \"truncate\",\n    \"datetimestr_utc\",\n    \"urlsafe\",\n    \"texsafe\",\n    \"percentage\",\n    \"affiliate_id\",\n    \"bookreader_host\",\n    \"private_collections\",\n    \"private_collection_in\",\n    \"extract_year\",\n    # functions imported from elsewhere\n    \"parse_datetime\",\n    \"safeint\",\n]\n__docformat__ = \"restructuredtext en\"\n\n\ndef sanitize(html, encoding='utf8'):\n    \"\"\"Removes unsafe tags and attributes from html and adds\n    ``rel=\"nofollow\"`` attribute to all external links.\n    Using encoding=None if passing Unicode strings.\n    encoding=\"utf8\" matches default format for earlier versions of Genshi\n    https://genshi.readthedocs.io/en/latest/upgrade/#upgrading-from-genshi-0-6-x-to-the-development-version\n    \"\"\"\n\n    # Can't sanitize unless genshi module is available\n    if genshi is None:\n        return html\n\n    def get_nofollow(name, event):\n        attrs = event[1][1]\n\n        if href := attrs.get('href', ''):\n            # add rel=nofollow to all absolute links\n            _, host, _, _, _ = urlsplit(href)\n            if host:\n                return 'nofollow'\n\n    try:\n        html = genshi.HTML(html, encoding=encoding)\n\n    # except (genshi.ParseError, UnicodeDecodeError, UnicodeError) as e:\n    # don't catch Unicode errors so we can tell if we're getting bytes\n    except genshi.ParseError:\n        if BeautifulSoup:\n            # Bad html. Tidy it up using BeautifulSoup\n            html = str(BeautifulSoup(html, \"lxml\"))\n            try:\n                html = genshi.HTML(html)\n            except Exception:\n                # Failed to sanitize.\n                # We can't do any better than returning the original HTML, without sanitizing.\n                return html\n        else:\n            raise\n\n    stream = (\n        html\n        | genshi.filters.HTMLSanitizer()\n        | genshi.filters.Transformer(\"//a\").attr(\"rel\", get_nofollow)\n    )\n    return stream.render()\n\n\nclass NothingEncoder(json.JSONEncoder):\n    def default(self, obj):\n        \"\"\"Custom JSON encoder for handling Nothing values.\n\n        Returns None if a value is a Nothing object. Otherwise,\n        encode values using the default JSON encoder.\n        \"\"\"\n        if isinstance(obj, Nothing):\n            return None\n        if isinstance(obj, date):\n            return obj.isoformat()\n        return super().default(obj)\n\n\ndef json_encode(d, **kw):\n    \"\"\"Calls json.dumps on the given data d.\n    If d is a Nothing object, passes an empty list to json.dumps.\n\n    Returns the json.dumps results.\n    \"\"\"\n    return json.dumps([] if isinstance(d, Nothing) else d, **kw)\n\n\ndef safesort(iterable, key=None, reverse=False):\n    \"\"\"Sorts heterogeneous of objects without raising errors.\n\n    Sorting heterogeneous objects sometimes causes error. For example,\n    datetime and Nones don't go well together. This function takes special\n    care to make that work.\n    \"\"\"\n    key = key or (lambda x: x)\n\n    def safekey(x):\n        k = key(x)\n        return (k.__class__.__name__, k)\n\n    return sorted(iterable, key=safekey, reverse=reverse)\n\n\ndef days_since(then, now=None):\n    delta = then - (now or datetime.now())\n    return abs(delta.days)\n\n\ndef datestr(then, now=None, lang=None, relative=True):\n    \"\"\"Internationalized version of web.datestr.\"\"\"\n    lang = lang or web.ctx.lang\n    if relative:\n        if now is None:\n            now = datetime.now()\n        delta = then - now\n        if abs(delta.days) < 4:  # Threshold from web.py\n            return babel.dates.format_timedelta(\n                delta, add_direction=True, locale=_get_babel_locale(lang)\n            )\n    return format_date(then, lang=lang)\n\n\ndef datetimestr_utc(then):\n    return then.strftime(\"%Y-%m-%dT%H:%M:%SZ\")\n\n\ndef format_date(date, lang=None):\n    lang = lang or web.ctx.lang\n    locale = _get_babel_locale(lang)\n    return babel.dates.format_date(date, format=\"long\", locale=locale)\n\n\ndef _get_babel_locale(lang):\n    try:\n        return babel.Locale(lang)\n    except babel.core.UnknownLocaleError:\n        return babel.Locale(\"en\")\n\n\ndef sprintf(s, *a, **kw):\n    \"\"\"Handy utility for string replacements.\n\n    >>> sprintf('hello %s', 'python')\n    'hello python'\n    >>> sprintf('hello %(name)s', name='python')\n    'hello python'\n    \"\"\"\n    if args := kw or a:\n        return s % args\n    else:\n        return s\n\n\ndef cond(pred, true_value, false_value=\"\"):\n    \"\"\"Lisp style cond function.\n\n    Hanly to use instead of if-else expression.\n    \"\"\"\n    return true_value if pred else false_value\n\n\ndef commify(number, lang=None):\n    \"\"\"localized version of web.commify\"\"\"\n    try:\n        lang = lang or web.ctx.get(\"lang\") or \"en\"\n        return babel.numbers.format_decimal(int(number), locale=lang)\n    except Exception:\n        return str(number)\n\n\ndef truncate(text, limit):\n    \"\"\"Truncate text and add ellipses if it longer than specified limit.\"\"\"\n    if not text:\n        return ''\n    if len(text) <= limit:\n        return text\n    return text[:limit] + \"...\"\n\n\ndef urlsafe(path: str) -> str:\n    \"\"\"Replaces the unsafe chars from path with underscores.\"\"\"\n    return _get_safepath_re().sub('_', path).strip('_')[:100]\n\n\n@web.memoize\ndef _get_safepath_re():\n    \"\"\"Make regular expression that matches all unsafe chars.\"\"\"\n    # unsafe chars according to RFC 2396\n    reserved = \";/?:@&=+$,\"\n    delims = '<>#%\"'\n    unwise = \"{}|\\\\^[]`\"\n    space = ' \\n\\r'\n\n    unsafe = reserved + delims + unwise + space\n    pattern = '[%s]+' % \"\".join(re.escape(c) for c in unsafe)\n    return re.compile(pattern)\n\n\n_texsafe_map = {\n    '\"': r'\\textquotedbl{}',\n    '#': r'\\#',\n    '$': r'\\$',\n    '%': r'\\%',\n    '&': r'\\&',\n    '<': r'\\textless{}',\n    '>': r'\\textgreater{}',\n    '\\\\': r'\\textbackslash{}',\n    '^': r'\\^{}',\n    '_': r'\\_{}',\n    '{': r'\\{',\n    '}': r'\\}',\n    '|': r'\\textbar{}',\n    '~': r'\\~{}',\n}\n\n_texsafe_re = None\n\n\ndef texsafe(text):\n    \"\"\"Escapes the special characters in the given text for using it in tex type setting.\n\n    Tex (or Latex) uses some characters in the ascii character range for\n    special notations. These characters must be escaped when occur in the\n    regular text. This function escapes those special characters.\n\n    The list of special characters and the latex command to typeset them can\n    be found in `The Comprehensive LaTeX Symbol List`_.\n\n    .. _The Comprehensive LaTeX Symbol List: http://www.ctan.org/tex-archive/info/symbols/comprehensive/symbols-a4.pdf\n    \"\"\"\n    global _texsafe_re\n    if _texsafe_re is None:\n        pattern = \"[%s]\" % re.escape(\"\".join(list(_texsafe_map)))\n        _texsafe_re = re.compile(pattern)\n\n    return _texsafe_re.sub(lambda m: _texsafe_map[m.group(0)], text)\n\n\ndef percentage(value, total):\n    \"\"\"Computes percentage.\n\n    >>> percentage(1, 10)\n    10.0\n    >>> percentage(0, 0)\n    0.0\n    \"\"\"\n    return (value * 100.0) / total if total else 0.0\n\n\ndef uniq(values, key=None):\n    \"\"\"Returns the unique entries from the given values in the original order.\n\n    The value of the optional `key` parameter should be a function that takes\n    a single argument and returns a key to test the uniqueness.\n    \"\"\"\n    key = key or (lambda x: x)\n    s = set()\n    result = []\n    for v in values:\n        k = key(v)\n        if k not in s:\n            s.add(k)\n            result.append(v)\n    return result\n\n\ndef affiliate_id(affiliate):\n    return config.get('affiliate_ids', {}).get(affiliate, '')\n\n\ndef bookreader_host():\n    return config.get('bookreader_host', '')\n\n\ndef private_collections():\n    \"\"\"Collections which are lendable but should not be linked from OL\n    TODO: Remove when we can handle institutional books\"\"\"\n    return ['georgetown-university-law-library-rr']\n\n\ndef private_collection_in(collections):\n    return any(x in private_collections() for x in collections)\n\n\ndef extract_year(input):\n    \"\"\"Extracts the year from an author's birth or death date.\"\"\"\n    if result := re.search(r'\\d{4}', input):\n        return result.group()\n    else:\n        return ''\n\n\ndef _get_helpers():\n    _globals = globals()\n    return web.storage((k, _globals[k]) for k in __all__)\n\n\n# This must be at the end of this module\nhelpers = _get_helpers()\n", "path": "openlibrary/core/helpers.py"}]}