We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
32 bit tests failing
The 32 bit tests are currently failing, due to a failed attempt to install the `cryptography` library from source. See e.g. https://dev.azure.com/sunpy/sunpy/_build/results?buildId=9154&view=logs&jobId=a7b3aa55-7d57-562f-3433-7f6b2d4252da&j=2920fa54-56c6-5dfd-d3d8-7b1b694d90cb&t=bba4d609-c264-5f9c-e072-020a8a3b573f

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `sunpy/util/util.py`
Content:
```
1 """
2 This module provides general utility functions.
3 """
4 import os
5 import hashlib
6 from shutil import get_terminal_size
7 from itertools import chain, count
8 from collections import UserList
9 
10 __all__ = ['unique', 'replacement_filename', 'expand_list',
11            'expand_list_generator', 'dict_keys_same', 'hash_file', "get_width"]
12 
13 
14 def unique(itr, key=None):
15     """
16     Return only unique elements of a sequence.
17 
18     Parameters
19     ----------
20     itr : `iterable`
21         Any iterable sequence.
22     key : `function`, optional
23         A function to apply to each element in the iterable. Defaults to `None`.
24 
25     Returns
26     -------
27     `set`:
28         A `set` of each unique element.
29     """
30     items = set()
31     if key is None:
32         for elem in itr:
33             if elem not in items:
34                 yield elem
35                 items.add(elem)
36     else:
37         for elem in itr:
38             x = key(elem)
39             if x not in items:
40                 yield elem
41                 items.add(x)
42 
43 
44 def replacement_filename(path):
45     """
46     Return a replacement path if input path is currently in use.
47 
48     Enumerates until an unused filename is found, e.g., "foo.fits" becomes
49     "foo.0.fits", if that is used, "foo.1.fits" and so on.
50 
51     Parameters
52     ----------
53     path : `str`
54         A string path.
55 
56     Returns
57     -------
58     `str`:
59         A string path.
60     """
61     if not os.path.exists(path):
62         return path
63     else:
64         dir_, filename = os.path.split(path)
65         base, ext = os.path.splitext(filename)
66         for c in count():
67             name = base + '.' + str(c) + ext
68             newpath = os.path.join(dir_, name)
69             if not os.path.exists(newpath):
70                 return newpath
71 
72 
73 def expand_list(inp):
74     """
75     Expand a list of lists or tuples.
76 
77     Parameters
78     ----------
79     inp : `list`, `tuple`, `collections.UserList`
80         The iterable to expand.
81 
82     Returns
83     -------
84     `list`
85         A flat list consisting of the entries of the input.
86 
87     References
88     ----------
89     * https://stackoverflow.com/questions/2185822/expanding-elements-in-a-list/2185971#2185971
90     """
91     return [item for item in expand_list_generator(inp)]
92 
93 
94 def expand_list_generator(inp):
95     for item in inp:
96         if isinstance(item, (tuple, list, UserList)):
97             yield from expand_list_generator(item)
98         else:
99             yield item
100 
101 
102 def partial_key_match(key, dictionary):
103     """
104     Return value/values from a dictionary based on a partial key.
105 
106     Each element of the partial key is matched against the keys of the dictionary and
107     if a partial match is found the value of the key is returned.
108 
109     Even a partial match works here i.e even if the key matches partially a value is returned.
110 
111     Parameters
112     ----------
113     key : `tuple`
114           A tuple containing the partial key.
115     dictionary: `dict`
116           The target dictionary from which we want to retrieve the value based on the partial key.
117 
118     Yields
119     ------
120     value
121         The value of the matched key.
122 
123     References
124     ----------
125     * https://stackoverflow.com/questions/18893624/partial-match-dictionary-keyof-tuples-in-python
126 
127     Examples
128     ----------
129     >>> d = {('abc','def','ghi') : 1, ('abc', 'def', 'xyz') : 2, ('pqr', 'lmn', 'tuv') : 3}
130     >>> list(partial_key_match(('abc', 'def', None), d))
131         [1, 2]
132     """
133     for k, v in dictionary.items():
134         if all(k1 == k2 or k2 is None for k1, k2 in zip(k, key)):
135             yield v
136 
137 
138 def dict_keys_same(list_of_dicts):
139     """
140     Makes sure that a list of dictionaries all have the same keys.
141 
142     If a key is missing, it will be added but with a value of None.
143 
144     Parameters
145     ----------
146     list_of_dicts : `list` of `dict`
147           A list containing each dictonary to parse.
148 
149     Returns
150     ------
151     `list`
152         The list with each dict updated.
153 
154     References
155     ----------
156     * https://stackoverflow.com/questions/10482439/make-sure-all-dicts-in-a-list-have-the-same-keys
157 
158     Examples
159     ----------
160     >>> l = [{'x': 42}, {'x': 23, 'y': 5}]
161     >>> dict_keys_same(l)
162         [{'x': 42, 'y': None}, {'x': 23, 'y': 5}]
163     """
164     all_keys = set(chain.from_iterable(list_of_dicts))
165     for d in list_of_dicts:
166         d.update({key: None for key in all_keys if key not in d})
167     return list_of_dicts
168 
169 
170 def hash_file(path):
171     """
172     Returns the SHA-256 hash of a file.
173 
174     Parameters
175     ----------
176     path: `str`
177         The path of the file to be hashed.
178 
179     Returns
180     -------
181     `str`
182         SHA-256 hash of the file.
183 
184     References
185     ----------
186     * https://stackoverflow.com/a/22058673
187     """
188     BUF_SIZE = 65536
189     sha256 = hashlib.sha256()
190 
191     with open(path, 'rb') as f:
192         while True:
193             data = f.read(BUF_SIZE)
194             if not data:
195                 break
196             sha256.update(data)
197 
198     return sha256.hexdigest()
199 
200 
201 def get_width():
202     """
203     Gets the width of the current terminal.
204     Accounts for if the 'COLUMNS' environmental variable is set.
205 
206     Returns
207     -------
208     `int`
209         Width of the terminal you are in.
210         Works for IPython notebooks and normal terminals.
211     """
212     width = os.environ.get("COLUMNS", None)
213     if width:
214         width = int(width)
215     else:
216         width, _ = get_terminal_size()
217     return width
218
```

Path: `sunpy/data/data_manager/cache.py`
Content:
```
1 import os
2 from pathlib import Path
3 from datetime import datetime
4 from warnings import warn
5 from urllib.request import urlopen
6 
7 import astropy.units as u
8 from astropy.time import TimeDelta
9 
10 from sunpy.time import parse_time
11 from sunpy.util.exceptions import SunpyUserWarning
12 from sunpy.util.net import get_filename
13 from sunpy.util.util import hash_file, replacement_filename
14 
15 __all__ = ['Cache']
16 
17 
18 class Cache:
19     """
20     Cache provides a way to download and cache files.
21 
22     Parameters
23     ----------
24     downloader: Implementation of `~sunpy.data.data_manager.downloader.DownloaderBase`
25         Downloader object for downloading remote files.
26     storage: Implementation of `~sunpy.data.data_manager.storage.StorageProviderBase`
27         Storage to store metadata about the files.
28     cache_dir: `str` or `pathlib.Path`
29         Directory where the downloaded files will be stored.
30     expiry: `astropy.units.quantity.Quantity` or `None`, optional
31         The interval after which the cache is invalidated. If the expiry is `None`,
32         then the expiry is not checked. Defaults to 10 days.
33     """
34 
35     def __init__(self, downloader, storage, cache_dir, expiry=10*u.day):
36         self._downloader = downloader
37         self._storage = storage
38         self._cache_dir = Path(cache_dir)
39         self._expiry = expiry if expiry is None else TimeDelta(expiry)
40 
41     def download(self, urls, redownload=False):
42         """
43         Downloads the files from the urls.
44 
45         The overall flow of this function is:
46             1. If ``redownload``: Download, update cache and return file path.
47             2. If not ``redownload``: Check cache,
48                 i. If present in cache:
49                     - If cache has expired, remove the entry from cache, download and add to cache
50                     - If cache has not expired, return the path
51 
52         Parameters
53         ----------
54         urls: `list` or `str`
55             A list of urls or a single url.
56         redownload: `bool`
57             Whether to skip cache and redownload.
58 
59         Returns
60         -------
61         `pathlib.PosixPath`
62             Path to the downloaded file.
63         """
64         if isinstance(urls, str):
65             urls = [urls]
66         # Program flow
67         # 1. If redownload: Download, update cache and return file path
68         # 2. If not redownload: Check cache,
69         #    i. If present in cache:
70         #        - If cache expired, remove entry from cache, download and add to cache
71         #        - If cache not expired, return path
72         details = None
73         for url in urls:
74             details = self._get_by_url(url)
75             if details:
76                 break
77         if details:
78             if redownload or self._has_expired(details):
79                 # if file is in cache and it has to be redownloaded or the cache has expired
80                 # then remove the file and delete the details from the storage
81                 os.remove(details['file_path'])
82                 self._storage.delete_by_key('url', details['url'])
83             else:
84                 return Path(details['file_path'])
85 
86         file_path, file_hash, url = self._download_and_hash(urls)
87 
88         self._storage.store({
89             'file_hash': file_hash,
90             'file_path': str(file_path),
91             'url': url,
92             'time': datetime.now().isoformat(),
93         })
94         return file_path
95 
96     def _has_expired(self, details):
97         """
98         Whether the url corresponding to details in cache has expired or not.
99 
100         Parameters
101         ----------
102         details: `dict`
103             Details detached from cache.
104 
105         Returns
106         -------
107         `bool`
108             Whether the url has expired or not.
109         """
110         time = details.get("time", datetime.now().isoformat())
111 
112         # TODO: Remove this once we depend on Python >=3.7
113         if hasattr(datetime, "fromisoformat"):
114             time = datetime.fromisoformat(time)
115         else:
116             time = parse_time(time).datetime
117         return self._expiry and datetime.now() - time > self._expiry
118 
119     def get_by_hash(self, sha_hash):
120         """
121         Returns the details which is matched by hash if present in cache.
122 
123         Parameters
124         ----------
125         sha_hash: `str`
126             SHA-256 hash of the file.
127         """
128         details = self._storage.find_by_key('file_hash', sha_hash)
129         return details
130 
131     def _get_by_url(self, url):
132         """
133         Returns the details which is matched by url if present in cache.
134 
135         Parameters
136         ----------
137         url: `str`
138             URL of the file.
139         """
140         details = self._storage.find_by_key('url', url)
141         return details
142 
143     def _download_and_hash(self, urls):
144         """
145         Downloads the file and returns the path, hash and url it used to download.
146 
147         Parameters
148         ----------
149         urls: `list`
150             List of urls.
151 
152         Returns
153         -------
154         `str`, `str`, `str`
155             Path, hash and URL of the file.
156         """
157         def download(url):
158             path = self._cache_dir / get_filename(urlopen(url), url)
159             # replacement_filename returns a string and we want a Path object
160             path = Path(replacement_filename(path))
161             self._downloader.download(url, path)
162             shahash = hash_file(path)
163             return path, shahash, url
164 
165         for url in urls:
166             try:
167                 return download(url)
168             except Exception as e:
169                 warn(e, SunpyUserWarning)
170                 error = e
171         else:
172             raise RuntimeError from error
173
```
--- 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/sunpy/data/data_manager/cache.py b/sunpy/data/data_manager/cache.py
--- a/sunpy/data/data_manager/cache.py
+++ b/sunpy/data/data_manager/cache.py
@@ -157,16 +157,17 @@
         def download(url):
             path = self._cache_dir / get_filename(urlopen(url), url)
             # replacement_filename returns a string and we want a Path object
-            path = Path(replacement_filename(path))
+            path = Path(replacement_filename(str(path)))
             self._downloader.download(url, path)
             shahash = hash_file(path)
             return path, shahash, url
 
+        errors = []
         for url in urls:
             try:
                 return download(url)
             except Exception as e:
-                warn(e, SunpyUserWarning)
-                error = e
+                warn(f"{e}", SunpyUserWarning)
+                errors.append(f"{e}")
         else:
-            raise RuntimeError from error
+            raise RuntimeError(errors)
diff --git a/sunpy/util/util.py b/sunpy/util/util.py
--- a/sunpy/util/util.py
+++ b/sunpy/util/util.py
@@ -41,7 +41,7 @@
                 items.add(x)
 
 
-def replacement_filename(path):
+def replacement_filename(path: str) -> str:
     """
     Return a replacement path if input path is currently in use.

{"golden_diff": "diff --git a/sunpy/data/data_manager/cache.py b/sunpy/data/data_manager/cache.py\n--- a/sunpy/data/data_manager/cache.py\n+++ b/sunpy/data/data_manager/cache.py\n@@ -157,16 +157,17 @@\n         def download(url):\n             path = self._cache_dir / get_filename(urlopen(url), url)\n             # replacement_filename returns a string and we want a Path object\n-            path = Path(replacement_filename(path))\n+            path = Path(replacement_filename(str(path)))\n             self._downloader.download(url, path)\n             shahash = hash_file(path)\n             return path, shahash, url\n \n+        errors = []\n         for url in urls:\n             try:\n                 return download(url)\n             except Exception as e:\n-                warn(e, SunpyUserWarning)\n-                error = e\n+                warn(f\"{e}\", SunpyUserWarning)\n+                errors.append(f\"{e}\")\n         else:\n-            raise RuntimeError from error\n+            raise RuntimeError(errors)\ndiff --git a/sunpy/util/util.py b/sunpy/util/util.py\n--- a/sunpy/util/util.py\n+++ b/sunpy/util/util.py\n@@ -41,7 +41,7 @@\n                 items.add(x)\n \n \n-def replacement_filename(path):\n+def replacement_filename(path: str) -> str:\n     \"\"\"\n     Return a replacement path if input path is currently in use.\n", "issue": "32 bit tests failing\nThe 32 bit tests are currently failing, due to a failed attempt to install the `cryptography` library from source. See e.g. https://dev.azure.com/sunpy/sunpy/_build/results?buildId=9154&view=logs&jobId=a7b3aa55-7d57-562f-3433-7f6b2d4252da&j=2920fa54-56c6-5dfd-d3d8-7b1b694d90cb&t=bba4d609-c264-5f9c-e072-020a8a3b573f\n", "before_files": [{"content": "\"\"\"\nThis module provides general utility functions.\n\"\"\"\nimport os\nimport hashlib\nfrom shutil import get_terminal_size\nfrom itertools import chain, count\nfrom collections import UserList\n\n__all__ = ['unique', 'replacement_filename', 'expand_list',\n           'expand_list_generator', 'dict_keys_same', 'hash_file', \"get_width\"]\n\n\ndef unique(itr, key=None):\n    \"\"\"\n    Return only unique elements of a sequence.\n\n    Parameters\n    ----------\n    itr : `iterable`\n        Any iterable sequence.\n    key : `function`, optional\n        A function to apply to each element in the iterable. Defaults to `None`.\n\n    Returns\n    -------\n    `set`:\n        A `set` of each unique element.\n    \"\"\"\n    items = set()\n    if key is None:\n        for elem in itr:\n            if elem not in items:\n                yield elem\n                items.add(elem)\n    else:\n        for elem in itr:\n            x = key(elem)\n            if x not in items:\n                yield elem\n                items.add(x)\n\n\ndef replacement_filename(path):\n    \"\"\"\n    Return a replacement path if input path is currently in use.\n\n    Enumerates until an unused filename is found, e.g., \"foo.fits\" becomes\n    \"foo.0.fits\", if that is used, \"foo.1.fits\" and so on.\n\n    Parameters\n    ----------\n    path : `str`\n        A string path.\n\n    Returns\n    -------\n    `str`:\n        A string path.\n    \"\"\"\n    if not os.path.exists(path):\n        return path\n    else:\n        dir_, filename = os.path.split(path)\n        base, ext = os.path.splitext(filename)\n        for c in count():\n            name = base + '.' + str(c) + ext\n            newpath = os.path.join(dir_, name)\n            if not os.path.exists(newpath):\n                return newpath\n\n\ndef expand_list(inp):\n    \"\"\"\n    Expand a list of lists or tuples.\n\n    Parameters\n    ----------\n    inp : `list`, `tuple`, `collections.UserList`\n        The iterable to expand.\n\n    Returns\n    -------\n    `list`\n        A flat list consisting of the entries of the input.\n\n    References\n    ----------\n    * https://stackoverflow.com/questions/2185822/expanding-elements-in-a-list/2185971#2185971\n    \"\"\"\n    return [item for item in expand_list_generator(inp)]\n\n\ndef expand_list_generator(inp):\n    for item in inp:\n        if isinstance(item, (tuple, list, UserList)):\n            yield from expand_list_generator(item)\n        else:\n            yield item\n\n\ndef partial_key_match(key, dictionary):\n    \"\"\"\n    Return value/values from a dictionary based on a partial key.\n\n    Each element of the partial key is matched against the keys of the dictionary and\n    if a partial match is found the value of the key is returned.\n\n    Even a partial match works here i.e even if the key matches partially a value is returned.\n\n    Parameters\n    ----------\n    key : `tuple`\n          A tuple containing the partial key.\n    dictionary: `dict`\n          The target dictionary from which we want to retrieve the value based on the partial key.\n\n    Yields\n    ------\n    value\n        The value of the matched key.\n\n    References\n    ----------\n    * https://stackoverflow.com/questions/18893624/partial-match-dictionary-keyof-tuples-in-python\n\n    Examples\n    ----------\n    >>> d = {('abc','def','ghi') : 1, ('abc', 'def', 'xyz') : 2, ('pqr', 'lmn', 'tuv') : 3}\n    >>> list(partial_key_match(('abc', 'def', None), d))\n        [1, 2]\n    \"\"\"\n    for k, v in dictionary.items():\n        if all(k1 == k2 or k2 is None for k1, k2 in zip(k, key)):\n            yield v\n\n\ndef dict_keys_same(list_of_dicts):\n    \"\"\"\n    Makes sure that a list of dictionaries all have the same keys.\n\n    If a key is missing, it will be added but with a value of None.\n\n    Parameters\n    ----------\n    list_of_dicts : `list` of `dict`\n          A list containing each dictonary to parse.\n\n    Returns\n    ------\n    `list`\n        The list with each dict updated.\n\n    References\n    ----------\n    * https://stackoverflow.com/questions/10482439/make-sure-all-dicts-in-a-list-have-the-same-keys\n\n    Examples\n    ----------\n    >>> l = [{'x': 42}, {'x': 23, 'y': 5}]\n    >>> dict_keys_same(l)\n        [{'x': 42, 'y': None}, {'x': 23, 'y': 5}]\n    \"\"\"\n    all_keys = set(chain.from_iterable(list_of_dicts))\n    for d in list_of_dicts:\n        d.update({key: None for key in all_keys if key not in d})\n    return list_of_dicts\n\n\ndef hash_file(path):\n    \"\"\"\n    Returns the SHA-256 hash of a file.\n\n    Parameters\n    ----------\n    path: `str`\n        The path of the file to be hashed.\n\n    Returns\n    -------\n    `str`\n        SHA-256 hash of the file.\n\n    References\n    ----------\n    * https://stackoverflow.com/a/22058673\n    \"\"\"\n    BUF_SIZE = 65536\n    sha256 = hashlib.sha256()\n\n    with open(path, 'rb') as f:\n        while True:\n            data = f.read(BUF_SIZE)\n            if not data:\n                break\n            sha256.update(data)\n\n    return sha256.hexdigest()\n\n\ndef get_width():\n    \"\"\"\n    Gets the width of the current terminal.\n    Accounts for if the 'COLUMNS' environmental variable is set.\n\n    Returns\n    -------\n    `int`\n        Width of the terminal you are in.\n        Works for IPython notebooks and normal terminals.\n    \"\"\"\n    width = os.environ.get(\"COLUMNS\", None)\n    if width:\n        width = int(width)\n    else:\n        width, _ = get_terminal_size()\n    return width\n", "path": "sunpy/util/util.py"}, {"content": "import os\nfrom pathlib import Path\nfrom datetime import datetime\nfrom warnings import warn\nfrom urllib.request import urlopen\n\nimport astropy.units as u\nfrom astropy.time import TimeDelta\n\nfrom sunpy.time import parse_time\nfrom sunpy.util.exceptions import SunpyUserWarning\nfrom sunpy.util.net import get_filename\nfrom sunpy.util.util import hash_file, replacement_filename\n\n__all__ = ['Cache']\n\n\nclass Cache:\n    \"\"\"\n    Cache provides a way to download and cache files.\n\n    Parameters\n    ----------\n    downloader: Implementation of `~sunpy.data.data_manager.downloader.DownloaderBase`\n        Downloader object for downloading remote files.\n    storage: Implementation of `~sunpy.data.data_manager.storage.StorageProviderBase`\n        Storage to store metadata about the files.\n    cache_dir: `str` or `pathlib.Path`\n        Directory where the downloaded files will be stored.\n    expiry: `astropy.units.quantity.Quantity` or `None`, optional\n        The interval after which the cache is invalidated. If the expiry is `None`,\n        then the expiry is not checked. Defaults to 10 days.\n    \"\"\"\n\n    def __init__(self, downloader, storage, cache_dir, expiry=10*u.day):\n        self._downloader = downloader\n        self._storage = storage\n        self._cache_dir = Path(cache_dir)\n        self._expiry = expiry if expiry is None else TimeDelta(expiry)\n\n    def download(self, urls, redownload=False):\n        \"\"\"\n        Downloads the files from the urls.\n\n        The overall flow of this function is:\n            1. If ``redownload``: Download, update cache and return file path.\n            2. If not ``redownload``: Check cache,\n                i. If present in cache:\n                    - If cache has expired, remove the entry from cache, download and add to cache\n                    - If cache has not expired, return the path\n\n        Parameters\n        ----------\n        urls: `list` or `str`\n            A list of urls or a single url.\n        redownload: `bool`\n            Whether to skip cache and redownload.\n\n        Returns\n        -------\n        `pathlib.PosixPath`\n            Path to the downloaded file.\n        \"\"\"\n        if isinstance(urls, str):\n            urls = [urls]\n        # Program flow\n        # 1. If redownload: Download, update cache and return file path\n        # 2. If not redownload: Check cache,\n        #    i. If present in cache:\n        #        - If cache expired, remove entry from cache, download and add to cache\n        #        - If cache not expired, return path\n        details = None\n        for url in urls:\n            details = self._get_by_url(url)\n            if details:\n                break\n        if details:\n            if redownload or self._has_expired(details):\n                # if file is in cache and it has to be redownloaded or the cache has expired\n                # then remove the file and delete the details from the storage\n                os.remove(details['file_path'])\n                self._storage.delete_by_key('url', details['url'])\n            else:\n                return Path(details['file_path'])\n\n        file_path, file_hash, url = self._download_and_hash(urls)\n\n        self._storage.store({\n            'file_hash': file_hash,\n            'file_path': str(file_path),\n            'url': url,\n            'time': datetime.now().isoformat(),\n        })\n        return file_path\n\n    def _has_expired(self, details):\n        \"\"\"\n        Whether the url corresponding to details in cache has expired or not.\n\n        Parameters\n        ----------\n        details: `dict`\n            Details detached from cache.\n\n        Returns\n        -------\n        `bool`\n            Whether the url has expired or not.\n        \"\"\"\n        time = details.get(\"time\", datetime.now().isoformat())\n\n        # TODO: Remove this once we depend on Python >=3.7\n        if hasattr(datetime, \"fromisoformat\"):\n            time = datetime.fromisoformat(time)\n        else:\n            time = parse_time(time).datetime\n        return self._expiry and datetime.now() - time > self._expiry\n\n    def get_by_hash(self, sha_hash):\n        \"\"\"\n        Returns the details which is matched by hash if present in cache.\n\n        Parameters\n        ----------\n        sha_hash: `str`\n            SHA-256 hash of the file.\n        \"\"\"\n        details = self._storage.find_by_key('file_hash', sha_hash)\n        return details\n\n    def _get_by_url(self, url):\n        \"\"\"\n        Returns the details which is matched by url if present in cache.\n\n        Parameters\n        ----------\n        url: `str`\n            URL of the file.\n        \"\"\"\n        details = self._storage.find_by_key('url', url)\n        return details\n\n    def _download_and_hash(self, urls):\n        \"\"\"\n        Downloads the file and returns the path, hash and url it used to download.\n\n        Parameters\n        ----------\n        urls: `list`\n            List of urls.\n\n        Returns\n        -------\n        `str`, `str`, `str`\n            Path, hash and URL of the file.\n        \"\"\"\n        def download(url):\n            path = self._cache_dir / get_filename(urlopen(url), url)\n            # replacement_filename returns a string and we want a Path object\n            path = Path(replacement_filename(path))\n            self._downloader.download(url, path)\n            shahash = hash_file(path)\n            return path, shahash, url\n\n        for url in urls:\n            try:\n                return download(url)\n            except Exception as e:\n                warn(e, SunpyUserWarning)\n                error = e\n        else:\n            raise RuntimeError from error\n", "path": "sunpy/data/data_manager/cache.py"}], "after_files": [{"content": "\"\"\"\nThis module provides general utility functions.\n\"\"\"\nimport os\nimport hashlib\nfrom shutil import get_terminal_size\nfrom itertools import chain, count\nfrom collections import UserList\n\n__all__ = ['unique', 'replacement_filename', 'expand_list',\n           'expand_list_generator', 'dict_keys_same', 'hash_file', \"get_width\"]\n\n\ndef unique(itr, key=None):\n    \"\"\"\n    Return only unique elements of a sequence.\n\n    Parameters\n    ----------\n    itr : `iterable`\n        Any iterable sequence.\n    key : `function`, optional\n        A function to apply to each element in the iterable. Defaults to `None`.\n\n    Returns\n    -------\n    `set`:\n        A `set` of each unique element.\n    \"\"\"\n    items = set()\n    if key is None:\n        for elem in itr:\n            if elem not in items:\n                yield elem\n                items.add(elem)\n    else:\n        for elem in itr:\n            x = key(elem)\n            if x not in items:\n                yield elem\n                items.add(x)\n\n\ndef replacement_filename(path: str) -> str:\n    \"\"\"\n    Return a replacement path if input path is currently in use.\n\n    Enumerates until an unused filename is found, e.g., \"foo.fits\" becomes\n    \"foo.0.fits\", if that is used, \"foo.1.fits\" and so on.\n\n    Parameters\n    ----------\n    path : `str`\n        A string path.\n\n    Returns\n    -------\n    `str`:\n        A string path.\n    \"\"\"\n    if not os.path.exists(path):\n        return path\n    else:\n        dir_, filename = os.path.split(path)\n        base, ext = os.path.splitext(filename)\n        for c in count():\n            name = base + '.' + str(c) + ext\n            newpath = os.path.join(dir_, name)\n            if not os.path.exists(newpath):\n                return newpath\n\n\ndef expand_list(inp):\n    \"\"\"\n    Expand a list of lists or tuples.\n\n    Parameters\n    ----------\n    inp : `list`, `tuple`, `collections.UserList`\n        The iterable to expand.\n\n    Returns\n    -------\n    `list`\n        A flat list consisting of the entries of the input.\n\n    References\n    ----------\n    * https://stackoverflow.com/questions/2185822/expanding-elements-in-a-list/2185971#2185971\n    \"\"\"\n    return [item for item in expand_list_generator(inp)]\n\n\ndef expand_list_generator(inp):\n    for item in inp:\n        if isinstance(item, (tuple, list, UserList)):\n            yield from expand_list_generator(item)\n        else:\n            yield item\n\n\ndef partial_key_match(key, dictionary):\n    \"\"\"\n    Return value/values from a dictionary based on a partial key.\n\n    Each element of the partial key is matched against the keys of the dictionary and\n    if a partial match is found the value of the key is returned.\n\n    Even a partial match works here i.e even if the key matches partially a value is returned.\n\n    Parameters\n    ----------\n    key : `tuple`\n          A tuple containing the partial key.\n    dictionary: `dict`\n          The target dictionary from which we want to retrieve the value based on the partial key.\n\n    Yields\n    ------\n    value\n        The value of the matched key.\n\n    References\n    ----------\n    * https://stackoverflow.com/questions/18893624/partial-match-dictionary-keyof-tuples-in-python\n\n    Examples\n    ----------\n    >>> d = {('abc','def','ghi') : 1, ('abc', 'def', 'xyz') : 2, ('pqr', 'lmn', 'tuv') : 3}\n    >>> list(partial_key_match(('abc', 'def', None), d))\n        [1, 2]\n    \"\"\"\n    for k, v in dictionary.items():\n        if all(k1 == k2 or k2 is None for k1, k2 in zip(k, key)):\n            yield v\n\n\ndef dict_keys_same(list_of_dicts):\n    \"\"\"\n    Makes sure that a list of dictionaries all have the same keys.\n\n    If a key is missing, it will be added but with a value of None.\n\n    Parameters\n    ----------\n    list_of_dicts : `list` of `dict`\n          A list containing each dictonary to parse.\n\n    Returns\n    ------\n    `list`\n        The list with each dict updated.\n\n    References\n    ----------\n    * https://stackoverflow.com/questions/10482439/make-sure-all-dicts-in-a-list-have-the-same-keys\n\n    Examples\n    ----------\n    >>> l = [{'x': 42}, {'x': 23, 'y': 5}]\n    >>> dict_keys_same(l)\n        [{'x': 42, 'y': None}, {'x': 23, 'y': 5}]\n    \"\"\"\n    all_keys = set(chain.from_iterable(list_of_dicts))\n    for d in list_of_dicts:\n        d.update({key: None for key in all_keys if key not in d})\n    return list_of_dicts\n\n\ndef hash_file(path):\n    \"\"\"\n    Returns the SHA-256 hash of a file.\n\n    Parameters\n    ----------\n    path: `str`\n        The path of the file to be hashed.\n\n    Returns\n    -------\n    `str`\n        SHA-256 hash of the file.\n\n    References\n    ----------\n    * https://stackoverflow.com/a/22058673\n    \"\"\"\n    BUF_SIZE = 65536\n    sha256 = hashlib.sha256()\n\n    with open(path, 'rb') as f:\n        while True:\n            data = f.read(BUF_SIZE)\n            if not data:\n                break\n            sha256.update(data)\n\n    return sha256.hexdigest()\n\n\ndef get_width():\n    \"\"\"\n    Gets the width of the current terminal.\n    Accounts for if the 'COLUMNS' environmental variable is set.\n\n    Returns\n    -------\n    `int`\n        Width of the terminal you are in.\n        Works for IPython notebooks and normal terminals.\n    \"\"\"\n    width = os.environ.get(\"COLUMNS\", None)\n    if width:\n        width = int(width)\n    else:\n        width, _ = get_terminal_size()\n    return width\n", "path": "sunpy/util/util.py"}, {"content": "import os\nfrom pathlib import Path\nfrom datetime import datetime\nfrom warnings import warn\nfrom urllib.request import urlopen\n\nimport astropy.units as u\nfrom astropy.time import TimeDelta\n\nfrom sunpy.time import parse_time\nfrom sunpy.util.exceptions import SunpyUserWarning\nfrom sunpy.util.net import get_filename\nfrom sunpy.util.util import hash_file, replacement_filename\n\n__all__ = ['Cache']\n\n\nclass Cache:\n    \"\"\"\n    Cache provides a way to download and cache files.\n\n    Parameters\n    ----------\n    downloader: Implementation of `~sunpy.data.data_manager.downloader.DownloaderBase`\n        Downloader object for downloading remote files.\n    storage: Implementation of `~sunpy.data.data_manager.storage.StorageProviderBase`\n        Storage to store metadata about the files.\n    cache_dir: `str` or `pathlib.Path`\n        Directory where the downloaded files will be stored.\n    expiry: `astropy.units.quantity.Quantity` or `None`, optional\n        The interval after which the cache is invalidated. If the expiry is `None`,\n        then the expiry is not checked. Defaults to 10 days.\n    \"\"\"\n\n    def __init__(self, downloader, storage, cache_dir, expiry=10*u.day):\n        self._downloader = downloader\n        self._storage = storage\n        self._cache_dir = Path(cache_dir)\n        self._expiry = expiry if expiry is None else TimeDelta(expiry)\n\n    def download(self, urls, redownload=False):\n        \"\"\"\n        Downloads the files from the urls.\n\n        The overall flow of this function is:\n            1. If ``redownload``: Download, update cache and return file path.\n            2. If not ``redownload``: Check cache,\n                i. If present in cache:\n                    - If cache has expired, remove the entry from cache, download and add to cache\n                    - If cache has not expired, return the path\n\n        Parameters\n        ----------\n        urls: `list` or `str`\n            A list of urls or a single url.\n        redownload: `bool`\n            Whether to skip cache and redownload.\n\n        Returns\n        -------\n        `pathlib.PosixPath`\n            Path to the downloaded file.\n        \"\"\"\n        if isinstance(urls, str):\n            urls = [urls]\n        # Program flow\n        # 1. If redownload: Download, update cache and return file path\n        # 2. If not redownload: Check cache,\n        #    i. If present in cache:\n        #        - If cache expired, remove entry from cache, download and add to cache\n        #        - If cache not expired, return path\n        details = None\n        for url in urls:\n            details = self._get_by_url(url)\n            if details:\n                break\n        if details:\n            if redownload or self._has_expired(details):\n                # if file is in cache and it has to be redownloaded or the cache has expired\n                # then remove the file and delete the details from the storage\n                os.remove(details['file_path'])\n                self._storage.delete_by_key('url', details['url'])\n            else:\n                return Path(details['file_path'])\n\n        file_path, file_hash, url = self._download_and_hash(urls)\n\n        self._storage.store({\n            'file_hash': file_hash,\n            'file_path': str(file_path),\n            'url': url,\n            'time': datetime.now().isoformat(),\n        })\n        return file_path\n\n    def _has_expired(self, details):\n        \"\"\"\n        Whether the url corresponding to details in cache has expired or not.\n\n        Parameters\n        ----------\n        details: `dict`\n            Details detached from cache.\n\n        Returns\n        -------\n        `bool`\n            Whether the url has expired or not.\n        \"\"\"\n        time = details.get(\"time\", datetime.now().isoformat())\n\n        # TODO: Remove this once we depend on Python >=3.7\n        if hasattr(datetime, \"fromisoformat\"):\n            time = datetime.fromisoformat(time)\n        else:\n            time = parse_time(time).datetime\n        return self._expiry and datetime.now() - time > self._expiry\n\n    def get_by_hash(self, sha_hash):\n        \"\"\"\n        Returns the details which is matched by hash if present in cache.\n\n        Parameters\n        ----------\n        sha_hash: `str`\n            SHA-256 hash of the file.\n        \"\"\"\n        details = self._storage.find_by_key('file_hash', sha_hash)\n        return details\n\n    def _get_by_url(self, url):\n        \"\"\"\n        Returns the details which is matched by url if present in cache.\n\n        Parameters\n        ----------\n        url: `str`\n            URL of the file.\n        \"\"\"\n        details = self._storage.find_by_key('url', url)\n        return details\n\n    def _download_and_hash(self, urls):\n        \"\"\"\n        Downloads the file and returns the path, hash and url it used to download.\n\n        Parameters\n        ----------\n        urls: `list`\n            List of urls.\n\n        Returns\n        -------\n        `str`, `str`, `str`\n            Path, hash and URL of the file.\n        \"\"\"\n        def download(url):\n            path = self._cache_dir / get_filename(urlopen(url), url)\n            # replacement_filename returns a string and we want a Path object\n            path = Path(replacement_filename(str(path)))\n            self._downloader.download(url, path)\n            shahash = hash_file(path)\n            return path, shahash, url\n\n        errors = []\n        for url in urls:\n            try:\n                return download(url)\n            except Exception as e:\n                warn(f\"{e}\", SunpyUserWarning)\n                errors.append(f\"{e}\")\n        else:\n            raise RuntimeError(errors)\n", "path": "sunpy/data/data_manager/cache.py"}]}