We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
"`Nothing` type not JSON serializable" errors occurring when fetching list data from `/seeds.json`
<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->
When a list containing Infogami's `Nothing` type is fetched using `/seeds.json`, a TypeError is thrown because `Nothing` is not JSON serializable.

### Evidence / Screenshot (if possible)

### Relevant url?
<!-- `https://openlibrary.org/...` -->
https://openlibrary.org/people/tmanarl/lists/OL197952L/seeds.json?debug=true

### Steps to Reproduce
<!-- What steps caused you to find the bug? -->
1. Navigate to the link listed above.

<!-- What actually happened after these steps? What did you expect to happen? -->
* Actual: An error page is displayed.
* Expected: A JSON-encoded list should be displayed

### Details

- **Logged in (Y/N)?** N/A
- **Browser type/version?** N/A
- **Operating system?** N/A
- **Environment (prod/dev/local)?** prod
<!-- If not sure, put prod -->

### Proposal & Constraints
<!-- What is the proposed solution / implementation? Is there a precedent of this approach succeeding elsewhere? -->
`helpers.py` contains a `json_encode` function that serializes `Nothing` types.  This can be used instead of the vanilla JSON encoder.

### Related files
<!-- Files related to this issue; this is super useful for new contributors who might want to help! If you're not sure, leave this blank; a maintainer will add them. -->

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


--- 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/formats.py`
Content:
```
1 """Library for loading and dumping data to json and yaml.
2 """
3 import json
4 import yaml
5 
6 __all__ = ["load_yaml", "dump_yaml"]
7 
8 
9 def load_yaml(text):
10     return yaml.safe_load(text)
11 
12 
13 def dump_yaml(data):
14     return yaml.safe_dump(data, indent=4, allow_unicode=True, default_flow_style=False)
15 
16 
17 def load(text, format):
18     if format == "json":
19         return json.loads(text)
20     elif format == "yaml":
21         return load_yaml(text)
22     else:
23         raise Exception("unsupported format %r" % format)
24 
25 
26 def dump(data, format):
27     if format == "json":
28         return json.dumps(data)
29     elif format == "yml":
30         return dump_yaml(data)
31     else:
32         raise Exception("unsupported format %r" % format)
33
```

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

{"golden_diff": "diff --git a/openlibrary/core/formats.py b/openlibrary/core/formats.py\n--- a/openlibrary/core/formats.py\n+++ b/openlibrary/core/formats.py\n@@ -3,6 +3,8 @@\n import json\n import yaml\n \n+from openlibrary.core.helpers import NothingEncoder\n+\n __all__ = [\"load_yaml\", \"dump_yaml\"]\n \n \n@@ -25,7 +27,7 @@\n \n def dump(data, format):\n     if format == \"json\":\n-        return json.dumps(data)\n+        return json.dumps(data, cls=NothingEncoder)\n     elif format == \"yml\":\n         return dump_yaml(data)\n     else:\ndiff --git a/openlibrary/core/helpers.py b/openlibrary/core/helpers.py\n--- a/openlibrary/core/helpers.py\n+++ b/openlibrary/core/helpers.py\n@@ -107,8 +107,24 @@\n     return stream.render()\n \n \n+class 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+        return super().default(obj)\n+\n+\n def json_encode(d, **kw):\n-    \"\"\"Same as json.dumps.\"\"\"\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", "issue": "\"`Nothing` type not JSON serializable\" errors occurring when fetching list data from `/seeds.json`\n<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->\r\nWhen a list containing Infogami's `Nothing` type is fetched using `/seeds.json`, a TypeError is thrown because `Nothing` is not JSON serializable.\r\n\r\n### Evidence / Screenshot (if possible)\r\n\r\n### Relevant url?\r\n<!-- `https://openlibrary.org/...` -->\r\nhttps://openlibrary.org/people/tmanarl/lists/OL197952L/seeds.json?debug=true\r\n\r\n### Steps to Reproduce\r\n<!-- What steps caused you to find the bug? -->\r\n1. Navigate to the link listed above.\r\n\r\n<!-- What actually happened after these steps? What did you expect to happen? -->\r\n* Actual: An error page is displayed.\r\n* Expected: A JSON-encoded list should be displayed\r\n\r\n### Details\r\n\r\n- **Logged in (Y/N)?** N/A\r\n- **Browser type/version?** N/A\r\n- **Operating system?** N/A\r\n- **Environment (prod/dev/local)?** prod\r\n<!-- If not sure, put prod -->\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`helpers.py` contains a `json_encode` function that serializes `Nothing` types.  This can be used instead of the vanilla JSON encoder.\r\n\r\n### Related files\r\n<!-- Files related to this issue; this is super useful for new contributors who might want to help! If you're not sure, leave this blank; a maintainer will add them. -->\r\n\r\n### Stakeholders\r\n<!-- @ tag stakeholders of this bug -->\r\n@libjenner\r\n\n", "before_files": [{"content": "\"\"\"Library for loading and dumping data to json and yaml.\n\"\"\"\nimport json\nimport yaml\n\n__all__ = [\"load_yaml\", \"dump_yaml\"]\n\n\ndef load_yaml(text):\n    return yaml.safe_load(text)\n\n\ndef dump_yaml(data):\n    return yaml.safe_dump(data, indent=4, allow_unicode=True, default_flow_style=False)\n\n\ndef load(text, format):\n    if format == \"json\":\n        return json.loads(text)\n    elif format == \"yaml\":\n        return load_yaml(text)\n    else:\n        raise Exception(\"unsupported format %r\" % format)\n\n\ndef dump(data, format):\n    if format == \"json\":\n        return json.dumps(data)\n    elif format == \"yml\":\n        return dump_yaml(data)\n    else:\n        raise Exception(\"unsupported format %r\" % format)\n", "path": "openlibrary/core/formats.py"}, {"content": "\"\"\"Generic helper functions to use in the templates and the webapp.\n\"\"\"\nimport json\nimport re\nfrom datetime import datetime\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# TODO: i18n should be moved to core or infogami\nfrom openlibrary.i18n import gettext as _  # noqa: F401\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        href = attrs.get('href', '')\n\n        if 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\ndef json_encode(d, **kw):\n    \"\"\"Same as json.dumps.\"\"\"\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    args = kw or a\n    if args:\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    if pred:\n        return true_value\n    else:\n        return 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_number(int(number), lang)\n    except:\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):\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": "\"\"\"Library for loading and dumping data to json and yaml.\n\"\"\"\nimport json\nimport yaml\n\nfrom openlibrary.core.helpers import NothingEncoder\n\n__all__ = [\"load_yaml\", \"dump_yaml\"]\n\n\ndef load_yaml(text):\n    return yaml.safe_load(text)\n\n\ndef dump_yaml(data):\n    return yaml.safe_dump(data, indent=4, allow_unicode=True, default_flow_style=False)\n\n\ndef load(text, format):\n    if format == \"json\":\n        return json.loads(text)\n    elif format == \"yaml\":\n        return load_yaml(text)\n    else:\n        raise Exception(\"unsupported format %r\" % format)\n\n\ndef dump(data, format):\n    if format == \"json\":\n        return json.dumps(data, cls=NothingEncoder)\n    elif format == \"yml\":\n        return dump_yaml(data)\n    else:\n        raise Exception(\"unsupported format %r\" % format)\n", "path": "openlibrary/core/formats.py"}, {"content": "\"\"\"Generic helper functions to use in the templates and the webapp.\n\"\"\"\nimport json\nimport re\nfrom datetime import datetime\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# TODO: i18n should be moved to core or infogami\nfrom openlibrary.i18n import gettext as _  # noqa: F401\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        href = attrs.get('href', '')\n\n        if 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        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    args = kw or a\n    if args:\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    if pred:\n        return true_value\n    else:\n        return 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_number(int(number), lang)\n    except:\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):\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"}]}