Skip to content

Commit 85b3a63

Browse files
committed
Revised file-upload logic, client can now upload multiple files OR large files. Fixes #23
1 parent 156b7ff commit 85b3a63

File tree

6 files changed

+132
-85
lines changed

6 files changed

+132
-85
lines changed

meorg_client/cli.py

Lines changed: 26 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import meorg_client.utilities as mcu
55
import os
66
import sys
7-
from inspect import getmembers
87
import getpass
98
from pathlib import Path
109
import json
@@ -21,20 +20,19 @@ def _get_client():
2120
# Get the dev-mode flag from the environment, better than passing the dev flag everywhere.
2221
dev_mode = os.getenv("MEORG_DEV_MODE", "0") == "1"
2322

24-
credentials = mcu.get_user_data_filepath('credentials.json')
25-
credentials_dev = mcu.get_user_data_filepath('credentials-dev.json')
23+
credentials = mcu.get_user_data_filepath("credentials.json")
24+
credentials_dev = mcu.get_user_data_filepath("credentials-dev.json")
2625

2726
# In dev mode and the configuration file exists
2827
if dev_mode and credentials_dev.is_file():
29-
credentials = mcu.load_user_data('credentials-dev.json')
30-
28+
credentials = mcu.load_user_data("credentials-dev.json")
29+
3130
# In dev mode and it doesn't (i.e. Actions)
3231
elif dev_mode and not credentials_dev.is_file():
3332
credentials = dict(
34-
email=os.getenv('MEORG_EMAIL'),
35-
password=os.getenv('MEORG_PASSWORD')
33+
email=os.getenv("MEORG_EMAIL"), password=os.getenv("MEORG_PASSWORD")
3634
)
37-
35+
3836
# Production credentials
3937
else:
4038
credentials = mcu.load_user_data("credentials.json")
@@ -65,11 +63,10 @@ def _call(func, **kwargs):
6563
try:
6664
return func(**kwargs)
6765
except Exception as ex:
68-
6966
click.echo(ex.msg, err=True)
7067

7168
# Bubble up the exception
72-
if os.getenv('MEORG_DEV_MODE') == '1':
69+
if os.getenv("MEORG_DEV_MODE") == "1":
7370
raise
7471

7572
sys.exit(1)
@@ -86,7 +83,7 @@ def cli():
8683
pass
8784

8885

89-
@click.command('list')
86+
@click.command("list")
9087
def list_endpoints():
9188
"""
9289
List the available endpoints for the server.
@@ -107,27 +104,8 @@ def list_endpoints():
107104
click.echo(out)
108105

109106

110-
@click.command('status')
111-
@click.argument("id")
112-
def file_status(id):
113-
"""
114-
Check the file status based on the job ID from file-upload.
115-
116-
Prints the true file ID or a status.
117-
"""
118-
client = _get_client()
119-
response_data = _call(client.get_file_status, id=id).get("data")
120-
121-
# If the file is complete (transferred to object store), get the true ID
122-
if response_data.get("status") == "complete":
123-
file_id = response_data.get("files")[0].get("file")
124-
click.echo(file_id)
125-
else:
126-
click.echo("Pending")
127-
128-
129-
@click.command('upload')
130-
@click.argument("file_path")
107+
@click.command("upload")
108+
@click.argument("file_path", nargs=-1)
131109
def file_upload(file_path):
132110
"""
133111
Upload a file to the server.
@@ -137,12 +115,13 @@ def file_upload(file_path):
137115
client = _get_client()
138116

139117
# Upload the file, get the job ID
140-
response = _call(client.upload_file, file_path=file_path)
141-
job_id = response.get("data").get("jobId")
142-
click.echo(job_id)
118+
response = _call(client.upload_file, file_path=list(file_path))
119+
files = response.get("data").get("files")
120+
for f in files:
121+
click.echo(f.get("file"))
143122

144123

145-
@click.command('list')
124+
@click.command("list")
146125
@click.argument("id")
147126
def file_list(id):
148127
"""
@@ -157,7 +136,7 @@ def file_list(id):
157136
click.echo(f)
158137

159138

160-
@click.command('attach')
139+
@click.command("attach")
161140
@click.argument("file_id")
162141
@click.argument("output_id")
163142
def file_attach(file_id, output_id):
@@ -166,12 +145,12 @@ def file_attach(file_id, output_id):
166145
"""
167146
client = _get_client()
168147

169-
response = _call(client.attach_files_to_model_output, id=output_id, files=[file_id])
148+
_ = _call(client.attach_files_to_model_output, id=output_id, files=[file_id])
170149

171150
click.echo("SUCCESS")
172151

173152

174-
@click.command('start')
153+
@click.command("start")
175154
@click.argument("id")
176155
def analysis_start(id):
177156
"""
@@ -188,7 +167,7 @@ def analysis_start(id):
188167
click.echo(analysis_id)
189168

190169

191-
@click.command('status')
170+
@click.command("status")
192171
@click.argument("id")
193172
def analysis_status(id):
194173
"""
@@ -250,22 +229,25 @@ def initialise(dev=False):
250229

251230

252231
# Add groups for nested subcommands
253-
@click.group('endpoints', help='API endpoint commands.')
232+
@click.group("endpoints", help="API endpoint commands.")
254233
def cli_endpoints():
255234
pass
256235

257-
@click.group('file', help='File commands.')
236+
237+
@click.group("file", help="File commands.")
258238
def cli_file():
259239
pass
260240

261-
@click.group('analysis', help='Analysis commands.')
241+
242+
@click.group("analysis", help="Analysis commands.")
262243
def cli_analysis():
263244
pass
264245

246+
265247
# Add file commands
266248
cli_file.add_command(file_list)
267249
cli_file.add_command(file_upload)
268-
cli_file.add_command(file_status)
250+
# cli_file.add_command(file_status)
269251
cli_file.add_command(file_attach)
270252

271253
# Add endpoint commands

meorg_client/client.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import meorg_client.endpoints as endpoints
1010
import meorg_client.exceptions as mx
1111
import mimetypes as mt
12+
import io
1213

1314

1415
class Client:
@@ -213,45 +214,67 @@ def logout(self):
213214
self.headers.pop("X-User-Id", None)
214215
self.headers.pop("X-Auth-Token", None)
215216

216-
def get_file_status(self, id: str) -> Union[dict, requests.Response]:
217-
"""Get the file status.
217+
def upload_file(
218+
self,
219+
file_path: Union[str, list],
220+
file_obj: Union[io.BufferedReader, list] = None,
221+
) -> Union[dict, requests.Response]:
222+
"""Upload a file.
218223
219224
Parameters
220225
----------
221-
id : str
222-
Job ID of the file.
226+
file_path : str or list
227+
Path to the file.
228+
file_obj : io.BufferedReader, optional
229+
File object (handle) to allow direct supply of file object, by default None
223230
224231
Returns
225232
-------
226233
Union[dict, requests.Response]
227234
Response from ME.org.
228235
"""
229-
return self._make_request(
230-
method=mcc.HTTP_GET, endpoint=endpoints.FILE_STATUS, url_params=dict(id=id)
231-
)
232236

233-
def upload_file(self, file_path: str) -> Union[dict, requests.Response]:
234-
"""Upload a file.
237+
payload = list()
235238

236-
Parameters
237-
----------
238-
file_path : str
239-
Path to the file.
239+
# Cast as list for iterative upload
240+
if not isinstance(file_path, list):
241+
file_path = [file_path]
240242

241-
Returns
242-
-------
243-
Union[dict, requests.Response]
244-
Response from ME.org.
245-
"""
246-
# Get the filename and extension
247-
filename = os.path.basename(file_path)
248-
ext = filename.split(".")[-1]
243+
# Payload assembly
244+
if file_obj is not None:
245+
# Cast as a list for iterative upload
246+
if not isinstance(file_obj, list):
247+
file_objs = [file_obj]
249248

250-
# Get the MIME type (raises a KeyError if it is unknown)
251-
mimetype = mt.types_map[f".{ext}"]
249+
if len(file_objs) != len(file_path):
250+
raise ValueError("Supplied file paths and file objects do not match")
252251

253-
# Assemble the file payload
254-
payload = dict(file=(filename, open(file_path, "rb"), mimetype))
252+
for ix, file_obj in enumerate(file_objs):
253+
if not isinstance(file_obj, io.BufferedReader) and not isinstance(
254+
file_obj, io.BytesIO
255+
):
256+
raise TypeError(
257+
f"Supplied file object {ix} is not an io.BufferedReader or io.BytesIO."
258+
)
259+
260+
_file_path = file_path[ix]
261+
262+
# Get the filename and extension
263+
filename = os.path.basename(_file_path)
264+
ext = filename.split(".")[-1]
265+
mimetype = mt.types_map[f".{ext}"]
266+
267+
payload.append(("file", (filename, file_obj, mimetype)))
268+
269+
else:
270+
for _file_path in file_path:
271+
# Get the filename and extension
272+
filename = os.path.basename(_file_path)
273+
ext = filename.split(".")[-1]
274+
mimetype = mt.types_map[f".{ext}"]
275+
file_obj = open(_file_path, "rb")
276+
277+
payload.append(("file", (filename, file_obj, mimetype)))
255278

256279
return self._make_request(
257280
method=mcc.HTTP_POST,

meorg_client/endpoints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
# Files
1111
FILE_LIST = "modeloutput/{id}/files"
12-
FILE_UPLOAD = "files"
12+
FILE_UPLOAD = "upload"
1313
FILE_STATUS = "files/status/{id}"
1414

1515
# Analysis

meorg_client/tests/test_cli.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,38 @@ def test_file_upload(runner):
2727
assert result.exit_code == 0
2828

2929
# Add the job_id to the store for the next test
30-
store.set("job_id", result.output.strip())
30+
store.set("file_id", result.output.strip())
3131

3232
# Let it wait for a short while, allow the server to transfer to object store.
3333
time.sleep(5)
3434

3535

36-
def test_file_status(runner):
37-
"""Test file-status via CLI."""
36+
def test_file_multiple(runner):
37+
"""Test file-upload via CLI."""
3838

39-
# Get the file ID based on the job ID
40-
job_id = store.get("job_id")
41-
result = runner.invoke(cli.file_status, [job_id])
39+
# Upload a tiny test file
40+
filepath = os.path.join(mu.get_installed_data_root(), "test/test.txt")
41+
result = runner.invoke(cli.file_upload, [filepath, filepath])
4242
assert result.exit_code == 0
43-
assert result.output != "Pending"
4443

45-
# Add file_id to the store for the next test
46-
store.set("file_id", result.output.strip())
44+
# Add the job_id to the store for the next test
45+
store.set("file_ids", result.output.strip())
46+
47+
# Let it wait for a short while, allow the server to transfer to object store.
48+
time.sleep(5)
49+
50+
51+
# def test_file_status(runner):
52+
# """Test file-status via CLI."""
53+
54+
# # Get the file ID based on the job ID
55+
# job_id = store.get("job_id")
56+
# result = runner.invoke(cli.file_status, [job_id])
57+
# assert result.exit_code == 0
58+
# assert result.output != "Pending"
59+
60+
# # Add file_id to the store for the next test
61+
# store.set("file_id", result.output.strip())
4762

4863

4964
def test_file_list(runner):

meorg_client/tests/test_client.py

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from meorg_client.client import Client
55
import meorg_client.utilities as mu
66
from conftest import store
7+
import io
78

89

910
def _get_authenticated_client():
@@ -55,14 +56,19 @@ def test_upload_file(client):
5556
store.set("file_upload", response)
5657

5758

58-
def test_file_status(client):
59-
# Get the response
60-
job_id = store.get("file_upload").get("data").get("jobId")
59+
def test_upload_file_multiple(client):
60+
"""Test the uploading of a file."""
61+
# Upload the file.
62+
filepath = os.path.join(mu.get_installed_data_root(), "test/test.txt")
6163

62-
response = client.get_file_status(job_id)
64+
# Upload the file
65+
response = client.upload_file([filepath, filepath])
66+
67+
# Make sure it worked
6368
assert client.success()
6469

65-
store.set("file_status", response)
70+
# Store the response.
71+
store.set("file_upload_multiple", response)
6672

6773

6874
def test_file_list(client):
@@ -73,7 +79,8 @@ def test_file_list(client):
7379

7480
def test_attach_files_to_model_output(client):
7581
# Get the file id from the job id
76-
file_id = store.get("file_status").get("data").get("files")[0].get("file")
82+
# file_id = store.get("file_status").get("data").get("files")[0].get("file")
83+
file_id = store.get("file_upload").get("data").get("files")[0].get("file")
7784

7885
# Attach it to the model output
7986
_ = client.attach_files_to_model_output(client._model_output_id, [file_id])
@@ -98,3 +105,22 @@ def test_logout(client):
98105
"""Test logout."""
99106
client.logout()
100107
assert "X-Auth-Token" not in client.headers.keys()
108+
109+
110+
def test_upload_file_large(client):
111+
"""Test the uploading of a large-ish file."""
112+
113+
# Create an in-memory 10mb file
114+
size = 10000000
115+
with io.BytesIO() as buffer:
116+
buffer.write(bytearray(os.urandom(size)))
117+
buffer.seek(0)
118+
119+
# Upload the file.
120+
filepath = os.path.join(mu.get_installed_data_root(), "test/test.txt")
121+
122+
# Upload the file
123+
_ = client.upload_file(file_path=filepath, file_obj=buffer)
124+
125+
# Make sure it worked
126+
assert client.success()

meorg_client/utilities.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ def get_user_data_filepath(filename):
6060
"""Get the filepath to the user file."""
6161
return Path.home() / ".meorg" / filename
6262

63+
6364
def load_user_data(filename):
6465
"""Load data from the user's home directory."""
6566
filepath = get_user_data_filepath(filename)

0 commit comments

Comments
 (0)