Skip to content

Commit fe83bee

Browse files
committed
make test pass
1 parent 38649bf commit fe83bee

File tree

2 files changed

+82
-20
lines changed

2 files changed

+82
-20
lines changed

clients/python/src/osparc/_api_files_api.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@
44
import json
55
import logging
66
import math
7-
import random
8-
import shutil
9-
import string
107
from pathlib import Path
118
from typing import Any, Iterator, List, Optional, Tuple, Union
129

@@ -28,6 +25,8 @@
2825
FileUploadData,
2926
UploadedPart,
3027
)
28+
from tempfile import NamedTemporaryFile
29+
from urllib.parse import urljoin
3130
from ._utils import (
3231
DEFAULT_TIMEOUT_SECONDS,
3332
PaginationGenerator,
@@ -65,24 +64,48 @@ def __getattr__(self, name: str) -> Any:
6564
return super().__getattribute__(name)
6665

6766
def download_file(
68-
self, file_id: str, *, destination_folder: Optional[Path] = None, **kwargs
67+
self,
68+
file_id: str,
69+
*,
70+
destination_folder: Optional[Path] = None,
71+
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
72+
**kwargs,
73+
) -> str:
74+
return asyncio.run(
75+
self.download_file_async(
76+
file_id=file_id,
77+
destination_folder=destination_folder,
78+
timeout_seconds=timeout_seconds,
79+
**kwargs,
80+
)
81+
)
82+
83+
async def download_file_async(
84+
self,
85+
file_id: str,
86+
*,
87+
destination_folder: Optional[Path] = None,
88+
timeout_seconds: int = DEFAULT_TIMEOUT_SECONDS,
89+
**kwargs,
6990
) -> str:
7091
if destination_folder is not None and not destination_folder.is_dir():
7192
raise RuntimeError(
7293
f"destination_folder: {destination_folder} must be a directory"
7394
)
74-
downloaded_file: Path = Path(super().download_file(file_id, **kwargs))
75-
if destination_folder is not None:
76-
dest_file: Path = destination_folder / downloaded_file.name
77-
while dest_file.is_file():
78-
new_name = (
79-
downloaded_file.stem
80-
+ "".join(random.choices(string.ascii_letters, k=8))
81-
+ downloaded_file.suffix
82-
)
83-
dest_file = destination_folder / new_name
84-
shutil.move(downloaded_file, dest_file)
85-
downloaded_file = dest_file
95+
downloaded_file = Path(
96+
NamedTemporaryFile(dir=destination_folder, delete=False).name
97+
)
98+
async with AsyncHttpClient(
99+
configuration=self.api_client.configuration, timeout=timeout_seconds
100+
) as session:
101+
url = urljoin(
102+
self.api_client.configuration.host, f"/v0/files/{file_id}/content"
103+
)
104+
with open(downloaded_file, mode="wb") as f:
105+
async for chunk in session.stream(
106+
"GET", url=url, auth=self._auth, follow_redirects=True
107+
):
108+
f.write(chunk)
86109
return str(downloaded_file.resolve())
87110

88111
def upload_file(
@@ -159,7 +182,7 @@ async def upload_file_async(
159182
)
160183
async with AsyncHttpClient(
161184
configuration=self.api_client.configuration,
162-
request_type="post",
185+
method="post",
163186
url=links.abort_upload,
164187
body=abort_body.to_dict(),
165188
base_url=self.api_client.configuration.host,

clients/python/src/osparc/_http_client.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
from contextlib import suppress
22
from datetime import datetime
33
from email.utils import parsedate_to_datetime
4-
from typing import Any, Awaitable, Callable, Dict, Optional, Set
4+
from typing import (
5+
Any,
6+
Awaitable,
7+
Callable,
8+
Dict,
9+
Optional,
10+
Set,
11+
Literal,
12+
AsyncGenerator,
13+
)
514

615
import httpx
716
import tenacity
@@ -17,14 +26,14 @@ def __init__(
1726
self,
1827
*,
1928
configuration: Configuration,
20-
request_type: Optional[str] = None,
29+
method: Optional[str] = None,
2130
url: Optional[str] = None,
2231
body: Optional[Dict] = None,
2332
**httpx_async_client_kwargs,
2433
):
2534
self.configuration = configuration
2635
self._client = httpx.AsyncClient(**httpx_async_client_kwargs)
27-
self._callback = getattr(self._client, request_type) if request_type else None
36+
self._callback = getattr(self._client, method) if method else None
2837
self._url = url
2938
self._body = body
3039
if self._callback is not None:
@@ -77,6 +86,30 @@ async def _():
7786

7887
return await _()
7988

89+
async def _stream(
90+
self, method: Literal["GET"], url: str, *args, **kwargs
91+
) -> AsyncGenerator[bytes, None]:
92+
n_attempts = self.configuration.retries.total
93+
assert isinstance(n_attempts, int)
94+
95+
@tenacity.retry(
96+
reraise=True,
97+
wait=self._wait_callback,
98+
stop=tenacity.stop_after_attempt(n_attempts),
99+
retry=tenacity.retry_if_exception_type(httpx.HTTPStatusError),
100+
)
101+
async def _() -> AsyncGenerator[bytes, None]:
102+
async with self._client.stream(
103+
method=method, url=url, *args, **kwargs
104+
) as response:
105+
if response.status_code in self.configuration.retries.status_forcelist:
106+
response.raise_for_status()
107+
async for chunk in response.aiter_bytes():
108+
yield chunk
109+
110+
async for chunk in _():
111+
yield chunk
112+
80113
async def put(self, *args, **kwargs) -> httpx.Response:
81114
return await self._request(self._client.put, *args, **kwargs)
82115

@@ -92,6 +125,12 @@ async def patch(self, *args, **kwargs) -> httpx.Response:
92125
async def get(self, *args, **kwargs) -> httpx.Response:
93126
return await self._request(self._client.get, *args, **kwargs)
94127

128+
async def stream(
129+
self, method: Literal["GET"], url: str, *args, **kwargs
130+
) -> AsyncGenerator[bytes, None]:
131+
async for chunk in self._stream(method=method, url=url, *args, **kwargs):
132+
yield chunk
133+
95134
def _wait_callback(self, retry_state: tenacity.RetryCallState) -> int:
96135
assert retry_state.outcome is not None
97136
if retry_state.outcome and retry_state.outcome.exception():

0 commit comments

Comments
 (0)