Skip to content

Commit 7b3691a

Browse files
committed
Merge branch 'develop'
2 parents 2a2d862 + 5fd48aa commit 7b3691a

File tree

6 files changed

+87
-43
lines changed

6 files changed

+87
-43
lines changed

arcsecond/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
"ArcsecondConnectionError",
88
"ArcsecondInvalidEndpointError"]
99

10-
__version__ = '0.7.3'
10+
__version__ = '0.7.4'

arcsecond/api/endpoints/_base.py

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
import click
55
import requests
6+
from requests_toolbelt.multipart import encoder
7+
68
from progress.spinner import Spinner
9+
from progress.bar import Bar
710

811
from arcsecond.api.constants import (
912
ARCSECOND_API_URL_DEV,
@@ -14,12 +17,18 @@
1417
API_AUTH_PATH_REGISTER)
1518

1619
from arcsecond.api.error import ArcsecondConnectionError, ArcsecondError
20+
from arcsecond.api.helpers import transform_payload_for_multipart_encoder_fields
1721
from arcsecond.config import config_file_read_api_key, config_file_read_organisation_memberships
1822
from arcsecond.options import State
1923

2024
SAFE_METHODS = ['GET', 'OPTIONS']
2125
WRITABLE_MEMBERSHIPS = ['superadmin', 'admin', 'member']
2226

27+
EVENT_METHOD_WILL_START = 'EVENT_METHOD_WILL_START'
28+
EVENT_METHOD_DID_FINISH = 'EVENT_METHOD_DID_FINISH'
29+
EVENT_METHOD_DID_FAIL = 'EVENT_METHOD_DID_FAIL'
30+
EVENT_METHOD_PROGRESS_PERCENT = 'EVENT_METHOD_PROGRESS_PERCENT'
31+
2332

2433
class APIEndPoint(object):
2534
name = None
@@ -84,22 +93,23 @@ def _check_organisation_membership_and_permission(self, method_name, organisatio
8493
memberships = config_file_read_organisation_memberships(self.state.config_section())
8594
if self.state.organisation not in memberships.keys():
8695
raise ArcsecondError('No membership found for organisation {}'.format(organisation))
96+
8797
membership = memberships[self.state.organisation]
8898
if method_name not in SAFE_METHODS and membership not in WRITABLE_MEMBERSHIPS:
8999
raise ArcsecondError('Membership for organisation {} has no write permission'.format(organisation))
90100

91-
def _async_perform_request(self, url, method, payload=None, files=None, **headers):
92-
def _async_perform_request_store_response(storage, method, url, payload, files, headers):
101+
def _async_perform_request(self, url, method, payload=None, **headers):
102+
def _async_perform_request_store_response(storage, method, url, payload, headers):
93103
try:
94-
storage['response'] = method(url, json=payload, files=files, headers=headers)
104+
storage['response'] = method(url, json=payload, headers=headers)
95105
except requests.exceptions.ConnectionError:
96106
storage['error'] = ArcsecondConnectionError(self._get_base_url())
97107
except Exception as e:
98108
storage['error'] = ArcsecondError(str(e))
99109

100110
storage = {}
101111
thread = threading.Thread(target=_async_perform_request_store_response,
102-
args=(storage, method, url, payload, files, headers))
112+
args=(storage, method, url, payload, headers))
103113
thread.start()
104114

105115
spinner = Spinner()
@@ -115,31 +125,59 @@ def _async_perform_request_store_response(storage, method, url, payload, files,
115125

116126
return storage.get('response', None)
117127

118-
def _perform_request(self, url, method, payload, **headers):
128+
def _prepare_request(self, url, method, payload, **headers):
119129
assert (url and method)
120130

121131
if not isinstance(method, str) or callable(method):
122132
raise ArcsecondError('Invalid HTTP request method {}. '.format(str(method)))
123133

124-
# Check API key, hence login state. Must do before check for org.
125-
headers = self._check_and_set_api_key(headers, url)
126-
127134
# Put method name aside in its own var.
128135
method_name = method.upper() if isinstance(method, str) else ''
129-
method = getattr(requests, method.lower()) if isinstance(method, str) else method
130-
files = payload.pop('files', None) if payload else None
131136

132137
if self.state and self.state.organisation:
133138
self._check_organisation_membership_and_permission(method_name, self.state.organisation)
134139

140+
# Check API key, hence login state. Must do before check for org.
141+
headers = self._check_and_set_api_key(headers, url)
142+
method = getattr(requests, method.lower()) if isinstance(method, str) else method
143+
135144
if payload:
145+
# Filtering None values out of payload.
136146
payload = {k: v for k, v in payload.items() if v is not None}
137147

148+
return url, method_name, method, payload, headers
149+
150+
def _perform_request(self, url, method, payload, callback=None, **headers):
151+
if self.state.verbose:
152+
click.echo('Preparing request...')
153+
154+
url, method_name, method, payload, headers = self._prepare_request(url, method, payload, **headers)
155+
138156
if self.state.verbose:
139157
click.echo('Sending {} request to {}'.format(method_name, url))
140-
click.echo('Payload: {}'.format(payload))
141158

142-
response = self._async_perform_request(url, method, payload, files, **headers)
159+
payload, fields = transform_payload_for_multipart_encoder_fields(payload)
160+
if fields:
161+
encoded_data = encoder.MultipartEncoder(fields=fields)
162+
bar, upload_callback = None, None
163+
164+
if self.state.is_using_cli is False and callback:
165+
upload_callback = lambda m: callback(EVENT_METHOD_PROGRESS_PERCENT, m.bytes_read / m.len * 100)
166+
elif self.state.verbose:
167+
bar = Bar('Uploading ' + fields['file'][0], suffix='%(percent)d%%')
168+
upload_callback = lambda m: bar.goto(m.bytes_read / m.len * 100)
169+
170+
upload_monitor = encoder.MultipartEncoderMonitor(encoded_data, upload_callback)
171+
headers.update(**{'Content-Type': upload_monitor.content_type})
172+
response = method(url, data=upload_monitor, headers=headers)
173+
174+
if self.state.verbose:
175+
bar.finish()
176+
else:
177+
if self.state.verbose:
178+
click.echo('Payload: {}'.format(payload))
179+
180+
response = self._async_perform_request(url, method, payload, **headers)
143181

144182
if response is None:
145183
raise ArcsecondConnectionError(url)
@@ -153,16 +191,16 @@ def _perform_request(self, url, method, payload, **headers):
153191
return None, response.text
154192

155193
def list(self, name='', **headers):
156-
return self._perform_request(self._list_url(name), 'get', None, **headers)
194+
return self._perform_request(self._list_url(name), 'get', None, None, **headers)
157195

158-
def create(self, payload, **headers):
159-
return self._perform_request(self._list_url(), 'post', payload, **headers)
196+
def create(self, payload, callback=None, **headers):
197+
return self._perform_request(self._list_url(), 'post', payload, callback, **headers)
160198

161199
def read(self, id_name_uuid, **headers):
162-
return self._perform_request(self._detail_url(id_name_uuid), 'get', None, **headers)
200+
return self._perform_request(self._detail_url(id_name_uuid), 'get', None, None, **headers)
163201

164202
def update(self, id_name_uuid, payload, **headers):
165-
return self._perform_request(self._detail_url(id_name_uuid), 'put', payload, **headers)
203+
return self._perform_request(self._detail_url(id_name_uuid), 'put', payload, None, **headers)
166204

167205
def delete(self, id_name_uuid, **headers):
168-
return self._perform_request(self._detail_url(id_name_uuid), 'delete', None, **headers)
206+
return self._perform_request(self._detail_url(id_name_uuid), 'delete', None, None, **headers)

arcsecond/api/helpers.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,23 @@
33
from .error import ArcsecondInputValueError
44

55

6-
def make_file_upload_payload(filepath):
7-
return {'files': {'file': open(os.path.abspath(filepath), 'rb')}}
6+
def make_file_upload_multipart_dict(filepath):
7+
return {'fields': {'file': (os.path.basename(filepath), open(os.path.abspath(filepath), 'rb'))}}
8+
9+
10+
def transform_payload_for_multipart_encoder_fields(payload):
11+
if isinstance(payload, str) and os.path.exists(payload) and os.path.isfile(payload):
12+
payload = make_file_upload_multipart_dict(payload) # transform a str into a dict
13+
14+
elif isinstance(payload, dict) and 'file' in payload.keys():
15+
file_value = payload.pop('file') # .pop() not .get()
16+
if file_value and os.path.exists(file_value) and os.path.isfile(file_value):
17+
payload.update(**make_file_upload_multipart_dict(file_value)) # unpack the resulting dict of make_file...()
18+
else:
19+
payload.update(file=file_value) # do nothing, it's not a file...
20+
21+
fields = payload.pop('fields', None) if payload else None
22+
return payload, fields
823

924

1025
def make_coords_dict(kwargs):

arcsecond/api/main.py

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# -*- coding: utf-8 -*-
22

33
import json
4-
import os
54
import pprint
65
import types
76
import webbrowser
@@ -21,7 +20,6 @@
2120
from arcsecond.options import State
2221
from .auth import AuthAPIEndPoint
2322
from .error import ArcsecondInvalidEndpointError, ArcsecondNotLoggedInError, ArcsecondTooManyPrefixesError
24-
from .helpers import make_file_upload_payload
2523

2624
from .endpoints import (ActivitiesAPIEndPoint, CataloguesAPIEndPoint, DatasetsAPIEndPoint, ExoplanetsAPIEndPoint,
2725
DataFilesAPIEndPoint, FindingChartsAPIEndPoint, InstrumentsAPIEndPoint, NightLogAPIEndPoint,
@@ -31,6 +29,7 @@
3129

3230
pp = pprint.PrettyPrinter(indent=4, depth=5)
3331
ECHO_PREFIX = u' • '
32+
ECHO_ERROR_PREFIX = u' • [error] '
3433

3534
__all__ = ["ArcsecondAPI"]
3635

@@ -70,7 +69,7 @@ def get_api_state(state=None, **kwargs):
7069

7170

7271
def set_api_factory(cls):
73-
def factory(endpoint_class, state, **kwargs):
72+
def factory(endpoint_class, state=None, **kwargs):
7473
return ArcsecondAPI(endpoint_class, state, **kwargs)
7574

7675
for endpoint_class in ENDPOINTS:
@@ -144,13 +143,15 @@ def _echo_error(cls, state, error):
144143
else:
145144
json_obj = json.loads(error)
146145
if 'detail' in json_obj.keys():
147-
click.echo(ECHO_PREFIX + json_obj['detail'])
146+
detail_msg = ', '.join(json_obj['detail']) if isinstance(json_obj['detail'], list) else json_obj['detail']
147+
click.echo(ECHO_ERROR_PREFIX + detail_msg)
148148
elif 'error' in json_obj.keys():
149-
click.echo(ECHO_PREFIX + json_obj['error'])
149+
error_msg = ', '.join(json_obj['error']) if isinstance(json_obj['error'], list) else json_obj['error']
150+
click.echo(ECHO_ERROR_PREFIX + error_msg)
150151
elif 'non_field_errors' in json_obj.keys():
151152
errors = json_obj['non_field_errors']
152153
message = ', '.join(errors) if isinstance(error, list) else str(errors)
153-
click.echo(ECHO_PREFIX + message)
154+
click.echo(ECHO_ERROR_PREFIX + message)
154155
else:
155156
click.echo(ECHO_PREFIX + str(error))
156157

@@ -166,22 +167,10 @@ def _check_endpoint_class(self, endpoint):
166167
raise ArcsecondInvalidEndpointError(endpoint, ENDPOINTS)
167168
return endpoint
168169

169-
def _check_for_file_in_payload(self, payload):
170-
if isinstance(payload, str) and os.path.exists(payload) and os.path.isfile(payload):
171-
return make_file_upload_payload(payload) # transform a str into a dict
172-
elif isinstance(payload, dict) and 'file' in payload.keys():
173-
file_value = payload.pop('file') # .pop() not .get()
174-
if file_value and os.path.exists(file_value) and os.path.isfile(file_value):
175-
payload.update(**make_file_upload_payload(file_value)) # unpack the resulting dict of make_file...()
176-
else:
177-
payload.update(file=file_value) # do nothing, it's not a file...
178-
return payload
179-
180170
def list(self, name=None, **headers):
181171
return self._echo_response(self.endpoint.list(name, **headers))
182172

183173
def create(self, payload, **headers):
184-
payload = self._check_for_file_in_payload(payload)
185174
return self._echo_response(self.endpoint.create(payload, **headers))
186175

187176
def read(self, id_name_uuid, **headers):

arcsecond/cli.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def activities(state, method, pk, **kwargs):
149149
api = Arcsecond.create_activities_api(state)
150150
if method == 'create':
151151
kwargs.update(coordinates=make_coords_dict(kwargs))
152-
api.create(kwargs)
152+
api.create(kwargs) # the kwargs dict is the payload!
153153
elif method == 'read':
154154
api.read(pk) # will handle list if pk is None
155155
elif method == 'update':
@@ -169,7 +169,7 @@ def activities(state, method, pk, **kwargs):
169169
def datasets(state, method, uuid, **kwargs):
170170
api = Arcsecond.create_datasets_api(state)
171171
if method == 'create':
172-
api.create(kwargs)
172+
api.create(kwargs) # the kwargs dict is the payload!
173173
elif method == 'read':
174174
api.read(uuid) # will handle list if pk is None
175175
elif method == 'update':
@@ -184,7 +184,8 @@ def datasets(state, method, uuid, **kwargs):
184184
@click.argument('dataset', required=True, nargs=1)
185185
@click.argument('method', required=False, nargs=1, type=MethodChoiceParamType(), default='read')
186186
@click.argument('pk', required=False, nargs=1)
187-
@click.option('--file', required=False, nargs=1, help="The path to the data file to upload. Can be zipped with gzip or bzip2.")
187+
@click.option('--file', required=False, nargs=1,
188+
help="The path to the data file to upload. Can be zipped with gzip or bzip2.")
188189
@click.option('--instrument', required=False, nargs=1, help="The UUID of the instrument.")
189190
@organisation_options
190191
@pass_state
@@ -200,7 +201,7 @@ def datafiles(state, dataset, method, pk, **kwargs):
200201
kwargs.pop('organisation')
201202
api = Arcsecond.create_datafiles_api(state=state, dataset=dataset)
202203
if method == 'create':
203-
api.create(kwargs)
204+
api.create(kwargs) # the kwargs dict is the payload!
204205
elif method == 'read':
205206
api.read(pk) # will handle list if pk is None
206207
elif method == 'update':

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
'six',
2929
'click',
3030
'requests',
31+
'requests_toolbelt',
3132
'pygments',
3233
'configparser',
3334
'progress'

0 commit comments

Comments
 (0)