Skip to content

Commit

Permalink
Merge 3d1c1b3 into 6bf5302
Browse files Browse the repository at this point in the history
  • Loading branch information
Glignos committed May 22, 2019
2 parents 6bf5302 + 3d1c1b3 commit 5025cf2
Show file tree
Hide file tree
Showing 7 changed files with 306 additions and 8 deletions.
65 changes: 62 additions & 3 deletions invenio_records_files/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Integration of records and files for Invenio.
r"""Integration of records and files for Invenio.
Invenio-Records-Files provides basic API for integrating
`Invenio-Records <https://invenio-records.rtfd.io/>`_
Expand Down Expand Up @@ -44,7 +44,7 @@
>>> from invenio_db import db
>>> db.create_all()
Last, since we're managing files, we need to create a base location. Here we
Lastly, since we're managing files, we need to create a base location. Here we
will create a location in a temporary directory:
>>> import tempfile
Expand All @@ -53,6 +53,13 @@
>>> db.session.add(Location(name='default', uri=tmppath, default=True))
>>> db.session.commit()
Configuration
-------------
You can define the prefix to access the files of a record through the
provided view by setting the invenio configuration
``RECORDS_FILES_RESOURCE_NAME``.
Creating a record
-----------------
You use Invenio-Records-Files basic API by importing
Expand Down Expand Up @@ -174,6 +181,56 @@
>>> fileobj is None
True
Integration with Invenio REST API
---------------------------------
Records-Files provides views to handle files attached to a record:
.. code-block:: console
# Upload a file named example.txt to the record with pid of 1
$ curl -X PUT http://localhost:5000/api/records/1/files/example.txt \\
--data-binary example.txt
# Get the list of files for this record
$ curl -X GET http://localhost:5000/api/records/1/files/
# Fetch the file of this record
$ curl -X GET http://localhost:5000/api/records/1/files/example.txt
This is implemented by combining Invenio-Records-REST and Invenio-Files-REST
and adding views accordingly with the ones registered under
`RECORDS_REST_ENDPOINTS
<https://invenio-records-rest.readthedocs.io
/en/latest/usage.html#configuration>`_
by translating and forwarding the request to the appropriate `REST views
<https://invenio-files-rest.readthedocs.io/en/latest/_modules/
invenio_files_rest/views.html>`_ of ``Invenio-Files-Rest``.
For example in the case of the following config:
.. code-block:: python
# Invenio-Records-Rest
RECORDS_REST_ENDPOINTS = dict(recid=dict(
# ...,
item_route='/records/<pid(recid):pid_value>',
#...,
)
)
# Invenio-Files-Rest
RECORDS_FILES_RESOURCE_NAME = 'files'
You can access the files of a record with a pid=1 under the
following url: ``/api/records/1/files/``
And you can add, remove or fetch a specific file, for instance ``example.txt``,
with the following one: ``/api/records/1/files/example.txt``.
According to the method of the request this will automatically use the
appropriate method of `Invenio-Files-Rest views
<https://invenio-files-rest.readthedocs.io/en/latest/
api.html#module-invenio_files_rest.views>`_.
More information about handling files through the REST API can be found `here
<https://invenio-files-rest.readthedocs.io/en/latest/usage.html>`_.
Integration with Invenio-Records-UI
-----------------------------------
Expand All @@ -197,6 +254,8 @@

from __future__ import absolute_import, print_function

from invenio_records_files.ext import InvenioRecordsFiles

from .version import __version__

__all__ = ('__version__', )
__all__ = ('__version__', 'InvenioRecordsFiles')
32 changes: 32 additions & 0 deletions invenio_records_files/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2019 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Invenio-Records-Files configuration."""


RECORDS_FILES_RESOURCE_NAME = 'files'
"""/records/<pid>/<RECORDS_FILES_RESOURCE_NAME>."""

RECORDS_FILES_REST_ENDPOINTS = {}
"""REST endpoints which will have files mounted.
If we wanted to attach files to ``recid``'s and ``depid``'s the configuration
would be the following:
.. code-block::
RECORDS_FILES_REST_ENDPOINTS = {
'RECORDS_REST_ENDPOINTS': [
'recid',
],
'DEPOSIT_REST_ENDPOINTS': [
'depid',
]
}
"""
34 changes: 34 additions & 0 deletions invenio_records_files/ext.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2019 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Flask extension for the Invenio-Records-Files."""


from __future__ import absolute_import, print_function

from invenio_records_files import config


class InvenioRecordsFiles(object):
"""Invenio-Records-Files extension."""

def __init__(self, app=None, **kwargs):
"""Extension initialization."""
if app:
self.init_app(app, **kwargs)

def init_app(self, app):
"""Flask application initialization."""
self.init_config(app)
app.extensions['invenio-records-files'] = self

def init_config(self, app):
"""Initialize configuration."""
for k in dir(config):
if k.startswith('RECORDS_FILES_'):
app.config.setdefault(k, getattr(config, k))
75 changes: 75 additions & 0 deletions invenio_records_files/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
#
# This file is part of Invenio.
# Copyright (C) 2019 CERN.
#
# Invenio is free software; you can redistribute it and/or modify it
# under the terms of the MIT License; see LICENSE file for more details.

"""Invenio-Records-Files REST integration."""

from __future__ import absolute_import, print_function

from flask import Blueprint, abort, g
from invenio_files_rest.views import bucket_view, object_view

from invenio_records_files.models import RecordsBuckets


def create_blueprint_from_app(app):
"""Create Invenio-Records-Files blueprint from a Flask application.
:params app: A Flask application.
:returns: Configured blueprint.
"""
records_files_blueprint = Blueprint(
'invenio_records_files',
__name__,
url_prefix='')

for config_name, rec_types in \
app.config['RECORDS_FILES_REST_ENDPOINTS'].iteritems():
files_resource_name = app.config['RECORDS_FILES_RESOURCE_NAME']
for rec_type in rec_types:
record_item_path = app.config[config_name][rec_type]['item_route']
records_files_blueprint.add_url_rule(
'{record_item_path}/{files_resource_name}'.format(**locals()),
view_func=bucket_view,
)

records_files_blueprint.add_url_rule(
'{record_item_path}/{files_resource_name}/<path:key>'.format(
**locals()),
view_func=object_view,
)

@records_files_blueprint.url_value_preprocessor
def resolve_pid_to_bucket_id(endpoint, values):
"""Flask URL preprocessor to resolve pid to Bucket ID.
In the ``records_files_blueprint`` we are gluing together Records-REST
and Files-REST APIs. Records-REST knows about PIDs but Files-REST does
not, this function will pre-process the URL so the PID is removed from
the URL and resolved to bucket ID which is injected into Files-REST
view calls:
/api/<record_type>/<pid_value>/files/<key> -> /files/<bucket>/<key>.
"""
uuid = str(values['pid_value'].data[0].object_uuid)
g.pid = values.pop('pid_value')
bucket = RecordsBuckets.query.filter_by(record_id=uuid).first()
if bucket:
values['bucket_id'] = str(bucket.bucket_id)
else:
abort(404, 'The requested record does not have files associated')

@records_files_blueprint.url_defaults
def restore_pid_to_url(endpoint, values):
"""Put ``pid_value`` back to the URL after matching Files-REST views.
Since we are computing the URL more than one times, we need the
original values of the request to be unchanged so that it can be
reproduced.
"""
values['pid_value'] = g.pid

return records_files_blueprint
11 changes: 8 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@
tests_require = [
'check-manifest>=0.25',
'coverage>=4.0',
'invenio-indexer>=1.0.0',
'invenio-records-rest>=1.4.2',
'invenio-search[elasticsearch6]>=1.0.0',
'isort>=4.3.4',
'mock>=1.3.0',
'pydocstyle>=1.0.0',
'pytest-cov>=1.8.0',
'pytest-pep8>=1.0.6',
'pytest>=3.7.0',
'invenio-indexer>=1.0.0',
'invenio-search[elasticsearch6]>=1.0.0',
]

extras_require = {
Expand Down Expand Up @@ -92,7 +93,11 @@
],
'invenio_jsonschemas.schemas': [
'records_files = invenio_records_files.jsonschemas',
]
],
'invenio_base.api_blueprints': [
'invenio_records_files = invenio_records_files.'
'views:records_files_blueprint',
],
},
extras_require=extras_require,
install_requires=install_requires,
Expand Down
64 changes: 62 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import os
import shutil
import tempfile
import uuid
from copy import deepcopy

import pytest
from flask import Flask
Expand All @@ -23,20 +25,38 @@
from invenio_files_rest.models import Bucket, Location
from invenio_files_rest.views import blueprint as files_rest_blueprint
from invenio_indexer import InvenioIndexer
from invenio_pidstore import InvenioPIDStore, current_pidstore
from invenio_records import InvenioRecords
from invenio_records_rest import InvenioRecordsREST
from invenio_records_rest.config import RECORDS_REST_ENDPOINTS
from invenio_records_rest.utils import PIDConverter
from invenio_search import InvenioSearch
from six import BytesIO
from sqlalchemy_utils.functions import create_database, database_exists, \
drop_database

from invenio_records_files import InvenioRecordsFiles
from invenio_records_files.api import Record, RecordsBuckets
from invenio_records_files.views import create_blueprint_from_app


@pytest.fixture
def docid_record_type_endpoint():
"""."""
docid = deepcopy(RECORDS_REST_ENDPOINTS['recid'])
docid['list_route'] = '/doc/'
docid['item_route'] = '/doc/<pid(recid):pid_value>'
return docid


@pytest.yield_fixture()
def app(request):
def app(request, docid_record_type_endpoint):
"""Flask application fixture."""
instance_path = tempfile.mkdtemp()
app_ = Flask(__name__, instance_path=instance_path)
RECORDS_REST_ENDPOINTS.update(
docid=docid_record_type_endpoint
)
app_.config.update(
FILES_REST_PERMISSION_FACTORY=lambda *a, **kw: type(
'Allow', (object, ), {'can': lambda self: True}
Expand All @@ -46,12 +66,23 @@ def app(request):
'SQLALCHEMY_DATABASE_URI', 'sqlite://'),
SQLALCHEMY_TRACK_MODIFICATIONS=True,
TESTING=True,
RECORDS_FILES_REST_ENDPOINTS={
'RECORDS_REST_ENDPOINTS': [
'recid',
]
},
RECORDS_REST_ENDPOINTS=RECORDS_REST_ENDPOINTS
)
app_.register_blueprint(files_rest_blueprint)
app_.url_map.converters['pid'] = PIDConverter
InvenioDB(app_)
InvenioRecords(app_)
InvenioFilesREST(app_)
InvenioIndexer(app_)
InvenioPIDStore(app_)
InvenioRecordsREST(app_)
InvenioRecordsFiles(app_)
app_.register_blueprint(files_rest_blueprint)
app_.register_blueprint(create_blueprint_from_app(app_))
search = InvenioSearch(app_)
search.register_mappings('records-files', 'data')

Expand All @@ -61,6 +92,13 @@ def app(request):
shutil.rmtree(instance_path)


@pytest.yield_fixture()
def client(app):
"""Get test client."""
with app.test_client() as client:
yield client


@pytest.yield_fixture()
def db(app):
"""Database fixture."""
Expand Down Expand Up @@ -96,6 +134,19 @@ def record(app, db):
return record


@pytest.fixture()
def minted_record(app, db):
"""Create a test record."""
data = {
'title': 'fuu'
}
with db.session.begin_nested():
rec_uuid = uuid.uuid4()
pid = current_pidstore.minters['recid'](rec_uuid, data)
record = Record.create(data, id_=rec_uuid)
return pid, record


@pytest.fixture()
def bucket(location, db):
"""Create a bucket."""
Expand All @@ -112,6 +163,15 @@ def record_with_bucket(record, bucket, db):
return record


@pytest.fixture()
def minted_record_with_bucket(minted_record, bucket, db):
"""Create a bucket."""
pid, record = minted_record
RecordsBuckets.create(bucket=bucket, record=record.model)
db.session.commit()
return pid, record


@pytest.fixture()
def generic_file(app, record):
"""Add a generic file to the record."""
Expand Down
Loading

0 comments on commit 5025cf2

Please sign in to comment.