We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
add a failing author name test
<!-- What issue does this PR close? -->
Demonstrates a bug in #9003

<!-- What does this PR achieve? [feature|hotfix|fix|refactor] -->

`Drini` -> `ini`

**Expected:**
`Drini` -> `Drini`

😉 

### Technical
<!-- What should be noted about the implementation? -->

### Testing
<!-- Steps for reviewer to reproduce/verify what this PR does/fixes. -->

### Screenshot
<!-- If this PR touches UI, please post evidence (screenshots) of it behaving correctly. -->

### Stakeholders
<!-- @ tag the lead (as labeled on the issue) and other stakeholders -->
@cdrini 
@scottbarnes 

<!-- Attribution Disclaimer: By proposing this pull request, I affirm to have made a best-effort and exercised my discretion to make sure relevant sections of this code which substantially leverage code suggestions, code generation, or code snippets from sources (e.g. Stack Overflow, GitHub) have been annotated with basic attribution so reviewers & contributors may have confidence and access to the correct context to evaluate and use this code. -->


--- 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/catalog/add_book/load_book.py`
Content:
```
1 from typing import TYPE_CHECKING, Any, Final
2 import web
3 from openlibrary.catalog.utils import flip_name, author_dates_match, key_int
4 from openlibrary.core.helpers import extract_year
5 
6 
7 if TYPE_CHECKING:
8     from openlibrary.plugins.upstream.models import Author
9 
10 
11 # Sort by descending length to remove the _longest_ match.
12 # E.g. remove "señorita" and not "señor", when both match.
13 HONORIFICS: Final = sorted(
14     [
15         'countess',
16         'doctor',
17         'doktor',
18         'dr',
19         'dr.',
20         'frau',
21         'fräulein',
22         'herr',
23         'lady',
24         'lord',
25         'm.',
26         'madame',
27         'mademoiselle',
28         'miss',
29         'mister',
30         'mistress',
31         'mixter',
32         'mlle',
33         'mlle.',
34         'mme',
35         'mme.',
36         'monsieur',
37         'mr',
38         'mr.',
39         'mrs',
40         'mrs.',
41         'ms',
42         'ms.',
43         'mx',
44         'mx.',
45         'professor',
46         'señor',
47         'señora',
48         'señorita',
49         'sir',
50         'sr.',
51         'sra.',
52         'srta.',
53     ],
54     key=lambda x: len(x),
55     reverse=True,
56 )
57 
58 HONORIFC_NAME_EXECPTIONS = frozenset(
59     {
60         "dr. seuss",
61         "dr seuss",
62         "dr oetker",
63         "doctor oetker",
64     }
65 )
66 
67 
68 def east_in_by_statement(rec, author):
69     """
70     Returns False if there is no by_statement in rec.
71     Otherwise returns whether author name uses eastern name order.
72     TODO: elaborate on what this actually means, and how it is used.
73 
74     :param dict rec: import source edition record
75     :param dict author: import source author dict: {"name": "Some One"}
76     :rtype: bool
77     """
78 
79     if 'by_statement' not in rec:
80         return False
81     if 'authors' not in rec:
82         return False
83     name = author['name']
84     flipped = flip_name(name)
85     name = name.replace('.', '')
86     name = name.replace(', ', '')
87     if name == flipped.replace('.', ''):
88         # name was not flipped
89         return False
90     return rec['by_statement'].find(name) != -1
91 
92 
93 def do_flip(author):
94     """
95     Given an author import dict, flip its name in place
96     i.e. Smith, John => John Smith
97 
98     :param dict author:
99     :rtype: None
100     """
101     if 'personal_name' in author and author['personal_name'] != author['name']:
102         # Don't flip names if name is more complex than personal_name (legacy behaviour)
103         return
104     first_comma = author['name'].find(', ')
105     if first_comma == -1:
106         return
107     # e.g: Harper, John Murdoch, 1845-
108     if author['name'].find(',', first_comma + 1) != -1:
109         return
110     if author['name'].find('i.e.') != -1:
111         return
112     if author['name'].find('i. e.') != -1:
113         return
114     name = flip_name(author['name'])
115     author['name'] = name
116     if 'personal_name' in author:
117         author['personal_name'] = name
118 
119 
120 def pick_from_matches(author, match):
121     """
122     Finds the best match for author from a list of OL authors records, match.
123 
124     :param dict author: Author import representation
125     :param list match: List of matching OL author records
126     :rtype: dict
127     :return: A single OL author record from match
128     """
129     maybe = []
130     if 'birth_date' in author and 'death_date' in author:
131         maybe = [m for m in match if 'birth_date' in m and 'death_date' in m]
132     elif 'date' in author:
133         maybe = [m for m in match if 'date' in m]
134     if not maybe:
135         maybe = match
136     if len(maybe) == 1:
137         return maybe[0]
138     return min(maybe, key=key_int)
139 
140 
141 def find_author(author: dict[str, Any]) -> list["Author"]:
142     """
143     Searches OL for an author by name.
144 
145     :param str name: Author's name
146     :rtype: list
147     :return: A list of OL author representations than match name
148     """
149 
150     def walk_redirects(obj, seen):
151         seen.add(obj['key'])
152         while obj['type']['key'] == '/type/redirect':
153             assert obj['location'] != obj['key']
154             obj = web.ctx.site.get(obj['location'])
155             seen.add(obj['key'])
156         return obj
157 
158     # Try for an 'exact' (case-insensitive) name match, but fall back to alternate_names,
159     # then last name with identical birth and death dates (that are not themselves `None` or '').
160     name = author["name"].replace("*", r"\*")
161     queries = [
162         {"type": "/type/author", "name~": name},
163         {"type": "/type/author", "alternate_names~": name},
164         {
165             "type": "/type/author",
166             "name~": f"* {name.split()[-1]}",
167             "birth_date~": f"*{extract_year(author.get('birth_date', '')) or -1}*",
168             "death_date~": f"*{extract_year(author.get('death_date', '')) or -1}*",
169         },  # Use `-1` to ensure an empty string from extract_year doesn't match empty dates.
170     ]
171     for query in queries:
172         if reply := list(web.ctx.site.things(query)):
173             break
174 
175     authors = [web.ctx.site.get(k) for k in reply]
176     if any(a.type.key != '/type/author' for a in authors):
177         seen: set[dict] = set()
178         authors = [walk_redirects(a, seen) for a in authors if a['key'] not in seen]
179     return authors
180 
181 
182 def find_entity(author: dict[str, Any]) -> "Author | None":
183     """
184     Looks for an existing Author record in OL by name
185     and returns it if found.
186 
187     :param dict author: Author import dict {"name": "Some One"}
188     :return: Existing Author record if found, or None.
189     """
190     name = author['name']
191     things = find_author(author)
192     et = author.get('entity_type')
193     if et and et != 'person':
194         if not things:
195             return None
196         db_entity = things[0]
197         assert db_entity['type']['key'] == '/type/author'
198         return db_entity
199     if ', ' in name:
200         flipped_name = flip_name(author["name"])
201         author_flipped_name = author.copy()
202         things += find_author(author_flipped_name)
203     match = []
204     seen = set()
205     for a in things:
206         key = a['key']
207         if key in seen:
208             continue
209         seen.add(key)
210         orig_key = key
211         assert a.type.key == '/type/author'
212         if 'birth_date' in author and 'birth_date' not in a:
213             continue
214         if 'birth_date' not in author and 'birth_date' in a:
215             continue
216         if not author_dates_match(author, a):
217             continue
218         match.append(a)
219     if not match:
220         return None
221     if len(match) == 1:
222         return match[0]
223     return pick_from_matches(author, match)
224 
225 
226 def remove_author_honorifics(name: str) -> str:
227     """
228     Remove honorifics from an author's name field.
229 
230     If the author's name is only an honorific, it will return the original name.
231     """
232     if name.casefold() in HONORIFC_NAME_EXECPTIONS:
233         return name
234 
235     if honorific := next(
236         (
237             honorific
238             for honorific in HONORIFICS
239             if name.casefold().startswith(honorific)
240         ),
241         None,
242     ):
243         return name[len(honorific) :].lstrip() or name
244 
245     return name
246 
247 
248 def import_author(author: dict[str, Any], eastern=False) -> "Author | dict[str, Any]":
249     """
250     Converts an import style new-author dictionary into an
251     Open Library existing author, or new author candidate, representation.
252     Does NOT create new authors.
253 
254     :param dict author: Author import record {"name": "Some One"}
255     :param bool eastern: Eastern name order
256     :return: Open Library style Author representation, either existing with "key",
257              or new candidate without "key".
258     """
259     if existing := find_entity(author):
260         assert existing.type.key == '/type/author'
261         for k in 'last_modified', 'id', 'revision', 'created':
262             if existing.k:
263                 del existing.k
264         new = existing
265         if 'death_date' in author and 'death_date' not in existing:
266             new['death_date'] = author['death_date']
267         return new
268     if author.get('entity_type') != 'org' and not eastern:
269         do_flip(author)
270     a = {'type': {'key': '/type/author'}}
271     for f in 'name', 'title', 'personal_name', 'birth_date', 'death_date', 'date':
272         if f in author:
273             a[f] = author[f]
274     return a
275 
276 
277 class InvalidLanguage(Exception):
278     def __init__(self, code):
279         self.code = code
280 
281     def __str__(self):
282         return "invalid language code: '%s'" % self.code
283 
284 
285 type_map = {'description': 'text', 'notes': 'text', 'number_of_pages': 'int'}
286 
287 
288 def build_query(rec):
289     """
290     Takes an edition record dict, rec, and returns an Open Library edition
291     suitable for saving.
292 
293     :param dict rec: Edition record to add to Open Library
294     :rtype: dict
295     :return: Open Library style edition representation
296     """
297     book = {
298         'type': {'key': '/type/edition'},
299     }
300 
301     for k, v in rec.items():
302         if k == 'authors':
303             if v and v[0]:
304                 book['authors'] = []
305                 for author in v:
306                     author['name'] = remove_author_honorifics(author['name'])
307                     east = east_in_by_statement(rec, author)
308                     book['authors'].append(import_author(author, eastern=east))
309             continue
310         if k in ('languages', 'translated_from'):
311             for language in v:
312                 if web.ctx.site.get('/languages/' + language) is None:
313                     raise InvalidLanguage(language)
314             book[k] = [{'key': '/languages/' + language} for language in v]
315             continue
316         if k in type_map:
317             t = '/type/' + type_map[k]
318             if isinstance(v, list):
319                 book[k] = [{'type': t, 'value': i} for i in v]
320             else:
321                 book[k] = {'type': t, 'value': v}
322         else:
323             book[k] = v
324     return book
325
```
--- 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/catalog/add_book/load_book.py b/openlibrary/catalog/add_book/load_book.py
--- a/openlibrary/catalog/add_book/load_book.py
+++ b/openlibrary/catalog/add_book/load_book.py
@@ -236,11 +236,11 @@
         (
             honorific
             for honorific in HONORIFICS
-            if name.casefold().startswith(honorific)
+            if name.casefold().startswith(f"{honorific} ")  # Note the trailing space.
         ),
         None,
     ):
-        return name[len(honorific) :].lstrip() or name
+        return name[len(f"{honorific} ") :].lstrip() or name
 
     return name

{"golden_diff": "diff --git a/openlibrary/catalog/add_book/load_book.py b/openlibrary/catalog/add_book/load_book.py\n--- a/openlibrary/catalog/add_book/load_book.py\n+++ b/openlibrary/catalog/add_book/load_book.py\n@@ -236,11 +236,11 @@\n         (\n             honorific\n             for honorific in HONORIFICS\n-            if name.casefold().startswith(honorific)\n+            if name.casefold().startswith(f\"{honorific} \")  # Note the trailing space.\n         ),\n         None,\n     ):\n-        return name[len(honorific) :].lstrip() or name\n+        return name[len(f\"{honorific} \") :].lstrip() or name\n \n     return name\n", "issue": "add a failing author name test\n<!-- What issue does this PR close? -->\r\nDemonstrates a bug in #9003\r\n\r\n<!-- What does this PR achieve? [feature|hotfix|fix|refactor] -->\r\n\r\n`Drini` -> `ini`\r\n\r\n**Expected:**\r\n`Drini` -> `Drini`\r\n\r\n\ud83d\ude09 \r\n\r\n### Technical\r\n<!-- What should be noted about the implementation? -->\r\n\r\n### Testing\r\n<!-- Steps for reviewer to reproduce/verify what this PR does/fixes. -->\r\n\r\n### Screenshot\r\n<!-- If this PR touches UI, please post evidence (screenshots) of it behaving correctly. -->\r\n\r\n### Stakeholders\r\n<!-- @ tag the lead (as labeled on the issue) and other stakeholders -->\r\n@cdrini \r\n@scottbarnes \r\n\r\n<!-- Attribution Disclaimer: By proposing this pull request, I affirm to have made a best-effort and exercised my discretion to make sure relevant sections of this code which substantially leverage code suggestions, code generation, or code snippets from sources (e.g. Stack Overflow, GitHub) have been annotated with basic attribution so reviewers & contributors may have confidence and access to the correct context to evaluate and use this code. -->\r\n\n", "before_files": [{"content": "from typing import TYPE_CHECKING, Any, Final\nimport web\nfrom openlibrary.catalog.utils import flip_name, author_dates_match, key_int\nfrom openlibrary.core.helpers import extract_year\n\n\nif TYPE_CHECKING:\n    from openlibrary.plugins.upstream.models import Author\n\n\n# Sort by descending length to remove the _longest_ match.\n# E.g. remove \"se\u00f1orita\" and not \"se\u00f1or\", when both match.\nHONORIFICS: Final = sorted(\n    [\n        'countess',\n        'doctor',\n        'doktor',\n        'dr',\n        'dr.',\n        'frau',\n        'fr\u00e4ulein',\n        'herr',\n        'lady',\n        'lord',\n        'm.',\n        'madame',\n        'mademoiselle',\n        'miss',\n        'mister',\n        'mistress',\n        'mixter',\n        'mlle',\n        'mlle.',\n        'mme',\n        'mme.',\n        'monsieur',\n        'mr',\n        'mr.',\n        'mrs',\n        'mrs.',\n        'ms',\n        'ms.',\n        'mx',\n        'mx.',\n        'professor',\n        'se\u00f1or',\n        'se\u00f1ora',\n        'se\u00f1orita',\n        'sir',\n        'sr.',\n        'sra.',\n        'srta.',\n    ],\n    key=lambda x: len(x),\n    reverse=True,\n)\n\nHONORIFC_NAME_EXECPTIONS = frozenset(\n    {\n        \"dr. seuss\",\n        \"dr seuss\",\n        \"dr oetker\",\n        \"doctor oetker\",\n    }\n)\n\n\ndef east_in_by_statement(rec, author):\n    \"\"\"\n    Returns False if there is no by_statement in rec.\n    Otherwise returns whether author name uses eastern name order.\n    TODO: elaborate on what this actually means, and how it is used.\n\n    :param dict rec: import source edition record\n    :param dict author: import source author dict: {\"name\": \"Some One\"}\n    :rtype: bool\n    \"\"\"\n\n    if 'by_statement' not in rec:\n        return False\n    if 'authors' not in rec:\n        return False\n    name = author['name']\n    flipped = flip_name(name)\n    name = name.replace('.', '')\n    name = name.replace(', ', '')\n    if name == flipped.replace('.', ''):\n        # name was not flipped\n        return False\n    return rec['by_statement'].find(name) != -1\n\n\ndef do_flip(author):\n    \"\"\"\n    Given an author import dict, flip its name in place\n    i.e. Smith, John => John Smith\n\n    :param dict author:\n    :rtype: None\n    \"\"\"\n    if 'personal_name' in author and author['personal_name'] != author['name']:\n        # Don't flip names if name is more complex than personal_name (legacy behaviour)\n        return\n    first_comma = author['name'].find(', ')\n    if first_comma == -1:\n        return\n    # e.g: Harper, John Murdoch, 1845-\n    if author['name'].find(',', first_comma + 1) != -1:\n        return\n    if author['name'].find('i.e.') != -1:\n        return\n    if author['name'].find('i. e.') != -1:\n        return\n    name = flip_name(author['name'])\n    author['name'] = name\n    if 'personal_name' in author:\n        author['personal_name'] = name\n\n\ndef pick_from_matches(author, match):\n    \"\"\"\n    Finds the best match for author from a list of OL authors records, match.\n\n    :param dict author: Author import representation\n    :param list match: List of matching OL author records\n    :rtype: dict\n    :return: A single OL author record from match\n    \"\"\"\n    maybe = []\n    if 'birth_date' in author and 'death_date' in author:\n        maybe = [m for m in match if 'birth_date' in m and 'death_date' in m]\n    elif 'date' in author:\n        maybe = [m for m in match if 'date' in m]\n    if not maybe:\n        maybe = match\n    if len(maybe) == 1:\n        return maybe[0]\n    return min(maybe, key=key_int)\n\n\ndef find_author(author: dict[str, Any]) -> list[\"Author\"]:\n    \"\"\"\n    Searches OL for an author by name.\n\n    :param str name: Author's name\n    :rtype: list\n    :return: A list of OL author representations than match name\n    \"\"\"\n\n    def walk_redirects(obj, seen):\n        seen.add(obj['key'])\n        while obj['type']['key'] == '/type/redirect':\n            assert obj['location'] != obj['key']\n            obj = web.ctx.site.get(obj['location'])\n            seen.add(obj['key'])\n        return obj\n\n    # Try for an 'exact' (case-insensitive) name match, but fall back to alternate_names,\n    # then last name with identical birth and death dates (that are not themselves `None` or '').\n    name = author[\"name\"].replace(\"*\", r\"\\*\")\n    queries = [\n        {\"type\": \"/type/author\", \"name~\": name},\n        {\"type\": \"/type/author\", \"alternate_names~\": name},\n        {\n            \"type\": \"/type/author\",\n            \"name~\": f\"* {name.split()[-1]}\",\n            \"birth_date~\": f\"*{extract_year(author.get('birth_date', '')) or -1}*\",\n            \"death_date~\": f\"*{extract_year(author.get('death_date', '')) or -1}*\",\n        },  # Use `-1` to ensure an empty string from extract_year doesn't match empty dates.\n    ]\n    for query in queries:\n        if reply := list(web.ctx.site.things(query)):\n            break\n\n    authors = [web.ctx.site.get(k) for k in reply]\n    if any(a.type.key != '/type/author' for a in authors):\n        seen: set[dict] = set()\n        authors = [walk_redirects(a, seen) for a in authors if a['key'] not in seen]\n    return authors\n\n\ndef find_entity(author: dict[str, Any]) -> \"Author | None\":\n    \"\"\"\n    Looks for an existing Author record in OL by name\n    and returns it if found.\n\n    :param dict author: Author import dict {\"name\": \"Some One\"}\n    :return: Existing Author record if found, or None.\n    \"\"\"\n    name = author['name']\n    things = find_author(author)\n    et = author.get('entity_type')\n    if et and et != 'person':\n        if not things:\n            return None\n        db_entity = things[0]\n        assert db_entity['type']['key'] == '/type/author'\n        return db_entity\n    if ', ' in name:\n        flipped_name = flip_name(author[\"name\"])\n        author_flipped_name = author.copy()\n        things += find_author(author_flipped_name)\n    match = []\n    seen = set()\n    for a in things:\n        key = a['key']\n        if key in seen:\n            continue\n        seen.add(key)\n        orig_key = key\n        assert a.type.key == '/type/author'\n        if 'birth_date' in author and 'birth_date' not in a:\n            continue\n        if 'birth_date' not in author and 'birth_date' in a:\n            continue\n        if not author_dates_match(author, a):\n            continue\n        match.append(a)\n    if not match:\n        return None\n    if len(match) == 1:\n        return match[0]\n    return pick_from_matches(author, match)\n\n\ndef remove_author_honorifics(name: str) -> str:\n    \"\"\"\n    Remove honorifics from an author's name field.\n\n    If the author's name is only an honorific, it will return the original name.\n    \"\"\"\n    if name.casefold() in HONORIFC_NAME_EXECPTIONS:\n        return name\n\n    if honorific := next(\n        (\n            honorific\n            for honorific in HONORIFICS\n            if name.casefold().startswith(honorific)\n        ),\n        None,\n    ):\n        return name[len(honorific) :].lstrip() or name\n\n    return name\n\n\ndef import_author(author: dict[str, Any], eastern=False) -> \"Author | dict[str, Any]\":\n    \"\"\"\n    Converts an import style new-author dictionary into an\n    Open Library existing author, or new author candidate, representation.\n    Does NOT create new authors.\n\n    :param dict author: Author import record {\"name\": \"Some One\"}\n    :param bool eastern: Eastern name order\n    :return: Open Library style Author representation, either existing with \"key\",\n             or new candidate without \"key\".\n    \"\"\"\n    if existing := find_entity(author):\n        assert existing.type.key == '/type/author'\n        for k in 'last_modified', 'id', 'revision', 'created':\n            if existing.k:\n                del existing.k\n        new = existing\n        if 'death_date' in author and 'death_date' not in existing:\n            new['death_date'] = author['death_date']\n        return new\n    if author.get('entity_type') != 'org' and not eastern:\n        do_flip(author)\n    a = {'type': {'key': '/type/author'}}\n    for f in 'name', 'title', 'personal_name', 'birth_date', 'death_date', 'date':\n        if f in author:\n            a[f] = author[f]\n    return a\n\n\nclass InvalidLanguage(Exception):\n    def __init__(self, code):\n        self.code = code\n\n    def __str__(self):\n        return \"invalid language code: '%s'\" % self.code\n\n\ntype_map = {'description': 'text', 'notes': 'text', 'number_of_pages': 'int'}\n\n\ndef build_query(rec):\n    \"\"\"\n    Takes an edition record dict, rec, and returns an Open Library edition\n    suitable for saving.\n\n    :param dict rec: Edition record to add to Open Library\n    :rtype: dict\n    :return: Open Library style edition representation\n    \"\"\"\n    book = {\n        'type': {'key': '/type/edition'},\n    }\n\n    for k, v in rec.items():\n        if k == 'authors':\n            if v and v[0]:\n                book['authors'] = []\n                for author in v:\n                    author['name'] = remove_author_honorifics(author['name'])\n                    east = east_in_by_statement(rec, author)\n                    book['authors'].append(import_author(author, eastern=east))\n            continue\n        if k in ('languages', 'translated_from'):\n            for language in v:\n                if web.ctx.site.get('/languages/' + language) is None:\n                    raise InvalidLanguage(language)\n            book[k] = [{'key': '/languages/' + language} for language in v]\n            continue\n        if k in type_map:\n            t = '/type/' + type_map[k]\n            if isinstance(v, list):\n                book[k] = [{'type': t, 'value': i} for i in v]\n            else:\n                book[k] = {'type': t, 'value': v}\n        else:\n            book[k] = v\n    return book\n", "path": "openlibrary/catalog/add_book/load_book.py"}], "after_files": [{"content": "from typing import TYPE_CHECKING, Any, Final\nimport web\nfrom openlibrary.catalog.utils import flip_name, author_dates_match, key_int\nfrom openlibrary.core.helpers import extract_year\n\n\nif TYPE_CHECKING:\n    from openlibrary.plugins.upstream.models import Author\n\n\n# Sort by descending length to remove the _longest_ match.\n# E.g. remove \"se\u00f1orita\" and not \"se\u00f1or\", when both match.\nHONORIFICS: Final = sorted(\n    [\n        'countess',\n        'doctor',\n        'doktor',\n        'dr',\n        'dr.',\n        'frau',\n        'fr\u00e4ulein',\n        'herr',\n        'lady',\n        'lord',\n        'm.',\n        'madame',\n        'mademoiselle',\n        'miss',\n        'mister',\n        'mistress',\n        'mixter',\n        'mlle',\n        'mlle.',\n        'mme',\n        'mme.',\n        'monsieur',\n        'mr',\n        'mr.',\n        'mrs',\n        'mrs.',\n        'ms',\n        'ms.',\n        'mx',\n        'mx.',\n        'professor',\n        'se\u00f1or',\n        'se\u00f1ora',\n        'se\u00f1orita',\n        'sir',\n        'sr.',\n        'sra.',\n        'srta.',\n    ],\n    key=lambda x: len(x),\n    reverse=True,\n)\n\nHONORIFC_NAME_EXECPTIONS = frozenset(\n    {\n        \"dr. seuss\",\n        \"dr seuss\",\n        \"dr oetker\",\n        \"doctor oetker\",\n    }\n)\n\n\ndef east_in_by_statement(rec, author):\n    \"\"\"\n    Returns False if there is no by_statement in rec.\n    Otherwise returns whether author name uses eastern name order.\n    TODO: elaborate on what this actually means, and how it is used.\n\n    :param dict rec: import source edition record\n    :param dict author: import source author dict: {\"name\": \"Some One\"}\n    :rtype: bool\n    \"\"\"\n\n    if 'by_statement' not in rec:\n        return False\n    if 'authors' not in rec:\n        return False\n    name = author['name']\n    flipped = flip_name(name)\n    name = name.replace('.', '')\n    name = name.replace(', ', '')\n    if name == flipped.replace('.', ''):\n        # name was not flipped\n        return False\n    return rec['by_statement'].find(name) != -1\n\n\ndef do_flip(author):\n    \"\"\"\n    Given an author import dict, flip its name in place\n    i.e. Smith, John => John Smith\n\n    :param dict author:\n    :rtype: None\n    \"\"\"\n    if 'personal_name' in author and author['personal_name'] != author['name']:\n        # Don't flip names if name is more complex than personal_name (legacy behaviour)\n        return\n    first_comma = author['name'].find(', ')\n    if first_comma == -1:\n        return\n    # e.g: Harper, John Murdoch, 1845-\n    if author['name'].find(',', first_comma + 1) != -1:\n        return\n    if author['name'].find('i.e.') != -1:\n        return\n    if author['name'].find('i. e.') != -1:\n        return\n    name = flip_name(author['name'])\n    author['name'] = name\n    if 'personal_name' in author:\n        author['personal_name'] = name\n\n\ndef pick_from_matches(author, match):\n    \"\"\"\n    Finds the best match for author from a list of OL authors records, match.\n\n    :param dict author: Author import representation\n    :param list match: List of matching OL author records\n    :rtype: dict\n    :return: A single OL author record from match\n    \"\"\"\n    maybe = []\n    if 'birth_date' in author and 'death_date' in author:\n        maybe = [m for m in match if 'birth_date' in m and 'death_date' in m]\n    elif 'date' in author:\n        maybe = [m for m in match if 'date' in m]\n    if not maybe:\n        maybe = match\n    if len(maybe) == 1:\n        return maybe[0]\n    return min(maybe, key=key_int)\n\n\ndef find_author(author: dict[str, Any]) -> list[\"Author\"]:\n    \"\"\"\n    Searches OL for an author by name.\n\n    :param str name: Author's name\n    :rtype: list\n    :return: A list of OL author representations than match name\n    \"\"\"\n\n    def walk_redirects(obj, seen):\n        seen.add(obj['key'])\n        while obj['type']['key'] == '/type/redirect':\n            assert obj['location'] != obj['key']\n            obj = web.ctx.site.get(obj['location'])\n            seen.add(obj['key'])\n        return obj\n\n    # Try for an 'exact' (case-insensitive) name match, but fall back to alternate_names,\n    # then last name with identical birth and death dates (that are not themselves `None` or '').\n    name = author[\"name\"].replace(\"*\", r\"\\*\")\n    queries = [\n        {\"type\": \"/type/author\", \"name~\": name},\n        {\"type\": \"/type/author\", \"alternate_names~\": name},\n        {\n            \"type\": \"/type/author\",\n            \"name~\": f\"* {name.split()[-1]}\",\n            \"birth_date~\": f\"*{extract_year(author.get('birth_date', '')) or -1}*\",\n            \"death_date~\": f\"*{extract_year(author.get('death_date', '')) or -1}*\",\n        },  # Use `-1` to ensure an empty string from extract_year doesn't match empty dates.\n    ]\n    for query in queries:\n        if reply := list(web.ctx.site.things(query)):\n            break\n\n    authors = [web.ctx.site.get(k) for k in reply]\n    if any(a.type.key != '/type/author' for a in authors):\n        seen: set[dict] = set()\n        authors = [walk_redirects(a, seen) for a in authors if a['key'] not in seen]\n    return authors\n\n\ndef find_entity(author: dict[str, Any]) -> \"Author | None\":\n    \"\"\"\n    Looks for an existing Author record in OL by name\n    and returns it if found.\n\n    :param dict author: Author import dict {\"name\": \"Some One\"}\n    :return: Existing Author record if found, or None.\n    \"\"\"\n    name = author['name']\n    things = find_author(author)\n    et = author.get('entity_type')\n    if et and et != 'person':\n        if not things:\n            return None\n        db_entity = things[0]\n        assert db_entity['type']['key'] == '/type/author'\n        return db_entity\n    if ', ' in name:\n        flipped_name = flip_name(author[\"name\"])\n        author_flipped_name = author.copy()\n        things += find_author(author_flipped_name)\n    match = []\n    seen = set()\n    for a in things:\n        key = a['key']\n        if key in seen:\n            continue\n        seen.add(key)\n        orig_key = key\n        assert a.type.key == '/type/author'\n        if 'birth_date' in author and 'birth_date' not in a:\n            continue\n        if 'birth_date' not in author and 'birth_date' in a:\n            continue\n        if not author_dates_match(author, a):\n            continue\n        match.append(a)\n    if not match:\n        return None\n    if len(match) == 1:\n        return match[0]\n    return pick_from_matches(author, match)\n\n\ndef remove_author_honorifics(name: str) -> str:\n    \"\"\"\n    Remove honorifics from an author's name field.\n\n    If the author's name is only an honorific, it will return the original name.\n    \"\"\"\n    if name.casefold() in HONORIFC_NAME_EXECPTIONS:\n        return name\n\n    if honorific := next(\n        (\n            honorific\n            for honorific in HONORIFICS\n            if name.casefold().startswith(f\"{honorific} \")  # Note the trailing space.\n        ),\n        None,\n    ):\n        return name[len(f\"{honorific} \") :].lstrip() or name\n\n    return name\n\n\ndef import_author(author: dict[str, Any], eastern=False) -> \"Author | dict[str, Any]\":\n    \"\"\"\n    Converts an import style new-author dictionary into an\n    Open Library existing author, or new author candidate, representation.\n    Does NOT create new authors.\n\n    :param dict author: Author import record {\"name\": \"Some One\"}\n    :param bool eastern: Eastern name order\n    :return: Open Library style Author representation, either existing with \"key\",\n             or new candidate without \"key\".\n    \"\"\"\n    if existing := find_entity(author):\n        assert existing.type.key == '/type/author'\n        for k in 'last_modified', 'id', 'revision', 'created':\n            if existing.k:\n                del existing.k\n        new = existing\n        if 'death_date' in author and 'death_date' not in existing:\n            new['death_date'] = author['death_date']\n        return new\n    if author.get('entity_type') != 'org' and not eastern:\n        do_flip(author)\n    a = {'type': {'key': '/type/author'}}\n    for f in 'name', 'title', 'personal_name', 'birth_date', 'death_date', 'date':\n        if f in author:\n            a[f] = author[f]\n    return a\n\n\nclass InvalidLanguage(Exception):\n    def __init__(self, code):\n        self.code = code\n\n    def __str__(self):\n        return \"invalid language code: '%s'\" % self.code\n\n\ntype_map = {'description': 'text', 'notes': 'text', 'number_of_pages': 'int'}\n\n\ndef build_query(rec):\n    \"\"\"\n    Takes an edition record dict, rec, and returns an Open Library edition\n    suitable for saving.\n\n    :param dict rec: Edition record to add to Open Library\n    :rtype: dict\n    :return: Open Library style edition representation\n    \"\"\"\n    book = {\n        'type': {'key': '/type/edition'},\n    }\n\n    for k, v in rec.items():\n        if k == 'authors':\n            if v and v[0]:\n                book['authors'] = []\n                for author in v:\n                    author['name'] = remove_author_honorifics(author['name'])\n                    east = east_in_by_statement(rec, author)\n                    book['authors'].append(import_author(author, eastern=east))\n            continue\n        if k in ('languages', 'translated_from'):\n            for language in v:\n                if web.ctx.site.get('/languages/' + language) is None:\n                    raise InvalidLanguage(language)\n            book[k] = [{'key': '/languages/' + language} for language in v]\n            continue\n        if k in type_map:\n            t = '/type/' + type_map[k]\n            if isinstance(v, list):\n                book[k] = [{'type': t, 'value': i} for i in v]\n            else:\n                book[k] = {'type': t, 'value': v}\n        else:\n            book[k] = v\n    return book\n", "path": "openlibrary/catalog/add_book/load_book.py"}]}