Skip to content

Commit 66f761a

Browse files
committed
services: create zenodo deposit through CAP
* creates a Zenodo deposit, with files from CAP * saves metadata about the Zenodo deposit, and attaches it to a CAP deposit * integration tests * closes #1938 * closes #1934 Signed-off-by: Ilias Koutsakis <[email protected]>
1 parent 4ff923c commit 66f761a

File tree

7 files changed

+503
-51
lines changed

7 files changed

+503
-51
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

+105-46
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import uuid
2828
from functools import wraps
2929

30+
import requests
3031
from flask import current_app, request
3132
from flask_login import current_user
3233
from invenio_access.models import ActionRoles, ActionUsers
@@ -49,7 +50,9 @@
4950
from sqlalchemy.orm.exc import NoResultFound
5051
from werkzeug.local import LocalProxy
5152

52-
from cap.modules.deposit.errors import DisconnectWebhookError, FileUploadError
53+
from cap.modules.auth.ext import _fetch_token
54+
from cap.modules.deposit.errors import AuthorizationError, \
55+
DisconnectWebhookError, FileUploadError
5356
from cap.modules.deposit.validators import NoRequiredValidator
5457
from cap.modules.experiments.permissions import exp_need_factory
5558
from cap.modules.mail.utils import post_action_notifications
@@ -75,6 +78,7 @@
7578
UpdateDepositPermission)
7679

7780
from .review import Reviewable
81+
from .tasks import upload_to_zenodo
7882

7983
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
8084

@@ -254,53 +258,109 @@ def upload(self, pid, *args, **kwargs):
254258
_, rec = request.view_args.get('pid_value').data
255259
record_uuid = str(rec.id)
256260
data = request.get_json()
257-
webhook = data.get('webhook', False)
258-
event_type = data.get('event_type', 'release')
259-
260-
try:
261-
url = data['url']
262-
except KeyError:
263-
raise FileUploadError('Missing url parameter.')
261+
target = data.get('target')
262+
263+
if target == 'zenodo':
264+
# check for token
265+
token = _fetch_token('zenodo')
266+
if not token:
267+
raise FileUploadError(
268+
'Token not found, please connect your Zenodo '
269+
'account before creating a deposit.')
270+
271+
files = data.get('files')
272+
bucket = data.get('bucket')
273+
if files and bucket:
274+
# first create a deposit, and check if token is expired
275+
deposit = requests.post(
276+
url=f'{current_app.config.get("ZENODO_SERVER_URL")}'
277+
f'/deposit/depositions',
278+
params=dict(access_token=token),
279+
json={},
280+
headers={'Content-Type': 'application/json'}
281+
)
282+
if not deposit.ok:
283+
if deposit.status_code == 401:
284+
raise AuthorizationError(
285+
'Authorization to Zenodo failed. '
286+
'Please reconnect.')
287+
raise FileUploadError(
288+
'Something went wrong, '
289+
'Zenodo deposit not created.')
290+
291+
dep_data = deposit.json()
292+
zenodo_bucket_url = dep_data['links']['bucket']
293+
zenodo_depid = dep_data['id']
294+
295+
# TODO: fix with serializers
296+
zenodo_deposit = {
297+
'id': zenodo_depid,
298+
'links': {
299+
'self': dep_data['links']['self'],
300+
'bucket': zenodo_bucket_url,
301+
'html': dep_data['links']['html'],
302+
'publish': dep_data['links']['publish'],
303+
},
304+
'files': []
305+
}
306+
self.setdefault('_zenodo', []).append(zenodo_deposit)
307+
self.commit()
308+
309+
# upload files to zenodo deposit
310+
upload_to_zenodo.delay(
311+
record_uuid, files, bucket, token, zenodo_depid, zenodo_bucket_url) # noqa
312+
else:
313+
raise FileUploadError(
314+
'You cannot create an empty Zenodo deposit. '
315+
'Please add some files.')
316+
else:
317+
webhook = data.get('webhook', False)
318+
event_type = data.get('event_type', 'release')
264319

265-
try:
266-
host, owner, repo, branch, filepath = parse_git_url(url)
267-
api = create_git_api(host, owner, repo, branch,
268-
current_user.id)
320+
try:
321+
url = data['url']
322+
except KeyError:
323+
raise FileUploadError('Missing url parameter.')
269324

270-
if filepath:
271-
if webhook:
272-
raise FileUploadError(
273-
'You cannot create a webhook on a file')
325+
try:
326+
host, owner, repo, branch, filepath = parse_git_url(url) # noqa
327+
api = create_git_api(host, owner, repo, branch,
328+
current_user.id)
274329

275-
download_repo_file(
276-
record_uuid,
277-
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}/{filepath}', # noqa
278-
*api.get_file_download(filepath),
279-
api.auth_headers,
280-
)
281-
elif webhook:
282-
if event_type == 'release':
283-
if branch:
330+
if filepath:
331+
if webhook:
284332
raise FileUploadError(
285-
'You cannot create a release webhook'
286-
' for a specific branch or sha.')
287-
288-
if event_type == 'push' and \
289-
api.branch is None and api.sha:
290-
raise FileUploadError(
291-
'You cannot create a push webhook'
292-
' for a specific sha.')
333+
'You cannot create a webhook on a file')
334+
335+
download_repo_file(
336+
record_uuid,
337+
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}/{filepath}', # noqa
338+
*api.get_file_download(filepath),
339+
api.auth_headers,
340+
)
341+
elif webhook:
342+
if event_type == 'release':
343+
if branch:
344+
raise FileUploadError(
345+
'You cannot create a release webhook'
346+
' for a specific branch or sha.')
347+
348+
if event_type == 'push' and \
349+
api.branch is None and api.sha:
350+
raise FileUploadError(
351+
'You cannot create a push webhook'
352+
' for a specific sha.')
293353

294-
create_webhook(record_uuid, api, event_type)
295-
else:
296-
download_repo.delay(
297-
record_uuid,
298-
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}.tar.gz', # noqa
299-
api.get_repo_download(),
300-
api.auth_headers)
354+
create_webhook(record_uuid, api, event_type)
355+
else:
356+
download_repo.delay(
357+
record_uuid,
358+
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}.tar.gz', # noqa
359+
api.get_repo_download(),
360+
api.auth_headers)
301361

302-
except GitError as e:
303-
raise FileUploadError(str(e))
362+
except GitError as e:
363+
raise FileUploadError(str(e))
304364

305365
return self
306366

@@ -584,16 +644,15 @@ def validate(self, **kwargs):
584644

585645
validator = NoRequiredValidator(schema, resolver=resolver)
586646

587-
result = {}
588-
result['errors'] = [
647+
errors = [
589648
FieldError(
590649
list(error.path)+error.validator_value,
591650
str(error.message))
592651
for error in validator.iter_errors(self)
593652
]
594653

595-
if result['errors']:
596-
raise DepositValidationError(None, errors=result['errors'])
654+
if errors:
655+
raise DepositValidationError(None, errors=errors)
597656
except RefResolutionError:
598657
raise DepositValidationError('Schema {} not found.'.format(
599658
self['$schema']))

cap/modules/deposit/errors.py

+12
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

cap/modules/deposit/tasks.py

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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(record_uuid, files, bucket, token,
41+
zenodo_depid, zenodo_bucket_url):
42+
"""Upload code to zenodo."""
43+
from cap.modules.deposit.api import CAPDeposit
44+
record = CAPDeposit.get_record(record_uuid)
45+
46+
file_list = []
47+
for filename in files:
48+
file_obj = ObjectVersion.get(bucket, filename)
49+
file_ins = FileInstance.get(file_obj.file_id)
50+
51+
# upload each file in the deposit
52+
with open(file_ins.uri, 'rb') as fp:
53+
file = requests.put(
54+
url=f'{zenodo_bucket_url}/{filename}',
55+
data=fp,
56+
params=dict(access_token=token),
57+
)
58+
59+
if file.ok:
60+
data = file.json()
61+
file_list.append({
62+
'self': data['links']['self'],
63+
'key': data['key'],
64+
'size': data['size']
65+
})
66+
else:
67+
current_app.logger.error(
68+
f'Uploading file {filename} to deposit {zenodo_depid} '
69+
f'failed with {file.status_code}.')
70+
71+
# optionally add metadata
72+
# resp = requests.put(
73+
# url=f'{zenodo_server_url}/deposit/depositions/{depid}',
74+
# params=dict(access_token=token),
75+
# data=json.dumps({}),
76+
# headers={'Content-Type': 'application/json'}
77+
# )
78+
79+
if file_list:
80+
# get the specific deposit we wish to update with files
81+
deposit = list(
82+
filter(lambda d: d['id'] == zenodo_depid, record['_zenodo']))
83+
84+
deposit[0]['files'] += file_list
85+
record.commit()
86+
db.session.commit()

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
@@ -108,7 +109,8 @@ def default_config():
108109
DEBUG=False,
109110
TESTING=True,
110111
APP_GITLAB_OAUTH_ACCESS_TOKEN='testtoken',
111-
MAIL_DEFAULT_SENDER="[email protected]")
112+
MAIL_DEFAULT_SENDER="[email protected]",
113+
ZENODO_SERVER_URL='https://zenodo-test.org')
112114

113115

114116
@pytest.fixture(scope='session')
@@ -401,6 +403,21 @@ def deposit(example_user, create_deposit):
401403
)
402404

403405

406+
@pytest.fixture
407+
def deposit_with_file(example_user, create_schema, create_deposit):
408+
"""New deposit with files."""
409+
create_schema('test-schema', experiment='CMS')
410+
return create_deposit(
411+
example_user,
412+
'test-schema',
413+
{
414+
'$ana_type': 'test-schema',
415+
'title': 'test title'
416+
},
417+
files={'test-file.txt': BytesIO(b'Hello world!')},
418+
experiment='CMS')
419+
420+
404421
@pytest.fixture
405422
def record(example_user, create_deposit):
406423
"""Example record."""

0 commit comments

Comments
 (0)