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
import sqlalchemy
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
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)
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,
Column('art_id', Integer, ForeignKey('art.id')),
Column('tag_id', Integer, ForeignKey('tag.tag_id')))
Column('art_id', Integer, ForeignKey('art.id')),
Column('tag_id', Integer, ForeignKey('tag.tag_id')))
artist_topic_table = Table('artist_topic', Base.metadata,
Column('artist_id', Integer, ForeignKey('artist.id')),
Column('topic_id', Integer, ForeignKey('topic.id')))
@ -25,11 +29,11 @@ class DBPresence(Base):
name = Column(String(20), primary_key=True)
domain = Column(String(20), primary_key=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])
#artists = relationship('DBArtist', foreign_keys='DBArtist.id', back_populates="presences")
arts = relationship("DBArt", secondary="art_author", viewonly=True)
artist = relationship("DBArtist", back_populates="presences")
arts = relationship("DBArt", secondary="art_author", back_populates="presences",
cascade="delete, all")
class DBArt(Base):
@ -41,8 +45,9 @@ class DBArt(Base):
title = Column(String, nullable=True)
link = Column(String, nullable=True)
tags = relationship("DBTag", secondary=art_tag_table, back_populates="art")
presences = relationship("DBPresence", secondary="art_author", viewonly=True)
tags = relationship("DBTag", secondary=art_tag_table, back_populates="art", viewonly=True)
presences = relationship("DBPresence", secondary="art_author", back_populates="arts",
cascade="all, delete")
class DBArtPresence(Base):
@ -50,11 +55,11 @@ class DBArtPresence(Base):
presence_name = 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__ = (
ForeignKeyConstraint(
["presence_name", "presence_domain"],
("presence_name", "presence_domain"),
["presence.name", "presence.domain"]
),
)
@ -64,9 +69,10 @@ class DBArtist(Base):
__tablename__ = "artist"
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
@ -76,7 +82,7 @@ class DBTopic(Base): # as of now unused
name = Column(String(20), unique=True, nullable=False)
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):
@ -95,7 +101,9 @@ class DBTag(Base):
category_id = Column(Integer, ForeignKey('tag_category.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")
# TODO check if cascade is required
Base.metadata.create_all(bind=engine)
@ -103,31 +111,79 @@ Base.metadata.create_all(bind=engine)
class Database:
__db: sqlalchemy.orm.Session = None
def __get_db(self) -> sqlalchemy.orm.Session:
db = SessionLocal()
Database.__db = SessionLocal() if Database.__db is None else Database.__db
try:
return db
return Database.__db
finally:
db.close()
##Database.__db.close()
pass
def __del__(self):
Database.__db.close()
Database.__db = None
# 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):
return self.__get_db().query(DBArt).where(DBArt.id == art_id).first()
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()
def update_art_by_id(self, art_id: int, title: str, authors: list, path: str, tags: list, link: str,
md5_hash: str):
def create_art_by_model(self, art: ArtnoID):
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
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
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):
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):
raise NotImplementedError
@ -156,17 +212,40 @@ class Database:
# 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:
result = self.__get_db().query(DBPresence)\
.filter(func.lower(DBPresence.name) == name.lower() and
func.lower(DBPresence.domain) == domain.lower()).first()
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):
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):
raise NotImplementedError
db_presence = self.get_presence(name=name, domain=domain)
self.__get_db().delete(db_presence)
self.__get_db().commit()
# Artist -> Presence
@ -176,15 +255,43 @@ class Database:
# 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()
return result
def update_artist(self, artist_id: int, name: str):
raise NotImplementedError
def create_artist(self, name: str, topics: List[int]):
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):
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

@ -2,12 +2,57 @@ from pydantic import BaseModel
from typing import List, Optional
class Art(BaseModel):
id: int
hash: str
path: str
title: Optional[str] = None
link: Optional[str] = None
class TopicNoId(BaseModel):
name: str
description: Optional[str]
class Config:
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
import uvicorn
from typing import List, Tuple
from database.database import Database
from database.models import Art, ArtnoID, Presence, ArtistNoId
app = FastAPI()
db = Database()
@ -9,8 +11,9 @@ db = Database()
@app.get("/artnet/metadata/art")
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:
raise HTTPException(status_code=406, detail="requires id or hash param")
print(f"Received GET on /artnet/metadata/art (id={id}, hash={hash}, tag_id={tag_id})")
#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
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:
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:
return result
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")
async def presence(name: str = None, domain: str = None, artist_id: int = None, art_id: int = 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:
raise HTTPException(status_code=406, detail="You must query with at least one parameter!")
print(f"Received GET on /artnet/metadata/presence (name={name}, domain={domain}, artist_id={artist_id}"
f", art_id={art_id}, art_md5={art_md5})")
result = None
if artist_id is not None and name is None and domain is None:
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:
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:
return result
raise HTTPException(status_code=404, detail="Presence was not found!")
@app.get("/artnet/metadata/artist")
async def artist(id: int = None, topic_id: int = None):
if id is None and topic_id is None:
raise HTTPException(status_code=406)
@app.post("/artnet/metadata/presence")
async def presence(presence: Presence):
print(f"Received POST on /artnet/metadata/presence body: presence={presence}")
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
if id is None and topic_id is None and description is None:
result = db.get_artist_list()
if id is not None:
result = db.get_artist(id)
elif topic_id is not None:
result = db.get_topic_artists(topic_id)
elif description is not None:
result = db.search_artist(description)
if result is not None:
return result
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")
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:
raise HTTPException(status_code=406)

Loading…
Cancel
Save