Skip to content

Commit 32f7798

Browse files
committed
services: create zenodo deposit through CAP
* updates the config, each user has access to their Zenod ccount/token * creates deposit, with metadata * uploads files to deposit * integration tests * closes #1938 * closes #1934 Signed-off-by: Ilias Koutsakis <[email protected]>
1 parent ac8dfee commit 32f7798

File tree

9 files changed

+624
-77
lines changed

9 files changed

+624
-77
lines changed

cap/config.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -720,10 +720,7 @@ def _(x):
720720

721721
# Zenodo
722722
# ======
723-
ZENODO_SERVER_URL = os.environ.get('APP_ZENODO_SERVER_URL',
724-
'https://zenodo.org/api')
725-
726-
ZENODO_ACCESS_TOKEN = os.environ.get('APP_ZENODO_ACCESS_TOKEN', 'CHANGE_ME')
723+
ZENODO_SERVER_URL = os.environ.get('APP_ZENODO_SERVER_URL', 'https://zenodo.org/api') # noqa
727724

728725
# Endpoints
729726
# =========

cap/modules/deposit/api.py

+77-45
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from sqlalchemy.orm.exc import NoResultFound
5050
from werkzeug.local import LocalProxy
5151

52+
from cap.modules.auth.ext import _fetch_token
5253
from cap.modules.deposit.errors import DisconnectWebhookError, FileUploadError
5354
from cap.modules.deposit.validators import NoRequiredValidator
5455
from cap.modules.experiments.permissions import exp_need_factory
@@ -76,6 +77,8 @@
7677
UpdateDepositPermission)
7778

7879
from .review import Reviewable
80+
from .tasks import upload_to_zenodo
81+
from .utils import create_zenodo_deposit
7982

8083
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
8184

@@ -256,53 +259,82 @@ def upload(self, pid, *args, **kwargs):
256259
_, rec = request.view_args.get('pid_value').data
257260
record_uuid = str(rec.id)
258261
data = request.get_json()
259-
webhook = data.get('webhook', False)
260-
event_type = data.get('event_type', 'release')
261-
262-
try:
263-
url = data['url']
264-
except KeyError:
265-
raise FileUploadError('Missing url parameter.')
266-
267-
try:
268-
host, owner, repo, branch, filepath = parse_git_url(url)
269-
api = create_git_api(host, owner, repo, branch,
270-
current_user.id)
271-
272-
if filepath:
273-
if webhook:
274-
raise FileUploadError(
275-
'You cannot create a webhook on a file')
276-
277-
download_repo_file(
278-
record_uuid,
279-
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}/{filepath}', # noqa
280-
*api.get_file_download(filepath),
281-
api.auth_headers,
282-
)
283-
elif webhook:
284-
if event_type == 'release':
285-
if branch:
286-
raise FileUploadError(
287-
'You cannot create a release webhook'
288-
' for a specific branch or sha.')
262+
target = data.get('target')
263+
264+
if target == 'zenodo':
265+
# check for token
266+
token = _fetch_token('zenodo')
267+
if not token:
268+
raise FileUploadError(
269+
'Please connect your Zenodo account '
270+
'before creating a deposit.')
271+
272+
files = data.get('files')
273+
bucket = data.get('bucket')
274+
zenodo_data = data.get('zenodo_data', {})
275+
276+
if files and bucket:
277+
zenodo_deposit = create_zenodo_deposit(token, zenodo_data) # noqa
278+
self.setdefault('_zenodo', []).append(zenodo_deposit)
279+
self.commit()
280+
281+
# upload files to zenodo deposit
282+
upload_to_zenodo.delay(
283+
files, bucket, token,
284+
zenodo_deposit['id'],
285+
zenodo_deposit['links']['bucket'])
286+
else:
287+
raise FileUploadError(
288+
'You cannot create an empty Zenodo deposit. '
289+
'Please add some files.')
290+
else:
291+
webhook = data.get('webhook', False)
292+
event_type = data.get('event_type', 'release')
289293

290-
if event_type == 'push' and \
291-
api.branch is None and api.sha:
292-
raise FileUploadError(
293-
'You cannot create a push webhook'
294-
' for a specific sha.')
294+
try:
295+
url = data['url']
296+
except KeyError:
297+
raise FileUploadError('Missing url parameter.')
295298

296-
create_webhook(record_uuid, api, event_type)
297-
else:
298-
download_repo.delay(
299-
record_uuid,
300-
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}.tar.gz', # noqa
301-
api.get_repo_download(),
302-
api.auth_headers)
303-
304-
except GitError as e:
305-
raise FileUploadError(str(e))
299+
try:
300+
host, owner, repo, branch, filepath = parse_git_url(url) # noqa
301+
api = create_git_api(host, owner, repo, branch,
302+
current_user.id)
303+
304+
if filepath:
305+
if webhook:
306+
raise FileUploadError(
307+
'You cannot create a webhook on a file')
308+
309+
download_repo_file(
310+
record_uuid,
311+
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}/{filepath}', # noqa
312+
*api.get_file_download(filepath),
313+
api.auth_headers,
314+
)
315+
elif webhook:
316+
if event_type == 'release':
317+
if branch:
318+
raise FileUploadError(
319+
'You cannot create a release webhook'
320+
' for a specific branch or sha.')
321+
322+
if event_type == 'push' and \
323+
api.branch is None and api.sha:
324+
raise FileUploadError(
325+
'You cannot create a push webhook'
326+
' for a specific sha.')
327+
328+
create_webhook(record_uuid, api, event_type)
329+
else:
330+
download_repo.delay(
331+
record_uuid,
332+
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}.tar.gz', # noqa
333+
api.get_repo_download(),
334+
api.auth_headers)
335+
336+
except GitError as e:
337+
raise FileUploadError(str(e))
306338

307339
return self
308340

cap/modules/deposit/errors.py

+27
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ def __init__(self, description, **kwargs):
8787
self.description = description or self.description
8888

8989

90+
class AuthorizationError(RESTException):
91+
"""Exception during authorization."""
92+
93+
code = 401
94+
95+
def __init__(self, description, **kwargs):
96+
"""Initialize exception."""
97+
super(AuthorizationError, self).__init__(**kwargs)
98+
99+
self.description = description or self.description
100+
101+
90102
class DisconnectWebhookError(RESTException):
91103
"""Exception during disconnecting webhook for analysis."""
92104

@@ -124,3 +136,18 @@ def __init__(self, description, errors=None, **kwargs):
124136

125137
self.description = description or self.description
126138
self.errors = [FieldError(e[0], e[1]) for e in errors.items()]
139+
140+
141+
class DataValidationError(RESTValidationError):
142+
"""Review validation error exception."""
143+
144+
code = 400
145+
146+
description = "Validation error. Try again with valid data"
147+
148+
def __init__(self, description, errors=None, **kwargs):
149+
"""Initialize exception."""
150+
super(DataValidationError, self).__init__(**kwargs)
151+
152+
self.description = description or self.description
153+
self.errors = [FieldError(e['field'], e['message']) for e in errors]

cap/modules/deposit/tasks.py

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# This file is part of CERN Analysis Preservation Framework.
4+
# Copyright (C) 2018 CERN.
5+
#
6+
# CERN Analysis Preservation Framework is free software; you can redistribute
7+
# it and/or modify it under the terms of the GNU General Public License as
8+
# published by the Free Software Foundation; either version 2 of the
9+
# License, or (at your option) any later version.
10+
#
11+
# CERN Analysis Preservation Framework is distributed in the hope that it will
12+
# be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
# General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU General Public License
17+
# along with CERN Analysis Preservation Framework; if not, write to the
18+
# Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
19+
# MA 02111-1307, USA.
20+
#
21+
# In applying this license, CERN does not
22+
# waive the privileges and immunities granted to it by virtue of its status
23+
# as an Intergovernmental Organization or submit itself to any jurisdiction.
24+
"""Tasks."""
25+
26+
from __future__ import absolute_import, print_function
27+
28+
import requests
29+
from flask import current_app
30+
from celery import shared_task
31+
from invenio_db import db
32+
from invenio_files_rest.models import FileInstance, ObjectVersion
33+
34+
35+
@shared_task(autoretry_for=(Exception, ),
36+
retry_kwargs={
37+
'max_retries': 5,
38+
'countdown': 10
39+
})
40+
def upload_to_zenodo(files, bucket, token, zenodo_depid, zenodo_bucket_url):
41+
"""Upload to Zenodo the files the user selected."""
42+
for filename in files:
43+
file_obj = ObjectVersion.get(bucket, filename)
44+
file_ins = FileInstance.get(file_obj.file_id)
45+
46+
with open(file_ins.uri, 'rb') as fp:
47+
file = requests.put(
48+
url=f'{zenodo_bucket_url}/{filename}',
49+
data=fp,
50+
params=dict(access_token=token),
51+
)
52+
53+
if not file.ok:
54+
current_app.logger.error(
55+
f'Uploading file {filename} to deposit {zenodo_depid} '
56+
f'failed with {file.status_code}.')

cap/modules/deposit/utils.py

+45
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,14 @@
2525

2626
from __future__ import absolute_import, print_function
2727

28+
import requests
29+
from flask import current_app
30+
from flask_login import current_user
2831
from invenio_access.models import Role
2932
from invenio_db import db
3033

34+
from cap.modules.deposit.errors import AuthorizationError, \
35+
DataValidationError, FileUploadError
3136
from cap.modules.records.utils import url_to_api_url
3237

3338

@@ -75,3 +80,43 @@ def add_api_to_links(links):
7580
item['links'] = add_api_to_links(item.get('links'))
7681

7782
return response
83+
84+
85+
def create_zenodo_deposit(token, data):
86+
"""Create a Zenodo deposit using the logged in user's credentials."""
87+
zenodo_url = current_app.config.get("ZENODO_SERVER_URL")
88+
deposit = requests.post(
89+
url=f'{zenodo_url}/deposit/depositions',
90+
params=dict(access_token=token),
91+
json={'metadata': data},
92+
headers={'Content-Type': 'application/json'}
93+
)
94+
95+
if not deposit.ok:
96+
if deposit.status_code == 401:
97+
raise AuthorizationError(
98+
'Authorization to Zenodo failed. Please reconnect.')
99+
if deposit.status_code == 400:
100+
data = deposit.json()
101+
if data.get('message') == 'Validation error.':
102+
raise DataValidationError(
103+
'Validation error on creating the Zenodo deposit.',
104+
errors=data.get('errors'))
105+
raise FileUploadError(
106+
'Something went wrong, Zenodo deposit not created.')
107+
108+
# TODO: fix with serializers
109+
data = deposit.json()
110+
zenodo_deposit = {
111+
'id': data['id'],
112+
'title': data.get('metadata', {}).get('title'),
113+
'creator': current_user.id,
114+
'created': data['created'],
115+
'links': {
116+
'self': data['links']['self'],
117+
'bucket': data['links']['bucket'],
118+
'html': data['links']['html'],
119+
'publish': data['links']['publish'],
120+
}
121+
}
122+
return zenodo_deposit

cap/modules/services/views/zenodo.py

-27
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
import requests
2929
from flask import current_app, jsonify
30-
from invenio_files_rest.models import FileInstance, ObjectVersion
3130

3231
from . import blueprint
3332

@@ -48,29 +47,3 @@ def get_zenodo_record(zenodo_id):
4847
"""Get record from zenodo (route)."""
4948
resp, status = _get_zenodo_record(zenodo_id)
5049
return jsonify(resp), status
51-
52-
53-
@blueprint.route('/zenodo/<bucket_id>/<filename>')
54-
def upload_to_zenodo(bucket_id, filename):
55-
"""Upload code to zenodo."""
56-
zenodo_server_url = current_app.config.get('ZENODO_SERVER_URL')
57-
params = {"access_token": current_app.config.get(
58-
'ZENODO_ACCESS_TOKEN')}
59-
filename = filename + '.tar.gz'
60-
61-
r = requests.post(zenodo_server_url,
62-
params=params, json={},
63-
)
64-
65-
file_obj = ObjectVersion.get(bucket_id, filename)
66-
file = FileInstance.get(file_obj.file_id)
67-
68-
bucket_url = r.json()['links']['bucket']
69-
with open(file.uri, 'rb') as fp:
70-
response = requests.put(
71-
bucket_url + '/{}'.format(filename),
72-
data=fp,
73-
params=params,
74-
)
75-
76-
return jsonify({"status": response.status_code})

docker-services.yml

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ services:
2727
- "INVENIO_RATELIMIT_STORAGE_URL=redis://cache:6379/3"
2828
- "INVENIO_CERN_APP_CREDENTIALS_KEY=CHANGE_ME"
2929
- "INVENIO_CERN_APP_CREDENTIALS_SECRET=CHANGE_ME"
30+
- "INVENIO_ZENODO_CLIENT_ID=CHANGE_ME"
31+
- "INVENIO_ZENODO_CLIENT_SECRET=CHANGE_ME"
3032
- "DEV_HOST=CHANGE_ME"
3133
lb:
3234
build: ./docker/haproxy/

tests/conftest.py

+18-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import tempfile
2929
from datetime import datetime, timedelta
3030
from uuid import uuid4
31+
from six import BytesIO
3132

3233
import pytest
3334
from flask import current_app
@@ -126,7 +127,8 @@ def default_config():
126127
DEBUG=False,
127128
TESTING=True,
128129
APP_GITLAB_OAUTH_ACCESS_TOKEN='testtoken',
129-
MAIL_DEFAULT_SENDER="[email protected]")
130+
MAIL_DEFAULT_SENDER="[email protected]",
131+
ZENODO_SERVER_URL='https://zenodo-test.org')
130132

131133

132134
@pytest.fixture(scope='session')
@@ -419,6 +421,21 @@ def deposit(example_user, create_deposit):
419421
)
420422

421423

424+
@pytest.fixture
425+
def deposit_with_file(example_user, create_schema, create_deposit):
426+
"""New deposit with files."""
427+
create_schema('test-schema', experiment='CMS')
428+
return create_deposit(
429+
example_user,
430+
'test-schema',
431+
{
432+
'$ana_type': 'test-schema',
433+
'title': 'test title'
434+
},
435+
files={'test-file.txt': BytesIO(b'Hello world!')},
436+
experiment='CMS')
437+
438+
422439
@pytest.fixture
423440
def record(example_user, create_deposit):
424441
"""Example record."""

0 commit comments

Comments
 (0)