diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index fab05db..d136499 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -1,4 +1,4 @@ -name: Linting Pipeline +name: General Pipeline on: pull_request: @@ -7,7 +7,7 @@ on: jobs: linter: - name: PyLint check + name: Lint runs-on: ubuntu-latest steps: - name: linter checkout diff --git a/.gitignore b/.gitignore index b694934..7fd4608 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,50 @@ -.venv \ No newline at end of file +# Created by https://www.toptal.com/developers/gitignore/api/python,flask,git +# Edit at https://www.toptal.com/developers/gitignore?templates=python,flask,git + +### Flask ### +instance/* +!instance/.gitignore +.webassets-cache +.env + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Flask stuff: +instance/ + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +### Git ### +*.orig +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt diff --git a/P5-Metadata-api.postman_collection.json b/P5-Metadata-api.postman_collection.json index 510c6bd..ed20a80 100644 --- a/P5-Metadata-api.postman_collection.json +++ b/P5-Metadata-api.postman_collection.json @@ -1,7 +1,7 @@ { "info": { "_postman_id": "7085b715-1a0f-41de-ba42-0d41bc8c2377", - "name": "P5-Metadata api", + "name": "P5-Metadata-api", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "item": [ @@ -31,7 +31,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"uuid\": \"{{$guid}}\",\r\n \"metadata\": [\r\n { \"author\": \"John Snut\", \"pages\": 420 }\r\n ]\r\n}", + "raw": "{\r\n \"metadata\":\r\n { \r\n \"author\": \"Other Test Author\", \r\n \"pages\": 69\r\n }\r\n}", "options": { "raw": { "language": "json" @@ -39,52 +39,14 @@ } }, "url": { - "raw": "http://localhost:5000/binduuid", + "raw": "http://localhost:5000/metadata", "protocol": "http", "host": [ "localhost" ], "port": "5000", "path": [ - "binduuid" - ] - } - }, - "response": [] - }, - { - "name": "TESTING_Get uuids", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://localhost:5000/getuuids", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "getuuids" - ] - } - }, - "response": [] - }, - { - "name": "TESTING_Get triples", - "request": { - "method": "GET", - "header": [], - "url": { - "raw": "http://localhost:5000/gettriples", - "protocol": "http", - "host": [ - "localhost" - ], - "port": "5000", - "path": [ - "gettriples" + "metadata" ] } }, @@ -97,7 +59,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\r\n \"uuid\": \"b3d47bdd-5190-4c43-892c-dab379a72ebc\",\r\n \"triple\": [\r\n \"knox-kb01.srv.aau.dk/Barack_Obama\", \r\n \"http://dbpedia.org/ontology/spouse\", \r\n \"knox-kb01.srv.aau.dk/Michelle_Obama\"\r\n ]\r\n}", + "raw": "{\r\n \"uuid\": \"b7d29cec-89e0-44b3-988f-960496374ba5\",\r\n \"triple\": [\r\n \"knox-kb01.srv.aau.dk/Barack_Obama\", \r\n \"http://dbpedia.org/ontology/spouse\", \r\n \"knox-kb01.srv.aau.dk/Michelle_Obama\"\r\n ]\r\n}", "options": { "raw": { "language": "json" @@ -105,14 +67,14 @@ } }, "url": { - "raw": "http://localhost:5000/bindtriple", + "raw": "http://localhost:5000/triple", "protocol": "http", "host": [ "localhost" ], "port": "5000", "path": [ - "bindtriple" + "triple" ] } }, @@ -136,14 +98,14 @@ } }, "url": { - "raw": "http://localhost:5000/getMetadata", + "raw": "http://localhost:5000/metadata", "protocol": "http", "host": [ "localhost" ], "port": "5000", "path": [ - "getMetadata" + "metadata" ] } }, diff --git a/README.md b/README.md index 4040a3d..fcebeed 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,15 @@ docker-compose -f dev-docker-compose.yml up 4. Access the api at the specified host and port (if not changed: http://localhost:5000/) ## Documentation -Use postman and import the documentation from the file "P5-Metadata-api.postman_collection.json" +Use Postman and import the documentation from the file "P5-Metadata-api.postman_collection.json" ## To make changes 1) Clone the repository from [here](https://github.com/Knox-AAU/Metadata-api) 2) Make the changes you want to make -3) Push them into a branch -4) Make a pull request -5) Ensure pipelines pass +3) Update the api documentation +4) Push them into a branch +5) Make a pull request +6) Ensure pipelines pass ## To use linter ```bash @@ -29,6 +30,4 @@ pylint ./**/*.py ``` ## TODO -- Remove testing functions -- Remove testing functions from documentation -- Update the functionality such that if a triple already exists in the database, then ignore the triple and store the metadata in the JSON object in the database connected to that triple +See: https://github.com/orgs/Knox-AAU/projects/26/ \ No newline at end of file diff --git a/dev-docker-compose.yml b/dev-docker-compose.yml index 3e01c5a..6c6858d 100644 --- a/dev-docker-compose.yml +++ b/dev-docker-compose.yml @@ -5,8 +5,8 @@ services: image: python:3.9 container_name: metadataapi_api environment: - - PYTHON_ENV=development - - DATABASE_URI='postgresql://myuser:mypassword@metadataapi_database:5432/mydatabase' + PYTHON_ENV: development + DATABASE_URI: postgresql://myuser:mypassword@metadataapi_database:5432/mydatabase command: sh -c "pip install -r /app/requirements.txt && python /app/src/app.py" volumes: - .:/app diff --git a/prod-docker-compose.yml b/prod-docker-compose.yml index bb8f5b6..58760f0 100644 --- a/prod-docker-compose.yml +++ b/prod-docker-compose.yml @@ -5,8 +5,8 @@ services: image: python:3.9 container_name: metadataapi_api environment: - - PYTHON_ENV=production - - DATABASE_URI='postgresql://myuser:mypassword@metadataapi_database:5432/mydatabase' + PYTHON_ENV: production + DATABASE_URI: postgresql://myuser:mypassword@metadataapi_database:5432/mydatabase command: sh -c "pip install -r /app/requirements.txt && python -u /app/src/app.py" volumes: - .:/app diff --git a/src/app.py b/src/app.py index dff33ad..863a3f4 100644 --- a/src/app.py +++ b/src/app.py @@ -1,7 +1,9 @@ """ Metadataapi entrance file """ import os +from uuid import uuid4 from flask import Flask, request, jsonify from flask_sqlalchemy import SQLAlchemy +from sqlalchemy.orm.attributes import flag_modified app = Flask(__name__) @@ -15,12 +17,12 @@ # Define a simple model class Metadata(db.Model): """ Metadata table model """ - uuid = db.Column(db.String(40), primary_key=True) + uuid = db.Column(db.String(36), primary_key=True) mdata = db.Column(db.JSON, nullable=False) # mdata because metadata is reserved in declarative flask apis class TripleUUID(db.Model): """ Triple UUID model """ - uuid = db.Column(db.String(40)) + uuids = db.Column(db.ARRAY(db.String(36))) triple = db.Column(db.String(2000), primary_key=True) # Create tables @@ -31,15 +33,21 @@ class TripleUUID(db.Model): @app.route('/') def index(): """ Acts as a health check """ - return jsonify({ "success": True, "message": os.environ['PYTHON_ENV']}) - -@app.route('/binduuid', methods=['POST']) -def binduuid(): - """ Method to take in a UUID and metadata and bind it together in a database """ + return jsonify({ "success": True, "message": "Metadata api is healthy" }) + +@app.route('/metadata', methods=['POST']) +def add_metadata(): + """ + @description: Method to take in metadata and bind it together with a UUID in the database + @body: { + metadata: object + } + @returns: uuid matching the metadata + """ try: data = request.get_json() - uuid = data.get('uuid') + uuid = str(uuid4()) metadata = data.get('metadata') new_metadata = Metadata(uuid=uuid, mdata=metadata) @@ -47,81 +55,87 @@ def binduuid(): db.session.add(new_metadata) db.session.commit() - return jsonify({'success': True, 'message': f'Saved metadata to database under UUID {uuid}'}) + return jsonify({ 'success': True, 'message': uuid }) except Exception as e: - return jsonify({'success': False, 'message': str(e)}) + return jsonify({ 'success': False, 'message': str(e) }) -@app.route('/bindtriple', methods=['POST']) -def bindtriple(): - """ Method to take in a UUID and a triple and bind it together in a database """ +@app.route('/metadata', methods=['GET']) +def get_metadata(): + """ + @description Method to take in a triple and return the metadata connected to this triple + @body: { + triple: array + } + @returns: metadata connected to specified triple + """ try: data = request.get_json() - uuid = data.get('uuid') - triple = data.get('triple') + triple_as_pk = data['triple'][0] + data['triple'][1] + data['triple'][2] - triple_as_pk = triple[0] + triple[1] + triple[2] + uuid = db.session.execute(db.select(TripleUUID).filter_by(triple=triple_as_pk)).scalar() - new_triple = TripleUUID(uuid=uuid, triple=triple_as_pk) + if uuid.uuids: + metadata = Metadata.query.filter(Metadata.uuid.in_(uuid.uuids)).all() - db.session.add(new_triple) - db.session.commit() + if metadata: + res = [] - return jsonify({'success': True, 'message': f'Saved data to database under triple {triple_as_pk}'}) + for data in metadata: + res.append(data.mdata) + return jsonify({'success': True, 'message': res }) - except Exception as e: - return jsonify({'success': False, 'message': str(e)}) + return jsonify({'success': False, 'message': f'No metadata found connected to UUID: {uuid.uuids}'}) -@app.route('/getMetadata') -def get_metadata(): - """ Method to take in a triple and return the metadata connected to this triple""" + return jsonify({'success': False, 'message': f'No UUID found connected to Triple: {triple_as_pk}'}) + + except Exception as e: + return jsonify({'success': False, 'message': str(e) }) + +@app.route('/triple', methods=['POST']) +def add_triple(): + """ + @description Method to take in a UUID and a triple and bind it together in a database + @body: { + uuid: string, + triple: array + } + @returns: triple in the format that was inserted as primary key + """ try: data = request.get_json() - triple_as_pk = data['triple'][0] + data['triple'][1] + data['triple'][2] - - uuid = db.session.execute(db.select(TripleUUID).filter_by(triple=triple_as_pk)).scalar() + uuid = data.get('uuid') + triple = data.get('triple') - if uuid.uuid: - metadata = db.session.execute(db.select(Metadata).filter_by(uuid=uuid.uuid)).scalar() + triple_as_pk = triple[0] + triple[1] + triple[2] - if metadata.mdata: - return jsonify({'success': True, 'message': metadata.mdata}) + uuid_array_from_pk = db.session.execute(db.select(TripleUUID).filter_by(triple=triple_as_pk)).scalar() - return jsonify({'success': False, 'message': f'No metadata found connected to UUID: {uuid.uuid}'}) + if uuid_array_from_pk: + # Add UUID to array of UUIDS + if uuid_array_from_pk.uuids: + if uuid in uuid_array_from_pk.uuids: + # If UUID is already in array of UUIDS + return jsonify({ 'success': False, 'message': f"UUID: {uuid} already exists!" }) - return jsonify({'success': False, 'message': f'No UUID found connected to Triple: {triple_as_pk}'}) + uuid_array_from_pk.uuids.append(uuid) + flag_modified(uuid_array_from_pk, 'uuids') - except Exception as e: - return jsonify({'success': False, 'message': e}) + else: + # Create new triple instance + new_triple = TripleUUID(uuids=[uuid], triple=triple_as_pk) + db.session.add(new_triple) -# TESTING ROUTES - REMOVE WHEN STABLE # -@app.route('/getuuids') -def getuuids(): - """ Testing function to get all uuids in the database - should be removed """ - try: - mdata = Metadata.query.all() - uuid_list = [{'uuid': d.uuid, 'metadata': d.mdata} for d in mdata] + db.session.commit() - return {'success': True, 'message': uuid_list} + return jsonify({ 'success': True, 'message': triple_as_pk }) except Exception as e: - return jsonify({'success': False, 'message': str(e)}) - -@app.route('/gettriples') -def gettriples(): - """ Testing function to get all triples in the database - should be removed """ - try: + return jsonify({ 'success': False, 'message': str(e) }) - triples = TripleUUID.query.all() - triples_list = [{'uuid': d.uuid, 'triple': d.triple} for d in triples] - - return {'success': True, 'message': triples_list} - - except Exception as e: - return jsonify({'success': False, 'message': str(e)}) if __name__ == '__main__': - app.run(host='0.0.0.0', port=5000, debug=os.environ.get('PYTHON_ENV', '').lower() == 'true') + app.run(host='0.0.0.0', port=5000, debug=os.environ.get('PYTHON_ENV', '').lower() == 'development')