""" This file contains POST routes methods. """ import bottle import json import threading from sqlalchemy.exc import IntegrityError import config import database import tools from reference_fetcher import arxiv def create_paper(db): """ Create a new paper identified by its DOI or arXiv eprint id. .. code-block:: bash POST /papers Content-Type: application/vnd.api+json Accept: application/vnd.api+json { "data": { "doi": "10.1126/science.1252319", // OR "arxiv_id": "1401.2910" } } .. code-block:: json { "data": { "type": "papers", "id": 1, "attributes": { "doi": "10.1126/science.1252319", "arxiv_id": "1401.2910" }, "links": { "self": "/papers/1" }, "relationships": { "cite": { "links": { "related": "/papers/1/relationships/cite" } }, … } } } :param db: A database session, injected by the ``Bottle`` plugin. :returns: An ``HTTPResponse``. """ data = json.loads(bottle.request.body.read().decode("utf-8")) # Validate the request if("data" not in data or "type" not in data["data"] or data["data"]["type"] != "papers" or ("doi" not in data["data"] and "arxiv_id" not in data["data"])): return bottle.HTTPError(403, "Forbidden") data = data["data"] if "doi" in data: paper = create_by_doi(data["doi"], db) elif "arxiv_id" in data: paper = create_by_arxiv(data["arxiv_id"], db) if paper is None: return bottle.HTTPError(409, "Conflict") # Return the resource response = { "data": paper.json_api_repr(db) } # Import "cite" relation add_cite_relationship(paper, db) # Return 200 with the correct body headers = {"Location": "/papers/%d" % (paper.id,)} return tools.APIResponse(status=200, body=tools.pretty_json(response), headers=headers) def create_by_doi(doi, db): """ Create a new resource identified by its DOI, if it does not exist. :param doi: The DOI of the paper. :param db: A database session. :returns: ``None`` if insertion failed, the ``Paper`` object otherwise. """ paper = database.Paper(doi=doi) # Try to fetch an arXiv id arxiv_id = arxiv.get_arxiv_eprint_from_doi(doi) if arxiv_id: paper.arxiv_id = arxiv_id # Add it to the database try: db.add(paper) db.flush() except IntegrityError: # Unique constraint violation, paper already exists db.rollback() return None # Return the paper return paper def create_by_arxiv(arxiv_id, db): """ Create a new resource identified by its arXiv eprint ID, if it does not exist. :param arxiv_id: The arXiv eprint ID. :param db: A database session. :returns: ``None`` if insertion failed, the ``Paper`` object otherwise. """ paper = database.Paper(arxiv_id=arxiv_id) # Try to fetch an arXiv id doi = arxiv.get_doi(arxiv_id) if doi: paper.doi = doi # Add it to the database try: db.add(paper) db.flush() except IntegrityError: # Unique constraint violation, paper already exists db.rollback() return None # Return the paper return paper def add_cite_relationship(paper, db): """ Add the "cite" relationships between the provided paper and the papers referenced by it. :param paper: The paper to fetch references from. :param db: A database session :returns: Nothing. """ # If paper is on arXiv if paper.arxiv_id is not None: # Get the cited DOIs cited_urls = arxiv.get_cited_dois(paper.arxiv_id) # Filter out the ones that were not matched cited_urls = [cited_urls[k] for k in cited_urls if cited_urls[k] is not None] for url in cited_urls: type, identifier = tools.get_identifier_from_url(url) if type is None: # No identifier found continue # Get the associated paper in the db right_paper = (db.query(database.Paper) .filter(getattr(database.Paper, type) == identifier) .first()) if right_paper is None: # If paper is not in db, add it if type == "doi": right_paper = create_by_doi(identifier, db) elif type == "arxiv_id": right_paper = create_by_arxiv(identifier, db) else: continue # Push this paper on the queue for update of cite relationships queue = database.CitationProcessingQueue() queue.paper = right_paper try: db.add(queue) except IntegrityError: # Unique constraint violation, relationship already exists db.rollback() # Update the relationships update_relationship_backend(paper.id, right_paper.id, "cite", db) # If paper is not on arXiv, nothing to do else: return def fetch_citations_in_queue(create_session): """ Process the first item in the queue, waiting for citation processing. i.. note:: Calls itself recursively after the time defined in ``config``, so that queued articles are processed concurrently. :param create_session: a ``SQLAlchemy`` ``sessionmaker``. :returns: Nothing. """ # Get a db Session db = create_session() queued = db.query(database.CitationProcessingQueue).first() if queued: print("Processing citation relationships for %s." % (queued.paper,)) # Process this paper add_cite_relationship(queued.paper, db) # Remove this paper from queue db.delete(queued) # Commit to the database try: db.commit() except: db.rollback() # Call this function again after a while threading.Timer( config.queue_polling_interval, lambda: fetch_citations_in_queue(create_session) ).start() def update_relationships(id, name, db): """ Update the relationships associated to a given paper. .. code-block:: bash POST /papers/1/relationships/cite Content-Type: application/vnd.api+json Accept: application/vnd.api+json { "data": [ { "type": "cite", "id": "2" }, … ] } .. code-block:: json HTTP 204 :param id: The id of the paper to update relationships. :param name: The name of the relationship to update. :param db: A database session, passed by Bottle plugin. :returns: No content. 204 on success, 403 on error. """ data = json.loads(bottle.request.body.read().decode("utf-8")) # Validate the request if "data" not in data: return bottle.HTTPError(403, "Forbidden") # Filter data, invalid entries are ignored data = [i for i in data["data"] if "type" in i and i["type"] == name and "id" in i] # Complete replacement (data == []) is forbidden if len(data) == 0: return bottle.HTTPError(403, "Forbidden") # Update all the relationships for i in data: if i["type"] == "tags": # Handle tags separately tag = db.query(database.Tag).filter_by(id=i["id"]).first() paper = db.query(database.Paper).filter_by(id=id).first() if paper is None or tag is None: # An error occurred => 403 return bottle.HTTPError(403, "Forbidden") paper.tags.append(tag) db.add(paper) db.flush() else: updated = update_relationship_backend(id, i["id"], name, db) if updated is None: # An error occurred => 403 return bottle.HTTPError(403, "Forbidden") # Return an empty 204 on success return tools.APIResponse(status=204, body="") def update_relationship_backend(left_id, right_id, name, db): """ Backend method to update a single relationship between two papers. :param left_id: ID of the paper on the left of the relationship. :param right_id: ID of the paper on the right of the relationship. :param name: Name of the relationship between the two papers. :param db: A database session. :returns: The updated left paper on success, ``None`` otherwise. """ # Load necessary resources left_paper = db.query(database.Paper).filter_by(id=left_id).first() right_paper = db.query(database.Paper).filter_by(id=right_id).first() if left_paper is None or right_paper is None: # Abort return None relationship = db.query(database.Relationship).filter_by(name=name).first() if relationship is None: relationship = database.Relationship(name=name) db.add(relationship) db.flush() # Update the relationship a = database.RelationshipAssociation(relationship_id=relationship.id) a.right_paper = right_paper left_paper.related_to.append(a) try: db.add(a) db.add(left_paper) except IntegrityError: # Unique constraint violation, relationship already exists db.rollback() return None return left_paper def create_tag(db): """ Create a new tag. .. code-block:: bash POST /tags Content-Type: application/vnd.api+json Accept: application/vnd.api+json { "data": { "name": "foobar", } } .. code-block:: json { "data": { "type": "tags", "id": 1, "attributes": { "name": "foobar", }, "links": { "self": "/tags/1" } } } :param db: A database session, injected by the ``Bottle`` plugin. :returns: An ``HTTPResponse``. """ data = json.loads(bottle.request.body.read().decode("utf-8")) # Validate the request if("data" not in data or "type" not in data["data"] or data["data"]["type"] != "tags" or "name" not in data["data"]): return bottle.HTTPError(403, "Forbidden") data = data["data"] tag = database.Tag(name=data["name"]) # Add it to the database try: db.add(tag) db.flush() except IntegrityError: # Unique constraint violation, paper already exists db.rollback() return bottle.HTTPError(409, "Conflict") # Return the resource response = { "data": tag.json_api_repr() } # Return 200 with the correct body headers = {"Location": "/tags/%d" % (tag.id,)} return tools.APIResponse(status=200, body=tools.pretty_json(response), headers=headers)