Skip to content

Commit 3e9820e

Browse files
authored
Merge pull request #29 from WEHI-ResearchComputing/base_url
Better URL Handling
2 parents 520f669 + 7dc1448 commit 3e9820e

File tree

3 files changed

+59
-16
lines changed

3 files changed

+59
-16
lines changed

filesender/api.py

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from dataclasses import dataclass
12
from typing import Any, Iterable, List, Optional, Tuple, AsyncIterator, Union
23
from filesender.download import files_from_page, DownloadFile
34
import filesender.response_types as response
@@ -99,13 +100,41 @@ def iter_files(paths: Iterable[Path], root: Optional[Path] = None) -> Iterable[T
99100
# If this is a nested file, use the relative path from the root directory as the name
100101
yield str(path.relative_to(root)), path
101102

103+
@dataclass
104+
class EndpointHandler:
105+
base: str
106+
107+
def api(self) -> str:
108+
return f"{self.base}/rest.php"
109+
110+
def download(self) -> str:
111+
return f"{self.base}/download.php"
112+
113+
def create_transfer(self) -> str:
114+
return f"{self.api()}/transfer"
115+
116+
def single_transfer(self, transfer_id: int) -> str:
117+
return f"{self.api()}/transfer/{transfer_id}"
118+
119+
def chunk(self, file_id: int, offset: int) -> str:
120+
return f"{self.api()}/file/{file_id}/chunk/{offset}"
121+
122+
def file(self, file_id: int) -> str:
123+
return f"{self.api()}/file/{file_id}"
124+
125+
def guest(self) -> str:
126+
return f"{self.api()}/guest"
127+
128+
def server_info(self) -> str:
129+
return f"{self.api()}/info"
130+
102131
class FileSenderClient:
103132
"""
104133
A client that can be used to programmatically interact with FileSender.
105134
"""
106135

107136
#: The base url of the file sender's API. For example https://filesender.aarnet.edu.au/rest.php
108-
base_url: str
137+
urls: EndpointHandler
109138
#: Size of upload chunks
110139
chunk_size: Optional[int]
111140
#: Authentication provider that will be used for all privileged requests
@@ -141,14 +170,16 @@ def __init__(
141170
speed up transfers, or reduce this number to reduce memory usage and network errors.
142171
This can be set to `None` to enable unlimited concurrency, but use at your own risk.
143172
"""
144-
self.base_url = base_url
173+
# self.base_url = base_url
174+
self.urls = EndpointHandler(base_url)
145175
self.auth = auth
146176
# FileSender seems to sometimes use redirects
147177
self.http_client = AsyncClient(timeout=None, follow_redirects=True)
148178
self.chunk_size = chunk_size
149179
self.concurrent_chunks = concurrent_chunks
150180
self.concurrent_files = concurrent_files
151181

182+
152183
async def prepare(self) -> None:
153184
"""
154185
Checks that the chunk size is appropriate and/or sets the chunk size based on the server info.
@@ -177,7 +208,7 @@ def on_retry(state: RetryCallState) -> None:
177208
if e is not None:
178209
message = exception_to_message(e)
179210

180-
logger.warn(f"Attempt {state.attempt_number}. {message}")
211+
logger.warning(f"Attempt {state.attempt_number}. {message}")
181212

182213
@retry(
183214
retry=retry_if_exception(should_retry),
@@ -209,7 +240,7 @@ async def create_transfer(
209240
return await self._sign_send(
210241
self.http_client.build_request(
211242
"POST",
212-
f"{self.base_url}/transfer",
243+
self.urls.create_transfer(),
213244
json=body,
214245
)
215246
)
@@ -228,12 +259,12 @@ async def update_transfer(
228259
body: See [`TransferUpdate`][filesender.request_types.TransferUpdate]
229260
230261
Returns:
231-
: See [`Transfer`][filesender.response_types.Transfer]
262+
See [`Transfer`][filesender.response_types.Transfer]
232263
"""
233264
return await self._sign_send(
234265
self.http_client.build_request(
235266
"PUT",
236-
f"{self.base_url}/transfer/{transfer_id}",
267+
self.urls.single_transfer(transfer_id),
237268
json=body,
238269
)
239270
)
@@ -254,7 +285,7 @@ async def update_file(
254285
await self._sign_send(
255286
self.http_client.build_request(
256287
"PUT",
257-
f"{self.base_url}/file/{file_info['id']}",
288+
self.urls.file(file_info['id']),
258289
params={"key": file_info["uid"]},
259290
json=body,
260291
)
@@ -300,7 +331,7 @@ async def _upload_chunk(
300331
return await self._sign_send(
301332
self.http_client.build_request(
302333
"PUT",
303-
f"{self.base_url}/file/{file_info['id']}/chunk/{offset}",
334+
self.urls.chunk(file_info["id"], offset),
304335
params={"key": file_info["uid"]},
305336
content=chunk,
306337
headers={
@@ -323,15 +354,15 @@ async def create_guest(self, body: request.Guest) -> response.Guest:
323354
: See [`Guest`][filesender.response_types.Guest]
324355
"""
325356
return await self._sign_send(
326-
self.http_client.build_request("POST", f"{self.base_url}/guest", json=body)
357+
self.http_client.build_request("POST", self.urls.guest(), json=body)
327358
)
328359

329360
async def _files_from_token(self, token: str) -> Iterable[DownloadFile]:
330361
"""
331362
Internal function that returns a list of file IDs for a given guest token
332363
"""
333364
download_page = await self.http_client.get(
334-
"https://filesender.aarnet.edu.au", params={"s": "download", "token": token}
365+
self.urls.base, params={"s": "download", "token": token}
335366
)
336367
return files_from_page(download_page.content)
337368

@@ -377,11 +408,8 @@ async def download_file(
377408
file_size: The file size in bytes, optionally.
378409
file_name: The file name of the file being downloaded. This will impact the name by which it's saved.
379410
"""
380-
download_endpoint = urlunparse(
381-
urlparse(self.base_url)._replace(path="/download.php")
382-
)
383411
async with self.http_client.stream(
384-
"GET", download_endpoint, params={"files_ids": file_id, "token": token}
412+
"GET", self.urls.download(), params={"files_ids": file_id, "token": token}
385413
) as res:
386414
# Determine filename from response, if not provided
387415
if file_name is None:
@@ -411,7 +439,7 @@ async def get_server_info(self) -> response.ServerInfo:
411439
Returns:
412440
: See [`ServerInfo`][filesender.response_types.ServerInfo].
413441
"""
414-
return (await self.http_client.get(f"{self.base_url}/info")).json()
442+
return (await self.http_client.get(self.urls.server_info())).json()
415443

416444
async def upload_workflow(
417445
self, files: List[Path], transfer_args: request.PartialTransfer = {}

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ description = "FileSender Python CLI and API client"
88
version = "2.1.0"
99
readme = "README.md"
1010
requires-python = ">=3.8"
11-
keywords = ["one", "two"]
1211
license = {text = "BSD-3-Clause"}
1312
classifiers = [
1413
"Programming Language :: Python :: 3",

test/test_client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66
from filesender.request_types import GuestOptions
77
from filesender.benchmark import make_tempfile, make_tempfiles, benchmark
8+
from unittest.mock import MagicMock, patch
89

910
def count_files_recursively(path: Path) -> int:
1011
"""
@@ -152,3 +153,18 @@ async def test_upload_semaphore(
152153
limited, unlimited = benchmark(paths, [1, float("inf")], [1, float("inf")], base_url, username, apikey, recipient)
153154
assert unlimited.time < limited.time
154155
assert unlimited.memory > limited.memory
156+
157+
@pytest.mark.asyncio
158+
async def test_client_download_url():
159+
"""
160+
Tests that the client constructs the correct download URL when downloading a file
161+
"""
162+
mock_http_client = MagicMock()
163+
token = "NOT A REAL TOKEN"
164+
client = FileSenderClient(base_url="http://localhost:8080")
165+
client.http_client = mock_http_client
166+
try:
167+
await client.download_files(token, out_dir=Path("NOT A REAL DIR"))
168+
except Exception:
169+
pass
170+
mock_http_client.get.assert_called_once_with("http://localhost:8080", params=dict(s="download", token=token))

0 commit comments

Comments
 (0)