Skip to content

Commit f0fe5f3

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 * addresses #1938 Signed-off-by: Ilias Koutsakis <[email protected]>
1 parent 4ff923c commit f0fe5f3

File tree

7 files changed

+462
-52
lines changed

7 files changed

+462
-52
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

+67-47
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
@@ -75,6 +76,7 @@
7576
UpdateDepositPermission)
7677

7778
from .review import Reviewable
79+
from .tasks import upload_to_zenodo
7880

7981
_datastore = LocalProxy(lambda: current_app.extensions['security'].datastore)
8082

@@ -254,53 +256,72 @@ def upload(self, pid, *args, **kwargs):
254256
_, rec = request.view_args.get('pid_value').data
255257
record_uuid = str(rec.id)
256258
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.')
264-
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)
269-
270-
if filepath:
271-
if webhook:
272-
raise FileUploadError(
273-
'You cannot create a webhook on a file')
274-
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:
284-
raise FileUploadError(
285-
'You cannot create a release webhook'
286-
' for a specific branch or sha.')
259+
target = data.get('target')
260+
261+
if target == 'zenodo':
262+
token = _fetch_token('zenodo')
263+
if not token:
264+
raise FileUploadError(
265+
'Token not found, please connect your Zenodo '
266+
'account before creating a deposit.')
287267

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.')
268+
files = data.get('files')
269+
bucket = data.get('bucket')
293270

294-
create_webhook(record_uuid, api, event_type)
271+
if files and bucket:
272+
upload_to_zenodo.delay(record_uuid, files, bucket, token) # noqa
295273
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)
274+
raise FileUploadError(
275+
'You cannot create an empty Zenodo deposit. '
276+
'Please add some files.')
277+
else:
278+
webhook = data.get('webhook', False)
279+
event_type = data.get('event_type', 'release')
280+
281+
try:
282+
url = data['url']
283+
except KeyError:
284+
raise FileUploadError('Missing url parameter.')
285+
286+
try:
287+
host, owner, repo, branch, filepath = parse_git_url(url) # noqa
288+
api = create_git_api(host, owner, repo, branch,
289+
current_user.id)
290+
291+
if filepath:
292+
if webhook:
293+
raise FileUploadError(
294+
'You cannot create a webhook on a file')
295+
296+
download_repo_file(
297+
record_uuid,
298+
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}/{filepath}', # noqa
299+
*api.get_file_download(filepath),
300+
api.auth_headers,
301+
)
302+
elif webhook:
303+
if event_type == 'release':
304+
if branch:
305+
raise FileUploadError(
306+
'You cannot create a release webhook'
307+
' for a specific branch or sha.')
308+
309+
if event_type == 'push' and \
310+
api.branch is None and api.sha:
311+
raise FileUploadError(
312+
'You cannot create a push webhook'
313+
' for a specific sha.')
314+
315+
create_webhook(record_uuid, api, event_type)
316+
else:
317+
download_repo.delay(
318+
record_uuid,
319+
f'repositories/{host}/{owner}/{repo}/{api.branch or api.sha}.tar.gz', # noqa
320+
api.get_repo_download(),
321+
api.auth_headers)
301322

302-
except GitError as e:
303-
raise FileUploadError(str(e))
323+
except GitError as e:
324+
raise FileUploadError(str(e))
304325

305326
return self
306327

@@ -584,16 +605,15 @@ def validate(self, **kwargs):
584605

585606
validator = NoRequiredValidator(schema, resolver=resolver)
586607

587-
result = {}
588-
result['errors'] = [
608+
errors = [
589609
FieldError(
590610
list(error.path)+error.validator_value,
591611
str(error.message))
592612
for error in validator.iter_errors(self)
593613
]
594614

595-
if result['errors']:
596-
raise DepositValidationError(None, errors=result['errors'])
615+
if errors:
616+
raise DepositValidationError(None, errors=errors)
597617
except RefResolutionError:
598618
raise DepositValidationError('Schema {} not found.'.format(
599619
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

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
from cap.modules.deposit.errors import AuthorizationError, FileUploadError
35+
36+
37+
@shared_task(autoretry_for=(Exception, ),
38+
retry_kwargs={
39+
'max_retries': 5,
40+
'countdown': 10
41+
})
42+
def upload_to_zenodo(record_uuid, files, bucket, token):
43+
"""Upload code to zenodo."""
44+
from cap.modules.deposit.api import CAPDeposit
45+
record = CAPDeposit.get_record(record_uuid)
46+
zenodo_server_url = current_app.config.get('ZENODO_SERVER_URL')
47+
48+
# in order to upload first create an empty deposit
49+
deposit = requests.post(
50+
url=f'{zenodo_server_url}/deposit/depositions',
51+
params=dict(access_token=token),
52+
json={},
53+
headers={'Content-Type': 'application/json'}
54+
)
55+
56+
if not deposit.ok:
57+
if deposit.status_code == 401:
58+
raise AuthorizationError(
59+
'Authorization to Zenodo failed. Please reconnect.')
60+
raise FileUploadError(
61+
'Something went wrong, Zenodo deposit not created.')
62+
63+
data = deposit.json()
64+
bucket_url = data['links']['bucket']
65+
depid = data['id']
66+
67+
# TODO: fix with serializers
68+
zenodo_deposit = {
69+
'id': depid,
70+
'links': {
71+
'self': data['links']['self'],
72+
'html': data['links']['html'],
73+
'publish': data['links']['publish'],
74+
},
75+
'files': []
76+
}
77+
78+
for filename in files:
79+
file_obj = ObjectVersion.get(bucket, filename)
80+
file_ins = FileInstance.get(file_obj.file_id)
81+
82+
# upload each file in the deposit
83+
with open(file_ins.uri, 'rb') as fp:
84+
file = requests.put(
85+
url=f'{bucket_url}/{filename}',
86+
data=fp,
87+
params=dict(access_token=token),
88+
)
89+
90+
if not file.ok:
91+
raise FileUploadError(
92+
'Files could not be attached to Zenodo deposit.')
93+
94+
data = file.json()
95+
zenodo_deposit['files'].append({
96+
'self': data['links']['self'],
97+
'key': data['key'],
98+
'size': data['size']
99+
})
100+
101+
# optionally add metadata
102+
# resp = requests.put(
103+
# url=f'{zenodo_server_url}/deposit/depositions/{depid}',
104+
# params=dict(access_token=token),
105+
# data=json.dumps({}),
106+
# headers={'Content-Type': 'application/json'}
107+
# )
108+
109+
# get the `_zenodo` field or add it with default []
110+
record.setdefault('_zenodo', []).append(zenodo_deposit)
111+
record.commit()
112+
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)