Repository: Cadair/parfive

Issue:
Make running download from an async context public
If you want to `await` the download you have to use `Downloader._run_download` and it's a little clunky. We should probably allow this as first class public API.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/Cadair/parfive/pull/54?src=pr&el=h1) Report
> Merging [#54](https://codecov.io/gh/Cadair/parfive/pull/54?src=pr&el=desc) into [master](https://codecov.io/gh/Cadair/parfive/commit/f846b1dfd72add2b4d1b2c9c06398e78ab3ac804?el=desc) will **decrease** coverage by `0.11%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/Cadair/parfive/pull/54/graphs/tree.svg?width=650&height=150&src=pr&token=QZ37DxYRPs)](https://codecov.io/gh/Cadair/parfive/pull/54?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master      #54      +/-   ##
==========================================
- Coverage   84.83%   84.71%   -0.12%     
==========================================
  Files           4        4              
  Lines         389      386       -3     
==========================================
- Hits          330      327       -3     
  Misses         59       59              
```


| [Impacted Files](https://codecov.io/gh/Cadair/parfive/pull/54?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [parfive/downloader.py](https://codecov.io/gh/Cadair/parfive/pull/54/diff?src=pr&el=tree#diff-cGFyZml2ZS9kb3dubG9hZGVyLnB5) | `91.20% <100.00%> (-0.13%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Cadair/parfive/pull/54?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Cadair/parfive/pull/54?src=pr&el=footer). Last update [f846b1d...721dfc1](https://codecov.io/gh/Cadair/parfive/pull/54?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.codecov.yaml b/.codecov.yaml
new file mode 100644
index 0000000..914f860
--- /dev/null
+++ b/.codecov.yaml
@@ -0,0 +1,5 @@
+coverage:
+  status:
+    project:
+      default:
+        threshold: 0.2%
diff --git a/changelog/54.feature.rst b/changelog/54.feature.rst
new file mode 100644
index 0000000..172ca48
--- /dev/null
+++ b/changelog/54.feature.rst
@@ -0,0 +1,3 @@
+Make `parfive.Downloader.run_download` public API and move all of the
+`~parfive.Downloader.download` logic into ``run_download``. This enabled using
+parfive from inside an async context.
diff --git a/parfive/downloader.py b/parfive/downloader.py
index baf1cbf..67ab952 100644
--- a/parfive/downloader.py
+++ b/parfive/downloader.py
@@ -42,7 +42,6 @@ class Downloader:
     ----------
     max_conn : `int`, optional
         The number of parallel download slots.
-
     progress : `bool`, optional
         If `True` show a main progress bar showing how many of the total files
         have been downloaded. If `False`, no progress bars will be shown at all.
@@ -51,7 +50,6 @@ class Downloader:
         detailing the progress of each individual file being downloaded.
     loop : `asyncio.AbstractEventLoop`, optional
         No longer used, and will be removed in a future release.
-
     notebook : `bool`, optional
         If `True` tqdm will be used in notebook mode. If `None` an attempt will
         be made to detect the notebook and guess which progress bar to use.
@@ -194,7 +192,7 @@ class Downloader:
 
         return asyncio.run(coro)
 
-    def download(self, timeouts=None):
+    async def run_download(self, timeouts=None):
         """
         Download all files in the queue.
 
@@ -219,8 +217,17 @@ class Downloader:
         """
         timeouts = timeouts or {"total": os.environ.get("PARFIVE_TOTAL_TIMEOUT", 5 * 60),
                                 "sock_read": os.environ.get("PARFIVE_SOCK_READ_TIMEOUT", 90)}
-        future = self._run_in_loop(self._run_download(timeouts))
-        dl_results = future.result()
+
+        total_files = self.queued_downloads
+
+        done = set()
+        with self._get_main_pb(total_files) as main_pb:
+            if len(self.http_queue):
+                done.update(await self._run_http_download(main_pb, timeouts))
+            if len(self.ftp_queue):
+                done.update(await self._run_ftp_download(main_pb, timeouts))
+
+        dl_results = await asyncio.gather(*done, return_exceptions=True)
 
         results = Results()
 
@@ -236,6 +243,35 @@ class Downloader:
 
         return results
 
+    def download(self, timeouts=None):
+        """
+        Download all files in the queue.
+
+        Parameters
+        ----------
+        timeouts : `dict`, optional
+            Overrides for the default timeouts for http downloads. Supported
+            keys are any accepted by the `aiohttp.ClientTimeout` class. Defaults
+            to 5 minutes for total session timeout and 90 seconds for socket
+            read timeout.
+
+        Returns
+        -------
+        `parfive.Results`
+            A list of files downloaded.
+
+        Notes
+        -----
+        This is a synchronous version of `~parfive.Downloader.run_download`, an
+        `asyncio` event loop will be created to run the download (in it's own
+        thread if a loop is already running).
+
+        The defaults for the `'total'` and `'sock_read'` timeouts can be
+        overridden by two environment variables ``PARFIVE_TOTAL_TIMEOUT`` and
+        ``PARFIVE_SOCK_READ_TIMEOUT``.
+        """
+        return self._run_in_loop(self.run_download(timeouts))
+
     def retry(self, results):
         """
         Retry any failed downloads in a results object.
@@ -281,28 +317,6 @@ class Downloader:
         else:
             return contextlib.contextmanager(lambda: iter([None]))()
 
-    async def _run_download(self, timeouts):
-        """
-        Download all files in the queue.
-
-        Returns
-        -------
-        `parfive.Results`
-            A list of filenames which successfully downloaded. This list also
-            has an attribute ``errors`` which lists any failed urls and their
-            error.
-        """
-        total_files = self.queued_downloads
-        done = set()
-        with self._get_main_pb(total_files) as main_pb:
-            if len(self.http_queue):
-                done.update(await self._run_http_download(main_pb, timeouts))
-            if len(self.ftp_queue):
-                done.update(await self._run_ftp_download(main_pb, timeouts))
-
-        # Return one future to represent all the results.
-        return asyncio.gather(*done, return_exceptions=True)
-
     async def _run_http_download(self, main_pb, timeouts):
         async with aiohttp.ClientSession(headers=self.headers) as session:
             futures = await self._run_from_queue(


Test patch:
diff --git a/parfive/tests/test_downloader.py b/parfive/tests/test_downloader.py
index 2957920..6c01a32 100644
--- a/parfive/tests/test_downloader.py
+++ b/parfive/tests/test_downloader.py
@@ -45,6 +45,23 @@ def test_download(httpserver, tmpdir):
     assert sha256sum(f[0]) == "a1c58cd340e3bd33f94524076f1fa5cf9a7f13c59d5272a9d4bc0b5bc436d9b3"
 
 
+@pytest.mark.asyncio
+async def test_async_download(httpserver, tmpdir):
+    httpserver.serve_content('SIMPLE  = T',
+                             headers={'Content-Disposition': "attachment; filename=testfile.fits"})
+    dl = Downloader()
+
+    dl.enqueue_file(httpserver.url, path=Path(tmpdir), max_splits=None)
+
+    assert dl.queued_downloads == 1
+
+    f = await dl.run_download()
+
+    assert len(f) == 1
+    assert Path(f[0]).name == "testfile.fits"
+    assert sha256sum(f[0]) == "a1c58cd340e3bd33f94524076f1fa5cf9a7f13c59d5272a9d4bc0b5bc436d9b3"
+
+
 def test_download_ranged_http(httpserver, tmpdir):
     tmpdir = str(tmpdir)
     httpserver.serve_content('SIMPLE  = T',