From b8e07eb528d345bc7a415d67513149713575163c Mon Sep 17 00:00:00 2001 From: Peery Date: Tue, 11 Jan 2022 23:55:27 +0100 Subject: [PATCH] Introduced URL encoding, Art2Presence relations URL parameters are now URL decoded by the webservice The tests now URL encode the parameters for every request. Art2Presence relations are now creatable with POST requests to /artnet/metadata/art and listing the presences in the body. Art2Presence tests have been introduced with relate_art.py Also updated column name artist.description artist.name to fit a db schema update. --- database/models.py | 30 +++++++++- main.py | 71 ++++++++++++++--------- tests/createAPI/create_delete_art.py | 18 +++--- tests/createAPI/create_delete_artist.py | 3 +- tests/createAPI/create_delete_presence.py | 4 +- tests/relationAPI/relate_art.py | 57 +++++++++++++++--- tests/run_tests.py | 32 ++++++++-- 7 files changed, 163 insertions(+), 52 deletions(-) diff --git a/database/models.py b/database/models.py index af45df3..747f378 100644 --- a/database/models.py +++ b/database/models.py @@ -15,7 +15,7 @@ class Topic(TopicNoId): class ArtistNoId(BaseModel): - description: str + name: str topics: Optional[List[int]] class Config: @@ -50,6 +50,34 @@ class ArtnoID(BaseModel): orm_mode = True +class TagnoID(BaseModel): + name: Optional[str] + description: Optional[str] + category_id: Optional[TagCategory] + + class Config: + orm_mode = True + + +class Tag(TagnoID): + tag_ID: 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]] + tags: Optional[List[Tag]] + + class Config: + orm_mode = True + + class Art(ArtnoID): id: int diff --git a/main.py b/main.py index 0e67d84..ebcbd72 100644 --- a/main.py +++ b/main.py @@ -1,3 +1,4 @@ +from urllib.parse import unquote from fastapi import FastAPI, HTTPException import uvicorn from typing import List, Tuple @@ -11,9 +12,8 @@ db = Database() @app.get("/artnet/metadata/art") async def art(id: int = None, hash: str = None, tag_id: int = None): + hash = unquote(hash) if hash is not None else None 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: @@ -38,46 +38,59 @@ async def art(art: ArtnoID, id: int = None): 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) + new_id = db.create_art_by_model(art).id 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!") + raise 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 + if art.presences is not None: + updated_art.presences = art.presences + 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})") +async def art(id: int, presence_name: str = None, presence_domain: str = None): + presence_name = unquote(presence_name) if presence_name is not None else None + presence_domain = unquote(presence_domain) if presence_domain is not None else None + print(f"Received DELETE on /artnet/metadata/art (id={id}, presence_name={presence_name}, " + f"presence_domain={presence_domain})") + print("All art available is", [a.id for a in db.get_art_list()]) 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) + + if id is not None and presence_name is None and presence_domain is None: + db.delete_art_by_id(id) + return + elif id is not None and presence_name is not None and presence_domain is not None: + try: + db.delete_art_presences_by_id(art_id=id, presence_name=presence_name, presence_domain=presence_domain) + except ValueError: + raise HTTPException(status_code=404, detail="The art-presence relation could not be found!") + return + + raise HTTPException(status_code=422, detail="Unknown parameter combination!") @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): + name = unquote(name) if name is not None else None + domain = unquote(domain) if domain is not None else None + art_md5 = unquote(art_md5) if art_md5 is not None else None 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 @@ -127,18 +140,19 @@ async def presence(name: str, domain: str): @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})") +async def artist(id: int = None, topic_id: int = None, name: str = None): + name = unquote(name) if name is not None else None + print(f"Received GET on /artnet/metadata/artist (id={id}, topic_id={topic_id}, name={name})") result = None - if id is None and topic_id is None and description is None: + if id is None and topic_id is None and name 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) + elif name is not None: + result = db.search_artist(name) if result is not None: return result @@ -149,12 +163,12 @@ async def artist(id: int = None, topic_id: int = None, description: str = None): 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) + db_artist = db.create_artist(name=artist.name, 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) + db.update_artist(artist_id=id, name=artist.name, topics=artist.topics) else: raise HTTPException(status_code=404, detail="Tried to edit unknown artist. ID was not found!") @@ -167,9 +181,9 @@ async def artist(id: int): db.delete_artist(id) - @app.get("/artnet/metadata/topic") async def topic(name: str = None, id: int = None, artist_id: int = None): + name = unquote(name) if name is not None else 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) @@ -191,6 +205,8 @@ async def topic(name: str = None, id: int = None, artist_id: int = None): @app.get("/artnet/metadata/tag") async def tag(id: int = None, name: str = None, fuzzy: bool = False, category: int = None): + name = unquote(name) if name is not None else None + print(f"Received GET on /artnet/metadata/tag (name={name}, id={id}, name={category}, fuzzy={fuzzy})") if id is None and name is None and category is None: raise HTTPException(status_code=406) @@ -216,8 +232,11 @@ async def tag(id: int = None, name: str = None, fuzzy: bool = False, category: i @app.get("/artnet/metadata/category") async def category(name: str = None, id: int = None): + name = unquote(name) if name is not None else None + print(f"Received GET on /artnet/metadata/category (name={name}, id={id})") if name is None and id is None: - raise HTTPException(status_code=406) + result = db.get_category_list() + return result result = None if id is not None: diff --git a/tests/createAPI/create_delete_art.py b/tests/createAPI/create_delete_art.py index 82c6e32..ba4efed 100644 --- a/tests/createAPI/create_delete_art.py +++ b/tests/createAPI/create_delete_art.py @@ -1,4 +1,5 @@ import requests +import urllib.parse import json from typing import List, Tuple @@ -63,7 +64,7 @@ def update_art_entries(url: str, port: int): raise Exception("Update Art Entry Test: FAILED") for i in range(len(test_art_entries)): - r = requests.get(f"http://{url}:{port}/artnet/metadata/art?id={test_art_entries[i]['ID']}") + r = requests.get(f"http://{url}:{port}/artnet/metadata/art?id={urllib.parse.quote(str(test_art_entries[i]['ID']))}") if json.loads(r.text)["title"].split(" ")[-1] != "updated": raise Exception("Update Art Entry Test: Failed (unexpected or no updated title)") if json.loads(r.text)["link"] != test_art_entries[i]["link"]: @@ -79,10 +80,7 @@ def delete_art_entries(url: str, port: int): if "id" in arts[i].keys(): r = delete_art_by_id(url, port, art_id=arts[i]["id"]) else: - try: - r = delete_art_by_hash(url, port, md5_hash=arts[i]["hash"]) - except ValueError: - r = None + r = delete_art_by_hash(url, port, md5_hash=arts[i]["hash"]) if r is not None and r.status_code != 200: raise Exception(f"Could not delete the art! {r.status_code}, {r.text}") @@ -112,20 +110,20 @@ def update_art(url: str, port: int, art_id: int, md5_hash: str = None, path: str if presences is not None: body["presences"] = [{"name": presence[0], "domain": presence[1]} for presence in presences] - r = requests.post(f"http://{url}:{port}/artnet/metadata/art?id={art_id}", + r = requests.post(f"http://{url}:{port}/artnet/metadata/art?id={urllib.parse.quote(str(art_id))}", json=body) return r def delete_art_by_id(url: str, port: int, art_id: int): - r = requests.delete(f"http://{url}:{port}/artnet/metadata/art?id={art_id}") + r = requests.delete(f"http://{url}:{port}/artnet/metadata/art?id={urllib.parse.quote(str(art_id))}") return r def delete_art_by_hash(url: str, port: int, md5_hash: str): - r1 = requests.get(f"http://{url}:{port}/artnet/metadata/art?hash={md5_hash}") + r1 = requests.get(f"http://{url}:{port}/artnet/metadata/art?hash={urllib.parse.quote(md5_hash)}") if r1.status_code != 200: raise ValueError("Could not delete art because it could not be found!") data = json.loads(str(r1.text)) @@ -154,7 +152,7 @@ def run_art_test(url: str, port: int): print(f"Creating {len(test_art_entries)} art entries ...") create_art_entries(url, port) - create_art_result = False if not len(list_art(url, port)) == 3 else True + create_art_result = False if not len(list_art(url, port)) == len(test_art_entries) else True print(f"Found {len(list_art(url, port))} art entries!") if not create_art_result: print(f"Failure! Unexpected number of art entries!") @@ -164,8 +162,6 @@ def run_art_test(url: str, port: int): update_art_result = True # given, the update function would raise an exception on error print(f"Found {len(list_art(url, port))} art entries!") - # TODO write test for updating presence-art relation - print("Deleting all art entries ...") delete_art_entries(url, port) delete_art_result = False if not len(list_art(url, port)) == 0 else True diff --git a/tests/createAPI/create_delete_artist.py b/tests/createAPI/create_delete_artist.py index 215ddfa..02d40f7 100644 --- a/tests/createAPI/create_delete_artist.py +++ b/tests/createAPI/create_delete_artist.py @@ -1,4 +1,5 @@ import requests +import urllib.parse from typing import List import json @@ -56,7 +57,7 @@ def create_artist(url: str, port: int, name: str, topics: List[int]): def delete_artist(url: str, port: int, id: int): - r = requests.delete(f"http://{url}:{port}/artnet/metadata/artist?id={id}") + r = requests.delete(f"http://{url}:{port}/artnet/metadata/artist?id={urllib.parse.quote(str(id))}") return r diff --git a/tests/createAPI/create_delete_presence.py b/tests/createAPI/create_delete_presence.py index a697158..7cebb2c 100644 --- a/tests/createAPI/create_delete_presence.py +++ b/tests/createAPI/create_delete_presence.py @@ -1,4 +1,5 @@ import requests +import urllib.parse import json from typing import List, Tuple @@ -30,7 +31,8 @@ def create_presence(url: str, port: int, name: str, domain: str, artist_id: int) def delete_presence(url: str, port: int, name: str, domain: str): - r = requests.delete(f"http://{url}:{port}/artnet/metadata/presence?name={name}&domain={domain}") + r = requests.delete(f"http://{url}:{port}/artnet/metadata/presence?name={urllib.parse.quote(name)}" + f"&domain={urllib.parse.quote(domain)}") if r.status_code != 200: print(f"Deleting Presence entry {name}@{domain}: FAILED") diff --git a/tests/relationAPI/relate_art.py b/tests/relationAPI/relate_art.py index 95e2754..8dd36c9 100644 --- a/tests/relationAPI/relate_art.py +++ b/tests/relationAPI/relate_art.py @@ -7,10 +7,10 @@ from tests.createAPI.create_delete_presence import test_presence_entries, list_p from tests.createAPI.create_delete_art import test_art_entries, list_art, create_art_entries, delete_art_entries test_art_presence_relations = [ - {"art_hash": test_art_entries[0]["hash"], "presence_name": test_presence_entries[0]["name"], "presence_domain": test_presence_entries[0]["domain"]}, - {"art_hash": test_art_entries[1]["hash"], "presence_name": test_presence_entries[0]["name"], "presence_domain": test_presence_entries[0]["domain"]}, - {"art_hash": test_art_entries[2]["hash"], "presence_name": test_presence_entries[0]["name"], "presence_domain": test_presence_entries[0]["domain"]}, - {"art_hash": test_art_entries[2]["hash"], "presence_name": test_presence_entries[1]["name"], "presence_domain": test_presence_entries[1]["domain"]}, + {"art_hash": test_art_entries[0]["hash"], "presence_name": test_art_entries[0]["presences"][0][0], "presence_domain": test_art_entries[0]["presences"][0][1]}, + {"art_hash": test_art_entries[1]["hash"], "presence_name": test_art_entries[1]["presences"][0][0], "presence_domain": test_art_entries[1]["presences"][0][1]}, + {"art_hash": test_art_entries[2]["hash"], "presence_name": test_art_entries[2]["presences"][0][0], "presence_domain": test_art_entries[1]["presences"][0][1]}, + {"art_hash": test_art_entries[2]["hash"], "presence_name": test_art_entries[2]["presences"][1][0], "presence_domain": test_art_entries[2]["presences"][1][1]}, ] @@ -38,6 +38,8 @@ def list_art_to_presence_relations(url: str, port: int): def art_hash_to_id(arts: List[dict]): """ Converts a list of arts into the format of {hash: id, hash1: id1} + + This allows reverse mapping of the usual ID->hash to hash->ID. :param arts: :return: """ @@ -77,6 +79,35 @@ def create_art_presence_relation(url: str, port: int, presence_name: str, presen return r +def delete_art_presence_relations(url: str, port: int): + arts = list_art(url, port) + art_hash_id = art_hash_to_id(arts) + for i in range(len(test_art_presence_relations)): + print(f"Deleting art-presence relation with id:{art_hash_id[test_art_presence_relations[i]['art_hash']]} " + f"presence_name:{test_art_presence_relations[i]['presence_name']} " + f"presence_domain:{test_art_presence_relations[i]['presence_domain']}") + r = delete_art_presence_relation(url, port, art_id=art_hash_id[test_art_presence_relations[i]["art_hash"]], + presence_name=test_art_presence_relations[i]["presence_name"], + presence_domain=test_art_presence_relations[i]["presence_domain"]) + if r.status_code != 200: + print(f"Tried to delete art-presence relation with name:{test_art_presence_relations[i]['presence_name']} " + f"domain:{test_art_presence_relations[i]['presence_domain']}" + f" art_id:{art_hash_id[test_art_presence_relations[i]['art_hash']]}") + print(f"Deleting Relation failed with {r.status_code} and reason {r.text}!") + raise Exception("Couldn't delete art-presence relation!") + + l = list_art_to_presence_relations(url, port) + if len(l[0]) == 0 and len(l[1]) == 0 and len(l[2]) == 0: + return True + else: + return False + + +def delete_art_presence_relation(url: str, port: int, presence_name: str, presence_domain: str, art_id: int): + return requests.delete(f"http://{url}:{port}/artnet/metadata/art?id={art_id}&presence_name={presence_name}" + f"&presence_domain={presence_domain}") + + def run_art_presence_relation_test(url, port): print() print("----------------") @@ -87,7 +118,7 @@ def run_art_presence_relation_test(url, port): print("Deleting leftover art entries ...") delete_art_entries(url, port) - print(f"Starting presence test with " + print(f"Leftover entries for the test are " f"({len(list_presences(url, port))}) presences, " f"({len(list_art(url, port))}) art!") @@ -96,6 +127,10 @@ def run_art_presence_relation_test(url, port): create_presence_entries(url, port) create_art_entries(url, port) + print(f"Starting art2presence test with " + f"({len(list_presences(url, port))}) presences, " + f"({len(list_art(url, port))}) art!") + art = list_art(url, port) # debug presences = list_presences(url, port) # debug @@ -109,6 +144,14 @@ def run_art_presence_relation_test(url, port): create_result = create_art_presence_relations(url, port) print(f"Found {sum([len(x) for x in list_art_to_presence_relations(url, port)])} art-presence relations!") - # TODO delete art-presence relation test + delete_result = delete_art_presence_relations(url, port) + print(f"Found {sum([len(x) for x in list_art_to_presence_relations(url, port)])} art-presence relations!") - return create_result + delete_art_entries(url, port) + delete_artist_entries(url, port) + + print(f"Ending art2presence test with " + f"({len(list_presences(url, port))}) presences, " + f"({len(list_art(url, port))}) art!") + + return create_result, delete_result diff --git a/tests/run_tests.py b/tests/run_tests.py index c0def89..039e4d3 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -1,6 +1,7 @@ from tests.createAPI.create_delete_art import run_art_test, list_art, delete_art_entries from tests.createAPI.create_delete_presence import run_presence_test, list_presences from tests.createAPI.create_delete_artist import delete_artist_entries, list_artists, run_artist_test +from tests.createAPI.create_delete_tag_category import delete_category_entries, list_tag_categories, run_tag_category_test from tests.relationAPI.relate_art import run_art_presence_relation_test @@ -18,10 +19,15 @@ def run_tests(url: str, port: int): if len(list_art(url, port)) > 0: print("Deleting leftover art ...") delete_art_entries(url, port) + if len(list_tag_categories(url, port)) > 0: + print("Deleting leftover tag categories ...") + delete_category_entries(url, port) create_artist_result, delete_artist_result = run_artist_test(url, port) create_presence_result, delete_presence_result, cascade_delete_presence_result = run_presence_test(url, port) create_art_result, update_art_result, delete_art_result = run_art_test(url, port) + create_art2presence_result, delete_art2presence_result = run_art_presence_relation_test(url, port) + create_category_result, update_category_result, delete_category_result = run_tag_category_test(url, port) print() print("-------- Test Results ---------") @@ -32,16 +38,32 @@ def run_tests(url: str, port: int): print(f"Presences: {len(r)}, {r}") r = list_art(url, port) print(f"Art: {len(r)}, {r}") + r = list_tag_categories(url, port) + print(f"Tag Categories: {len(r)}, {r}") print() print("Functions:") - print(f"Artists: \tCreate: {create_artist_result} \tUpdate: {'N/A'} \tDelete: {delete_artist_result} \t(Direct)") - print(f"Presences: \tCreate: {create_presence_result} \tUpdate: {'N/A'} " + print(f"Artists: \t\t\t\tCreate: {create_artist_result} \tUpdate: {'N/A'} " + f"\tDelete: {delete_artist_result} \t(Direct)") + print(f"Presences: \t\t\t\tCreate: {create_presence_result} \tUpdate: {'N/A'} " f"\tDelete: {delete_presence_result} \t(Direct), {cascade_delete_presence_result} \t(cascade@artist)") - print(f"Art: \t\tCreate: {create_art_result} \tUpdate: {update_art_result} " + print(f"Art: \t\t\t\t\tCreate: {create_art_result} \tUpdate: {update_art_result} " f"\tDelete: {delete_art_result} \t(Direct)") + print(f"Art2Presence: \t\t\tCreate: {create_art2presence_result} \tUpdate: {'N/A'} " + f"\tDelete: {delete_art2presence_result} \t(Direct)") + print(f"Tag Category: \t\t\tCreate: {create_category_result} \tUpdate: {update_category_result} " + f"\tDelete: {delete_category_result} \t(Direct)") + print(f"Tag: \t\t\t\t\tN/A") + print(f"(Artist) Topic: \t\tN/A") + print(f"Art Collection: \t\tN/A") + print(f"Art2Art Collection: \tN/A") + print(f"Artist2Topic: \t\t\tN/A") + print(f"Art2Tag: \t\t\t\tN/A") + print(f"Tag Alias: \t\t\t\tN/A") + print(f"Tag Implication: \t\tN/A") + print("-------------------------------") if __name__ == "__main__": - #run_tests("127.0.0.1", 8000) - run_art_presence_relation_test("127.0.0.1", 8000) + run_tests("127.0.0.1", 8000) +