We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
DeprecationWarning: `urllib.parse.splitquery()` is deprecated as of 3.8
<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->


### Evidence / Screenshot (if possible)
When we run pytest in our GitHub Actions, we get the following warning
```
=============================== warnings summary ===============================
openlibrary/coverstore/tests/test_doctests.py::test_doctest[openlibrary.coverstore.utils]
openlibrary/coverstore/tests/test_doctests.py::test_doctest[openlibrary.coverstore.utils]
openlibrary/coverstore/tests/test_doctests.py::test_doctest[openlibrary.coverstore.utils]
  /home/runner/work/openlibrary/openlibrary/openlibrary/coverstore/utils.py:88:
   DeprecationWarning: urllib.parse.splitquery() is deprecated as of 3.8, use urllib.parse.urlparse() instead
    base, query = splitquery(URL)
```
https://github.com/internetarchive/openlibrary/blob/master/openlibrary/coverstore/utils.py#L88

### Relevant url?
<!-- `https://openlibrary.org/...` -->

### Steps to Reproduce
<!-- What steps caused you to find the bug? -->
1. Go to ...
2. Do ...

<!-- What actually happened after these steps? What did you expect to happen? -->
* Actual: pytest warning
* Expected: No pytest warning

### Details

- **Logged in (Y/N)?**
- **Browser type/version?**
- **Operating system?**
- **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? -->

### 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 -->


--- 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/coverstore/utils.py`
Content:
```
1 """Utilities for coverstore"""
2 
3 import json
4 import mimetypes
5 import os
6 import random
7 import socket
8 import string
9 
10 import requests
11 import web
12 from urllib.parse import splitquery, unquote, unquote_plus  # type: ignore[attr-defined]
13 from urllib.parse import urlencode as real_urlencode
14 
15 from openlibrary.coverstore import config, oldb
16 
17 from io import IOBase as file
18 
19 socket.setdefaulttimeout(10.0)
20 
21 
22 def safeint(value, default=None):
23     """
24     >>> safeint('1')
25     1
26     >>> safeint('x')
27     >>> safeint('x', 0)
28     0
29     """
30     try:
31         return int(value)
32     except (TypeError, ValueError):
33         return default
34 
35 
36 def get_ol_url():
37     return web.rstrips(config.ol_url, "/")
38 
39 
40 def ol_things(key, value):
41     if oldb.is_supported():
42         return oldb.query(key, value)
43     else:
44         query = {
45             'type': '/type/edition',
46             key: value,
47             'sort': 'last_modified',
48             'limit': 10,
49         }
50         try:
51             d = dict(query=json.dumps(query))
52             result = download(get_ol_url() + '/api/things?' + real_urlencode(d))
53             result = json.loads(result)
54             return result['result']
55         except OSError:
56             import traceback
57 
58             traceback.print_exc()
59             return []
60 
61 
62 def ol_get(olkey):
63     if oldb.is_supported():
64         return oldb.get(olkey)
65     else:
66         try:
67             return json.loads(download(get_ol_url() + olkey + ".json"))
68         except OSError:
69             return None
70 
71 
72 USER_AGENT = (
73     "Mozilla/5.0 (Compatible; coverstore downloader http://covers.openlibrary.org)"
74 )
75 
76 
77 def download(url):
78     return requests.get(url, headers={'User-Agent': USER_AGENT}).content
79 
80 
81 def urldecode(url):
82     """
83     >>> urldecode('http://google.com/search?q=bar&x=y')
84     ('http://google.com/search', {'q': 'bar', 'x': 'y'})
85     >>> urldecode('http://google.com/')
86     ('http://google.com/', {})
87     """
88     base, query = splitquery(url)
89     query = query or ""
90     items = [item.split('=', 1) for item in query.split('&') if '=' in item]
91     d = {unquote(k): unquote_plus(v) for (k, v) in items}
92     return base, d
93 
94 
95 def changequery(url, **kw):
96     """
97     >>> changequery('http://google.com/search?q=foo', q='bar', x='y')
98     'http://google.com/search?q=bar&x=y'
99     """
100     base, params = urldecode(url)
101     params.update(kw)
102     return base + '?' + real_urlencode(params)
103 
104 
105 def read_file(path, offset, size, chunk=50 * 1024):
106     """Returns an iterator over file data at specified offset and size.
107 
108     >>> len(b"".join(read_file('/dev/urandom', 100, 10000)))
109     10000
110     """
111     with open(path, "rb") as f:
112         f.seek(offset)
113         while size:
114             data = f.read(min(chunk, size))
115             size -= len(data)
116             if data:
117                 yield data
118             else:
119                 raise OSError("file truncated")
120 
121 
122 def rm_f(filename):
123     try:
124         os.remove(filename)
125     except OSError:
126         pass
127 
128 
129 chars = string.ascii_letters + string.digits
130 
131 
132 def random_string(n):
133     return "".join([random.choice(chars) for i in range(n)])
134 
135 
136 def urlencode(data):
137     """
138     urlencodes the given data dictionary. If any of the value is a file object, data is multipart encoded.
139 
140     @@@ should go into web.browser
141     """
142     multipart = False
143     for v in data.values():
144         if isinstance(v, file):
145             multipart = True
146             break
147 
148     if not multipart:
149         return 'application/x-www-form-urlencoded', real_urlencode(data)
150     else:
151         # adopted from http://code.activestate.com/recipes/146306/
152         def get_content_type(filename):
153             return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
154 
155         def encode(key, value, out):
156             if isinstance(value, file):
157                 out.append('--' + BOUNDARY)
158                 out.append(
159                     f'Content-Disposition: form-data; name="{key}"; filename="{value.name}"'
160                 )
161                 out.append('Content-Type: %s' % get_content_type(value.name))
162                 out.append('')
163                 out.append(value.read())
164             elif isinstance(value, list):
165                 for v in value:
166                     encode(key, v)
167             else:
168                 out.append('--' + BOUNDARY)
169                 out.append('Content-Disposition: form-data; name="%s"' % key)
170                 out.append('')
171                 out.append(value)
172 
173         BOUNDARY = "----------ThIs_Is_tHe_bouNdaRY_$"
174         CRLF = '\r\n'
175         out = []
176         for k, v in data.items():
177             encode(k, v, out)
178         body = CRLF.join(out)
179         content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
180         return content_type, body
181 
182 
183 if __name__ == "__main__":
184     import doctest
185 
186     doctest.testmod()
187
```
--- 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/coverstore/utils.py b/openlibrary/coverstore/utils.py
--- a/openlibrary/coverstore/utils.py
+++ b/openlibrary/coverstore/utils.py
@@ -9,7 +9,7 @@
 
 import requests
 import web
-from urllib.parse import splitquery, unquote, unquote_plus  # type: ignore[attr-defined]
+from urllib.parse import urlsplit, urlunsplit, parse_qsl, unquote, unquote_plus  # type: ignore[attr-defined]
 from urllib.parse import urlencode as real_urlencode
 
 from openlibrary.coverstore import config, oldb
@@ -78,17 +78,17 @@
     return requests.get(url, headers={'User-Agent': USER_AGENT}).content
 
 
-def urldecode(url):
+def urldecode(url: str) -> tuple[str, dict[str, str]]:
     """
     >>> urldecode('http://google.com/search?q=bar&x=y')
     ('http://google.com/search', {'q': 'bar', 'x': 'y'})
     >>> urldecode('http://google.com/')
     ('http://google.com/', {})
     """
-    base, query = splitquery(url)
-    query = query or ""
-    items = [item.split('=', 1) for item in query.split('&') if '=' in item]
+    split_url = urlsplit(url)
+    items = parse_qsl(split_url.query)
     d = {unquote(k): unquote_plus(v) for (k, v) in items}
+    base = urlunsplit(split_url._replace(query=''))
     return base, d

{"golden_diff": "diff --git a/openlibrary/coverstore/utils.py b/openlibrary/coverstore/utils.py\n--- a/openlibrary/coverstore/utils.py\n+++ b/openlibrary/coverstore/utils.py\n@@ -9,7 +9,7 @@\n \n import requests\n import web\n-from urllib.parse import splitquery, unquote, unquote_plus  # type: ignore[attr-defined]\n+from urllib.parse import urlsplit, urlunsplit, parse_qsl, unquote, unquote_plus  # type: ignore[attr-defined]\n from urllib.parse import urlencode as real_urlencode\n \n from openlibrary.coverstore import config, oldb\n@@ -78,17 +78,17 @@\n     return requests.get(url, headers={'User-Agent': USER_AGENT}).content\n \n \n-def urldecode(url):\n+def urldecode(url: str) -> tuple[str, dict[str, str]]:\n     \"\"\"\n     >>> urldecode('http://google.com/search?q=bar&x=y')\n     ('http://google.com/search', {'q': 'bar', 'x': 'y'})\n     >>> urldecode('http://google.com/')\n     ('http://google.com/', {})\n     \"\"\"\n-    base, query = splitquery(url)\n-    query = query or \"\"\n-    items = [item.split('=', 1) for item in query.split('&') if '=' in item]\n+    split_url = urlsplit(url)\n+    items = parse_qsl(split_url.query)\n     d = {unquote(k): unquote_plus(v) for (k, v) in items}\n+    base = urlunsplit(split_url._replace(query=''))\n     return base, d\n", "issue": "DeprecationWarning: `urllib.parse.splitquery()` is deprecated as of 3.8\n<!-- What problem are we solving? What does the experience look like today? What are the symptoms? -->\r\n\r\n\r\n### Evidence / Screenshot (if possible)\r\nWhen we run pytest in our GitHub Actions, we get the following warning\r\n```\r\n=============================== warnings summary ===============================\r\nopenlibrary/coverstore/tests/test_doctests.py::test_doctest[openlibrary.coverstore.utils]\r\nopenlibrary/coverstore/tests/test_doctests.py::test_doctest[openlibrary.coverstore.utils]\r\nopenlibrary/coverstore/tests/test_doctests.py::test_doctest[openlibrary.coverstore.utils]\r\n  /home/runner/work/openlibrary/openlibrary/openlibrary/coverstore/utils.py:88:\r\n   DeprecationWarning: urllib.parse.splitquery() is deprecated as of 3.8, use urllib.parse.urlparse() instead\r\n    base, query = splitquery(URL)\r\n```\r\nhttps://github.com/internetarchive/openlibrary/blob/master/openlibrary/coverstore/utils.py#L88\r\n\r\n### Relevant url?\r\n<!-- `https://openlibrary.org/...` -->\r\n\r\n### Steps to Reproduce\r\n<!-- What steps caused you to find the bug? -->\r\n1. Go to ...\r\n2. Do ...\r\n\r\n<!-- What actually happened after these steps? What did you expect to happen? -->\r\n* Actual: pytest warning\r\n* Expected: No pytest warning\r\n\r\n### Details\r\n\r\n- **Logged in (Y/N)?**\r\n- **Browser type/version?**\r\n- **Operating system?**\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\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\n", "before_files": [{"content": "\"\"\"Utilities for coverstore\"\"\"\n\nimport json\nimport mimetypes\nimport os\nimport random\nimport socket\nimport string\n\nimport requests\nimport web\nfrom urllib.parse import splitquery, unquote, unquote_plus  # type: ignore[attr-defined]\nfrom urllib.parse import urlencode as real_urlencode\n\nfrom openlibrary.coverstore import config, oldb\n\nfrom io import IOBase as file\n\nsocket.setdefaulttimeout(10.0)\n\n\ndef safeint(value, default=None):\n    \"\"\"\n    >>> safeint('1')\n    1\n    >>> safeint('x')\n    >>> safeint('x', 0)\n    0\n    \"\"\"\n    try:\n        return int(value)\n    except (TypeError, ValueError):\n        return default\n\n\ndef get_ol_url():\n    return web.rstrips(config.ol_url, \"/\")\n\n\ndef ol_things(key, value):\n    if oldb.is_supported():\n        return oldb.query(key, value)\n    else:\n        query = {\n            'type': '/type/edition',\n            key: value,\n            'sort': 'last_modified',\n            'limit': 10,\n        }\n        try:\n            d = dict(query=json.dumps(query))\n            result = download(get_ol_url() + '/api/things?' + real_urlencode(d))\n            result = json.loads(result)\n            return result['result']\n        except OSError:\n            import traceback\n\n            traceback.print_exc()\n            return []\n\n\ndef ol_get(olkey):\n    if oldb.is_supported():\n        return oldb.get(olkey)\n    else:\n        try:\n            return json.loads(download(get_ol_url() + olkey + \".json\"))\n        except OSError:\n            return None\n\n\nUSER_AGENT = (\n    \"Mozilla/5.0 (Compatible; coverstore downloader http://covers.openlibrary.org)\"\n)\n\n\ndef download(url):\n    return requests.get(url, headers={'User-Agent': USER_AGENT}).content\n\n\ndef urldecode(url):\n    \"\"\"\n    >>> urldecode('http://google.com/search?q=bar&x=y')\n    ('http://google.com/search', {'q': 'bar', 'x': 'y'})\n    >>> urldecode('http://google.com/')\n    ('http://google.com/', {})\n    \"\"\"\n    base, query = splitquery(url)\n    query = query or \"\"\n    items = [item.split('=', 1) for item in query.split('&') if '=' in item]\n    d = {unquote(k): unquote_plus(v) for (k, v) in items}\n    return base, d\n\n\ndef changequery(url, **kw):\n    \"\"\"\n    >>> changequery('http://google.com/search?q=foo', q='bar', x='y')\n    'http://google.com/search?q=bar&x=y'\n    \"\"\"\n    base, params = urldecode(url)\n    params.update(kw)\n    return base + '?' + real_urlencode(params)\n\n\ndef read_file(path, offset, size, chunk=50 * 1024):\n    \"\"\"Returns an iterator over file data at specified offset and size.\n\n    >>> len(b\"\".join(read_file('/dev/urandom', 100, 10000)))\n    10000\n    \"\"\"\n    with open(path, \"rb\") as f:\n        f.seek(offset)\n        while size:\n            data = f.read(min(chunk, size))\n            size -= len(data)\n            if data:\n                yield data\n            else:\n                raise OSError(\"file truncated\")\n\n\ndef rm_f(filename):\n    try:\n        os.remove(filename)\n    except OSError:\n        pass\n\n\nchars = string.ascii_letters + string.digits\n\n\ndef random_string(n):\n    return \"\".join([random.choice(chars) for i in range(n)])\n\n\ndef urlencode(data):\n    \"\"\"\n    urlencodes the given data dictionary. If any of the value is a file object, data is multipart encoded.\n\n    @@@ should go into web.browser\n    \"\"\"\n    multipart = False\n    for v in data.values():\n        if isinstance(v, file):\n            multipart = True\n            break\n\n    if not multipart:\n        return 'application/x-www-form-urlencoded', real_urlencode(data)\n    else:\n        # adopted from http://code.activestate.com/recipes/146306/\n        def get_content_type(filename):\n            return mimetypes.guess_type(filename)[0] or 'application/octet-stream'\n\n        def encode(key, value, out):\n            if isinstance(value, file):\n                out.append('--' + BOUNDARY)\n                out.append(\n                    f'Content-Disposition: form-data; name=\"{key}\"; filename=\"{value.name}\"'\n                )\n                out.append('Content-Type: %s' % get_content_type(value.name))\n                out.append('')\n                out.append(value.read())\n            elif isinstance(value, list):\n                for v in value:\n                    encode(key, v)\n            else:\n                out.append('--' + BOUNDARY)\n                out.append('Content-Disposition: form-data; name=\"%s\"' % key)\n                out.append('')\n                out.append(value)\n\n        BOUNDARY = \"----------ThIs_Is_tHe_bouNdaRY_$\"\n        CRLF = '\\r\\n'\n        out = []\n        for k, v in data.items():\n            encode(k, v, out)\n        body = CRLF.join(out)\n        content_type = 'multipart/form-data; boundary=%s' % BOUNDARY\n        return content_type, body\n\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod()\n", "path": "openlibrary/coverstore/utils.py"}], "after_files": [{"content": "\"\"\"Utilities for coverstore\"\"\"\n\nimport json\nimport mimetypes\nimport os\nimport random\nimport socket\nimport string\n\nimport requests\nimport web\nfrom urllib.parse import urlsplit, urlunsplit, parse_qsl, unquote, unquote_plus  # type: ignore[attr-defined]\nfrom urllib.parse import urlencode as real_urlencode\n\nfrom openlibrary.coverstore import config, oldb\n\nfrom io import IOBase as file\n\nsocket.setdefaulttimeout(10.0)\n\n\ndef safeint(value, default=None):\n    \"\"\"\n    >>> safeint('1')\n    1\n    >>> safeint('x')\n    >>> safeint('x', 0)\n    0\n    \"\"\"\n    try:\n        return int(value)\n    except (TypeError, ValueError):\n        return default\n\n\ndef get_ol_url():\n    return web.rstrips(config.ol_url, \"/\")\n\n\ndef ol_things(key, value):\n    if oldb.is_supported():\n        return oldb.query(key, value)\n    else:\n        query = {\n            'type': '/type/edition',\n            key: value,\n            'sort': 'last_modified',\n            'limit': 10,\n        }\n        try:\n            d = dict(query=json.dumps(query))\n            result = download(get_ol_url() + '/api/things?' + real_urlencode(d))\n            result = json.loads(result)\n            return result['result']\n        except OSError:\n            import traceback\n\n            traceback.print_exc()\n            return []\n\n\ndef ol_get(olkey):\n    if oldb.is_supported():\n        return oldb.get(olkey)\n    else:\n        try:\n            return json.loads(download(get_ol_url() + olkey + \".json\"))\n        except OSError:\n            return None\n\n\nUSER_AGENT = (\n    \"Mozilla/5.0 (Compatible; coverstore downloader http://covers.openlibrary.org)\"\n)\n\n\ndef download(url):\n    return requests.get(url, headers={'User-Agent': USER_AGENT}).content\n\n\ndef urldecode(url: str) -> tuple[str, dict[str, str]]:\n    \"\"\"\n    >>> urldecode('http://google.com/search?q=bar&x=y')\n    ('http://google.com/search', {'q': 'bar', 'x': 'y'})\n    >>> urldecode('http://google.com/')\n    ('http://google.com/', {})\n    \"\"\"\n    split_url = urlsplit(url)\n    items = parse_qsl(split_url.query)\n    d = {unquote(k): unquote_plus(v) for (k, v) in items}\n    base = urlunsplit(split_url._replace(query=''))\n    return base, d\n\n\ndef changequery(url, **kw):\n    \"\"\"\n    >>> changequery('http://google.com/search?q=foo', q='bar', x='y')\n    'http://google.com/search?q=bar&x=y'\n    \"\"\"\n    base, params = urldecode(url)\n    params.update(kw)\n    return base + '?' + real_urlencode(params)\n\n\ndef read_file(path, offset, size, chunk=50 * 1024):\n    \"\"\"Returns an iterator over file data at specified offset and size.\n\n    >>> len(b\"\".join(read_file('/dev/urandom', 100, 10000)))\n    10000\n    \"\"\"\n    with open(path, \"rb\") as f:\n        f.seek(offset)\n        while size:\n            data = f.read(min(chunk, size))\n            size -= len(data)\n            if data:\n                yield data\n            else:\n                raise OSError(\"file truncated\")\n\n\ndef rm_f(filename):\n    try:\n        os.remove(filename)\n    except OSError:\n        pass\n\n\nchars = string.ascii_letters + string.digits\n\n\ndef random_string(n):\n    return \"\".join([random.choice(chars) for i in range(n)])\n\n\ndef urlencode(data):\n    \"\"\"\n    urlencodes the given data dictionary. If any of the value is a file object, data is multipart encoded.\n\n    @@@ should go into web.browser\n    \"\"\"\n    multipart = False\n    for v in data.values():\n        if isinstance(v, file):\n            multipart = True\n            break\n\n    if not multipart:\n        return 'application/x-www-form-urlencoded', real_urlencode(data)\n    else:\n        # adopted from http://code.activestate.com/recipes/146306/\n        def get_content_type(filename):\n            return mimetypes.guess_type(filename)[0] or 'application/octet-stream'\n\n        def encode(key, value, out):\n            if isinstance(value, file):\n                out.append('--' + BOUNDARY)\n                out.append(\n                    f'Content-Disposition: form-data; name=\"{key}\"; filename=\"{value.name}\"'\n                )\n                out.append('Content-Type: %s' % get_content_type(value.name))\n                out.append('')\n                out.append(value.read())\n            elif isinstance(value, list):\n                for v in value:\n                    encode(key, v)\n            else:\n                out.append('--' + BOUNDARY)\n                out.append('Content-Disposition: form-data; name=\"%s\"' % key)\n                out.append('')\n                out.append(value)\n\n        BOUNDARY = \"----------ThIs_Is_tHe_bouNdaRY_$\"\n        CRLF = '\\r\\n'\n        out = []\n        for k, v in data.items():\n            encode(k, v, out)\n        body = CRLF.join(out)\n        content_type = 'multipart/form-data; boundary=%s' % BOUNDARY\n        return content_type, body\n\n\nif __name__ == \"__main__\":\n    import doctest\n\n    doctest.testmod()\n", "path": "openlibrary/coverstore/utils.py"}]}