From a8f8590c5a53d0fe7f6f001000dba651868c7ddf Mon Sep 17 00:00:00 2001 From: "Phyks (Lucas Verney)" Date: Fri, 25 Dec 2015 20:09:56 +0100 Subject: [PATCH] Add basic routes to update relationships --- .gitignore | 1 + database.py | 30 +++++++++++++++++++++-- main.py | 12 ++++++++-- routes/get.py | 7 ++++++ routes/post.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index f78e190..a0b8c94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ __pycache__ docs/ +dev.db diff --git a/database.py b/database.py index 93b55d0..1fc34d7 100644 --- a/database.py +++ b/database.py @@ -1,9 +1,11 @@ """ This file contains the database schema in SQLAlchemy format. """ -from sqlalchemy import event, Column, Integer, String +from sqlalchemy import event +from sqlalchemy import Column, ForeignKey, Integer, String from sqlalchemy.engine import Engine from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import relationship as sqlalchemy_relationship Base = declarative_base() @@ -18,11 +20,27 @@ def set_sqlite_pragma(dbapi_connection, connection_record): cursor.close() +# TODO: Backref +class Association(Base): + # Relationships are to be read "left RELATION right" + __tablename__ = "association" + id = Column(Integer, primary_key=True) + left_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE")) + right_id = Column(Integer, ForeignKey("papers.id", ondelete="CASCADE")) + relationship_id = Column(Integer, + ForeignKey("relationships.id", + ondelete="CASCADE")) + right_paper = sqlalchemy_relationship("Paper", foreign_keys=right_id) + relationship = sqlalchemy_relationship("Relationship") + + class Paper(Base): - __tablename__ = 'papers' + __tablename__ = "papers" id = Column(Integer, primary_key=True) doi = Column(String(), nullable=True, unique=True) arxiv_id = Column(String(25), nullable=True, unique=True) + related = sqlalchemy_relationship("Association", + foreign_keys="Association.left_id") def __repr__(self): return "" % ( @@ -49,3 +67,11 @@ class Paper(Base): # TODO } } + + +class Relationship(Base): + __tablename__ = "relationships" + id = Column(Integer, primary_key=True) + name = Column(String(), unique=True) + associations = sqlalchemy_relationship("Association", + back_populates="relationship") diff --git a/main.py b/main.py index 15ac4e4..b3f6494 100755 --- a/main.py +++ b/main.py @@ -8,7 +8,8 @@ import routes import tools # Initialize db and include the SQLAlchemy plugin in bottle -engine = create_engine('sqlite:///:memory:', echo=True) +# engine = create_engine('sqlite:///:memory:', echo=True) +engine = create_engine('sqlite:///dev.db', echo=True) app = bottle.Bottle() plugin = sqlalchemy.Plugin( @@ -33,6 +34,7 @@ app.install(plugin) # Routes +# TODO: Routes for deletion @app.get("/") def index(): return tools.APIResponse(tools.pretty_json({ @@ -41,13 +43,19 @@ def index(): app.get("/papers", callback=routes.get.fetch_papers) app.get("/papers/", callback=routes.get.fetch_by_id) +app.get("/papers//relationships/", + callback=routes.get.fetch_relationship) # TODO: Fetch relationships app.post("/papers", callback=routes.post.create_paper) -# TODO: Update relationships +app.post("/papers//relationships/", + callback=routes.post.update_relationships) +# Complete replacement of relationships is forbidden +app.route("/papers//relationships/", method="PATCH", + callback=lambda id, name: bottle.HTTPError(403, "Forbidden")) if __name__ == "__main__": diff --git a/routes/get.py b/routes/get.py index 1f55829..b74d560 100644 --- a/routes/get.py +++ b/routes/get.py @@ -97,3 +97,10 @@ def fetch_by_id(id, db): "data": resource.json_api_repr() })) return bottle.HTTPError(404, "Not found") + + +def fetch_relationship(id, name, db): + """ + TODO + """ + pass diff --git a/routes/post.py b/routes/post.py index 7443faf..da2179c 100644 --- a/routes/post.py +++ b/routes/post.py @@ -138,3 +138,67 @@ def create_by_arxiv(arxiv, db): # Return the paper return paper + + +def update_relationships(id, name, db): + """ + Update the relationships associated to a given paper. + + :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: + 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.Association(relationship_id=relationship.id) + a.right_paper = right_paper + left_paper.related.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