Added POST, DELETE API for presences, Fixed Cascade Issues, Added generic entry listings

Implemented a way to create, update and delete presences over the API.
Implemented code to list >ALL< artists similar to Art entries. Should be secured against requesting the entire database at some point.

Added debug prints to all API calls for clearer logging
master
peery 3 years ago
parent 442cefc834
commit f2821c561e

@ -1,9 +1,13 @@
from typing import List
from sqlalchemy import Column, Boolean, Float, String, Integer, ForeignKey, Table, func, ForeignKeyConstraint from sqlalchemy import Column, Boolean, Float, String, Integer, ForeignKey, Table, func, ForeignKeyConstraint
import sqlalchemy import sqlalchemy
from sqlalchemy.orm import declarative_base, relationship, load_only from sqlalchemy.orm import declarative_base, relationship, load_only
from database.models import Art, ArtnoID, Artist
# TODO read sensitive data from environment file or similar # TODO read sensitive data from environment file or similar
SQLALCHEMY_DATABASE_URL = "postgresql://artnet_editor:G606Rm9sFEXe6wfTLxVu@127.0.0.1/artnet" SQLALCHEMY_DATABASE_URL = "postgresql://artnet_editor:G606Rm9sFEXe6wfTLxVu@127.0.0.1/test_artnet"
engine = sqlalchemy.create_engine(SQLALCHEMY_DATABASE_URL, echo=True) engine = sqlalchemy.create_engine(SQLALCHEMY_DATABASE_URL, echo=True)
SessionLocal = sqlalchemy.orm.sessionmaker(autocommit=False, autoflush=False, bind=engine) SessionLocal = sqlalchemy.orm.sessionmaker(autocommit=False, autoflush=False, bind=engine)
@ -12,8 +16,8 @@ Base = sqlalchemy.orm.declarative_base()
art_tag_table = Table('art_tag', Base.metadata, art_tag_table = Table('art_tag', Base.metadata,
Column('art_id', Integer, ForeignKey('art.id')), Column('art_id', Integer, ForeignKey('art.id')),
Column('tag_id', Integer, ForeignKey('tag.tag_id'))) Column('tag_id', Integer, ForeignKey('tag.tag_id')))
artist_topic_table = Table('artist_topic', Base.metadata, artist_topic_table = Table('artist_topic', Base.metadata,
Column('artist_id', Integer, ForeignKey('artist.id')), Column('artist_id', Integer, ForeignKey('artist.id')),
Column('topic_id', Integer, ForeignKey('topic.id'))) Column('topic_id', Integer, ForeignKey('topic.id')))
@ -25,11 +29,11 @@ class DBPresence(Base):
name = Column(String(20), primary_key=True) name = Column(String(20), primary_key=True)
domain = Column(String(20), primary_key=True) domain = Column(String(20), primary_key=True)
link = Column(String, nullable=True) link = Column(String, nullable=True)
artist_id = Column(Integer, ForeignKey('artist.id'), nullable=False, index=True) artist_id = Column(Integer, ForeignKey('artist.id', ondelete="CASCADE"), nullable=False)
artist = relationship("DBArtist", backref='presences', foreign_keys=[artist_id]) artist = relationship("DBArtist", back_populates="presences")
#artists = relationship('DBArtist', foreign_keys='DBArtist.id', back_populates="presences") arts = relationship("DBArt", secondary="art_author", back_populates="presences",
arts = relationship("DBArt", secondary="art_author", viewonly=True) cascade="delete, all")
class DBArt(Base): class DBArt(Base):
@ -41,8 +45,9 @@ class DBArt(Base):
title = Column(String, nullable=True) title = Column(String, nullable=True)
link = Column(String, nullable=True) link = Column(String, nullable=True)
tags = relationship("DBTag", secondary=art_tag_table, back_populates="art") tags = relationship("DBTag", secondary=art_tag_table, back_populates="art", viewonly=True)
presences = relationship("DBPresence", secondary="art_author", viewonly=True) presences = relationship("DBPresence", secondary="art_author", back_populates="arts",
cascade="all, delete")
class DBArtPresence(Base): class DBArtPresence(Base):
@ -50,11 +55,11 @@ class DBArtPresence(Base):
presence_name = Column(String(20), primary_key=True) presence_name = Column(String(20), primary_key=True)
presence_domain = Column(String(20), primary_key=True) presence_domain = Column(String(20), primary_key=True)
art_id = Column(Integer, ForeignKey(DBArt.id), primary_key=True) art_id = Column(Integer, ForeignKey(DBArt.id, ondelete="CASCADE"), primary_key=True)
__table_args__ = ( __table_args__ = (
ForeignKeyConstraint( ForeignKeyConstraint(
["presence_name", "presence_domain"], ("presence_name", "presence_domain"),
["presence.name", "presence.domain"] ["presence.name", "presence.domain"]
), ),
) )
@ -64,9 +69,10 @@ class DBArtist(Base):
__tablename__ = "artist" __tablename__ = "artist"
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
description = Column(String, nullable=True) description = Column(String, nullable=False)
topics = relationship("DBTopic", secondary="artist_topic", back_populates="artists") topics = relationship("DBTopic", secondary="artist_topic", back_populates="artists", viewonly=True)
presences = relationship("DBPresence", back_populates="artist", cascade="all, delete")
class DBTopic(Base): # as of now unused class DBTopic(Base): # as of now unused
@ -76,7 +82,7 @@ class DBTopic(Base): # as of now unused
name = Column(String(20), unique=True, nullable=False) name = Column(String(20), unique=True, nullable=False)
description = Column(String) description = Column(String)
artists = relationship("DBArtist", secondary="artist_topic", back_populates="topics") artists = relationship("DBArtist", secondary="artist_topic", back_populates="topics", viewonly=True)
class DBTagCategory(Base): class DBTagCategory(Base):
@ -95,7 +101,9 @@ class DBTag(Base):
category_id = Column(Integer, ForeignKey('tag_category.category_id')) category_id = Column(Integer, ForeignKey('tag_category.category_id'))
category = relationship("DBTagCategory", backref='tags', foreign_keys=[category_id]) category = relationship("DBTagCategory", backref='tags', foreign_keys=[category_id])
# TODO check if cascade is required
art = relationship("DBArt", secondary=art_tag_table, back_populates="tags") art = relationship("DBArt", secondary=art_tag_table, back_populates="tags")
# TODO check if cascade is required
Base.metadata.create_all(bind=engine) Base.metadata.create_all(bind=engine)
@ -103,31 +111,79 @@ Base.metadata.create_all(bind=engine)
class Database: class Database:
__db: sqlalchemy.orm.Session = None
def __get_db(self) -> sqlalchemy.orm.Session: def __get_db(self) -> sqlalchemy.orm.Session:
db = SessionLocal() Database.__db = SessionLocal() if Database.__db is None else Database.__db
try: try:
return db return Database.__db
finally: finally:
db.close() ##Database.__db.close()
pass
def __del__(self):
Database.__db.close()
Database.__db = None
# Art # Art
def get_art_list(self):
return self.__get_db().query(DBArt).all() # TODO FIX StackOverflow
def get_art_by_id(self, art_id: int): def get_art_by_id(self, art_id: int):
return self.__get_db().query(DBArt).where(DBArt.id == art_id).first() return self.__get_db().query(DBArt).where(DBArt.id == art_id).first()
def get_art_by_hash(self, md5_hash: str): def get_art_by_hash(self, md5_hash: str):
return self.__get_db().query(DBArt).filter(func.lower(DBArt.md5_hash) == md5_hash.lower()).first() return self.__get_db().query(DBArt).filter(func.lower(DBArt.md5_hash) == md5_hash.lower()).first()
def update_art_by_id(self, art_id: int, title: str, authors: list, path: str, tags: list, link: str, def create_art_by_model(self, art: ArtnoID):
md5_hash: str): if not (isinstance(art.path, str) and len(art.path) > 0):
raise ValueError("New Art must contain a path!")
db_art = DBArt(title=art.title, md5_hash=art.hash, path=art.path, link=art.link)
if self.get_art_by_hash(art.hash) is not None:
raise ValueError("New Art must not contain already known hashes! Is this really new?")
db = self.__get_db()
db.add(db_art)
db.commit()
# TODO implement saving art <-> presence relationship as given by art.presences
if art.presences is not None:
for presence in art.presences:
db_art_presence = DBArtPresence(presence_name=presence.name, presence_domain=presence.domain,
art_id=db_art.id)
db.add(db_art_presence)
db.commit()
return db_art.id
@DeprecationWarning
def update_art_by_id(self, art_id: int, title: str, path: str, tags: list, link: str,
md5_hash: str, authors: list = None):
raise NotImplementedError raise NotImplementedError
def update_art_by_hash(self, md5_hash: str, title: str, authors: list, path: str, tags: list, link: str): @DeprecationWarning
def update_art_by_hash(self, md5_hash: str, title: str, path: str, tags: list, link: str, authors: list = None):
raise NotImplementedError raise NotImplementedError
def update_art_by_model(self, art: Art):
db_art: DBArt = self.get_art_by_id(art.id)
db_art.title = art.title if art.title is not None else db_art.title
db_art.link = art.link if art.link is not None else db_art.link
db_art.md5_hash = art.hash if art.hash is not None else db_art.md5_hash
db_art.path = art.path if art.path is not None else db_art.path
self.__get_db().commit()
def delete_art_by_id(self, art_id: int): def delete_art_by_id(self, art_id: int):
raise NotImplementedError db_art: DBArt = self.get_art_by_id(art_id)
self.__get_db().delete(db_art)
self.__get_db().commit()
@DeprecationWarning
def delete_art_by_hash(self, md5_hash: str): def delete_art_by_hash(self, md5_hash: str):
raise NotImplementedError raise NotImplementedError
@ -156,17 +212,40 @@ class Database:
# Presence # Presence
def get_presence_list(self):
return self.__get_db().query(DBPresence).all() # TODO fix StackOverflow
def get_presence(self, name: str, domain: str) -> DBPresence: def get_presence(self, name: str, domain: str) -> DBPresence:
result = self.__get_db().query(DBPresence)\ result = self.__get_db().query(DBPresence)\
.filter(func.lower(DBPresence.name) == name.lower() and .filter(func.lower(DBPresence.name) == name.lower() and
func.lower(DBPresence.domain) == domain.lower()).first() func.lower(DBPresence.domain) == domain.lower()).first()
return result return result
def create_presence(self, name: str, domain: str, artist_id: int, link: str):
if not (len(name) > 0 and len(domain) > 0):
print(f"Name: \"{name}\" Domain: \"{domain}\"")
raise ValueError("New Presence must have some name and domain!")
db_presence = DBPresence(name=name, domain=domain, artist_id=artist_id, link=link)
db = self.__get_db()
db.add(db_presence)
db.commit()
return db_presence.name, db_presence.domain
def update_presence(self, name: str, domain: str, artist_id: int, link: str): def update_presence(self, name: str, domain: str, artist_id: int, link: str):
raise NotImplementedError db_presence = self.get_presence(name=name, domain=domain)
db_presence.link = link
db_presence.artist_id = artist_id
self.__get_db().commit()
def delete_presence(self, name: str, domain: str): def delete_presence(self, name: str, domain: str):
raise NotImplementedError db_presence = self.get_presence(name=name, domain=domain)
self.__get_db().delete(db_presence)
self.__get_db().commit()
# Artist -> Presence # Artist -> Presence
@ -176,15 +255,43 @@ class Database:
# Artist # Artist
def get_artist(self, artist_id: int): def get_artist_list(self):
return self.__get_db().query(DBArtist).all() # TODO fix StackOverflow
def get_artist(self, artist_id: int) -> DBArtist:
result = self.__get_db().query(DBArtist).where(DBArtist.id == artist_id).first() result = self.__get_db().query(DBArtist).where(DBArtist.id == artist_id).first()
return result return result
def update_artist(self, artist_id: int, name: str): def create_artist(self, name: str, topics: List[int]):
raise NotImplementedError db_artist = DBArtist(description=name, topics=topics)
db = self.__get_db()
db.add(db_artist)
db.commit()
return db_artist
def search_artist(self, name: str) -> Artist:
result = self.__get_db().query(DBArtist).where(DBArtist.description == name).all()
return result
def update_artist(self, artist_id: int, name: str = None, topics: List[int] = None):
db_artist = self.get_artist(artist_id)
if name is not None:
db_artist.description = name
if topics is not None:
db_artist.topics = topics
self.__get_db().commit()
def delete_artist(self, artist_id: int): def delete_artist(self, artist_id: int):
raise NotImplementedError db_artist = self.get_artist(artist_id)
self.__get_db().delete(db_artist)
self.__get_db().commit() # TODO FIX sqlalchemy.exc.IntegrityError: (psycopg2.errors.ForeignKeyViolation)
# TODO update or delete on table "artist" violates foreign key constraint "presence_artist_id_fkey" on table "presence"
# TODO DETAIL: Key (id)=(83) is still referenced from table "presence".
# Topic # Topic

@ -2,12 +2,57 @@ from pydantic import BaseModel
from typing import List, Optional from typing import List, Optional
class Art(BaseModel): class TopicNoId(BaseModel):
id: int name: str
hash: str description: Optional[str]
path: str
title: Optional[str] = None
link: Optional[str] = None
class Config: class Config:
orm_mode = True orm_mode = True
class Topic(TopicNoId):
topic_id: int
class ArtistNoId(BaseModel):
description: str
topics: Optional[List[int]]
class Config:
orm_mode = True
class Artist(ArtistNoId):
artist_id: int
class Config:
orm_mode = True
class Presence(BaseModel):
name: str
domain: str
link: Optional[str]
artist_id: Optional[int]
class Config:
orm_mode = True
class ArtnoID(BaseModel):
hash: Optional[str]
path: Optional[str]
title: Optional[str] = None
link: Optional[str] = None
presences: Optional[List[Presence]]
class Config:
orm_mode = True
class Art(ArtnoID):
id: int
class Config:
orm_mode = True

@ -1,7 +1,9 @@
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException
import uvicorn import uvicorn
from typing import List, Tuple
from database.database import Database from database.database import Database
from database.models import Art, ArtnoID, Presence, ArtistNoId
app = FastAPI() app = FastAPI()
db = Database() db = Database()
@ -9,8 +11,9 @@ db = Database()
@app.get("/artnet/metadata/art") @app.get("/artnet/metadata/art")
async def art(id: int = None, hash: str = None, tag_id: int = None): async def art(id: int = None, hash: str = None, tag_id: int = None):
if id is None and hash is None and tag_id is None: print(f"Received GET on /artnet/metadata/art (id={id}, hash={hash}, tag_id={tag_id})")
raise HTTPException(status_code=406, detail="requires id or hash param") #if id is None and hash is None and tag_id is None:
#raise HTTPException(status_code=406, detail="requires id or hash param")
result = None result = None
if id is not None: if id is not None:
@ -20,17 +23,63 @@ async def art(id: int = None, hash: str = None, tag_id: int = None):
elif tag_id is not None: elif tag_id is not None:
result = db.get_tag_art(tag_id) result = db.get_tag_art(tag_id)
if id is None and hash is None and tag_id is None: # TODO fix against listing enormous amounts of art (because all could be large)
result = db.get_art_list()
if result is not None: if result is not None:
return result return result
raise HTTPException(status_code=404, detail="Art was not found!") raise HTTPException(status_code=404, detail="Art was not found!")
@app.post("/artnet/metadata/art")
async def art(art: ArtnoID, id: int = None):
print(f"Received POST on /artnet/metadata/art (id={id}) body: art=({art})")
if id is None: # create new art entry
if art.presences is None: # tried to create art without any presence
raise HTTPException(status_code=422, detail="No presences were listed")
try:
for presence in art.presences:
r = db.get_presence(name=presence.name, domain=presence.domain)
#if db.get_presence(name=presence.name, domain=presence.domain) is None:
# raise HTTPException(status_code=422,
# detail=f"Presence ({presence.name}, {presence.domain}) is unknown!")
#db.create # create presence-art relationship
# TODO validate & create presence-art relationship
new_id = db.create_art_by_model(art)
return {"id": new_id}
except ValueError as e:
raise HTTPException(status_code=422, detail=str(e))
else: # update existing art entry
if db.get_art_by_id(id) is None:
return HTTPException(status_code=404, detail="The specified art could not be found!")
# TODO implement linking to one or multiple presences
# TODO enforcing at least one presence
updated_art = Art(id=id)
updated_art.hash = art.hash
updated_art.link = art.link
updated_art.title = art.title
updated_art.path = art.path
db.update_art_by_model(updated_art)
return True
@app.delete("/artnet/metadata/art")
async def art(id: int):
print(f"Received DELETE on /artnet/metadata/art (id={id})")
if db.get_art_by_id(id) is None:
raise HTTPException(status_code=404, detail="Art has not been found!")
db.delete_art_by_id(id)
@app.get("/artnet/metadata/presence") @app.get("/artnet/metadata/presence")
async def presence(name: str = None, domain: str = None, artist_id: int = None, art_id: int = None, async def presence(name: str = None, domain: str = None, artist_id: int = None, art_id: int = None,
art_md5: str = None): art_md5: str = None):
if name is None and domain is None and artist_id is None and art_id is None and art_md5 is None: print(f"Received GET on /artnet/metadata/presence (name={name}, domain={domain}, artist_id={artist_id}"
raise HTTPException(status_code=406, detail="You must query with at least one parameter!") f", art_id={art_id}, art_md5={art_md5})")
result = None result = None
if artist_id is not None and name is None and domain is None: if artist_id is not None and name is None and domain is None:
result = db.get_artist_presences(artist_id) result = db.get_artist_presences(artist_id)
@ -44,28 +93,84 @@ async def presence(name: str = None, domain: str = None, artist_id: int = None,
elif name is not None and domain is not None: elif name is not None and domain is not None:
result = db.get_presence(name, domain) result = db.get_presence(name, domain)
if name is None and domain is None and artist_id is None and art_id is None and art_md5 is None:
result = db.get_presence_list()
#raise HTTPException(status_code=406, detail="You must query with at least one parameter!")
if result is not None: if result is not None:
return result return result
raise HTTPException(status_code=404, detail="Presence was not found!") raise HTTPException(status_code=404, detail="Presence was not found!")
@app.get("/artnet/metadata/artist") @app.post("/artnet/metadata/presence")
async def artist(id: int = None, topic_id: int = None): async def presence(presence: Presence):
if id is None and topic_id is None: print(f"Received POST on /artnet/metadata/presence body: presence={presence}")
raise HTTPException(status_code=406) curr_presence = db.get_presence(name=presence.name, domain=presence.domain)
r = db.get_artist(presence.artist_id)
if r is None:
raise HTTPException(status_code=406, detail="Invalid Artist ID! Could not find artist.")
if curr_presence is None: # must be new presence
db.create_presence(name=presence.name, domain=presence.domain, artist_id=presence.artist_id, link=presence.link)
else: # editing current presence
db.update_presence(name=curr_presence.name, domain=curr_presence.domain,
artist_id=presence.artist_id, link=presence.link)
@app.delete("/artnet/metadata/presence")
async def presence(name: str, domain: str):
print(f"Received DELETE on /artnet/metadata/presence (name={name}, domain={domain})")
if db.get_presence(name, domain) is None:
raise HTTPException(status_code=404, detail="Presence has not been found!")
db.delete_presence(name, domain)
@app.get("/artnet/metadata/artist")
async def artist(id: int = None, topic_id: int = None, description: str = None):
print(f"Received GET on /artnet/metadata/artist (id={id}, topic_id={topic_id}, description={description})")
result = None result = None
if id is None and topic_id is None and description is None:
result = db.get_artist_list()
if id is not None: if id is not None:
result = db.get_artist(id) result = db.get_artist(id)
elif topic_id is not None: elif topic_id is not None:
result = db.get_topic_artists(topic_id) result = db.get_topic_artists(topic_id)
elif description is not None:
result = db.search_artist(description)
if result is not None: if result is not None:
return result return result
raise HTTPException(status_code=404, detail="Artist was not found!") raise HTTPException(status_code=404, detail="Artist was not found!")
@app.post("/artnet/metadata/artist")
async def artist(artist: ArtistNoId, id: int = None):
print(f"Received POST on /artnet/metadata/artist (id={id}) body: artist={artist}")
if id is None: # create new artist
db_artist = db.create_artist(name=artist.description, topics=artist.topics)
return db_artist.id
else:
if db.get_artist(id) is not None:
db.update_artist(artist_id=id, name=artist.description, topics=artist.topics)
else:
raise HTTPException(status_code=404, detail="Tried to edit unknown artist. ID was not found!")
@app.delete("/artnet/metadata/artist")
async def artist(id: int):
print(f"Received DELETE on /artnet/metadata/artist (id={id})")
if db.get_artist(id) is None:
raise HTTPException(status_code=404, detail="Tried to delete unknown artist. ID was not found!")
db.delete_artist(id)
@app.get("/artnet/metadata/topic") @app.get("/artnet/metadata/topic")
async def topic(name: str = None, id: int = None, artist_id: int = None): async def topic(name: str = None, id: int = None, artist_id: int = None):
print(f"Received GET on /artnet/metadata/topic (name={name}, id={id}, artist_id={artist_id})")
if name is None and id is None and artist_id is None: if name is None and id is None and artist_id is None:
raise HTTPException(status_code=406) raise HTTPException(status_code=406)

Loading…
Cancel
Save