good example on OpenFaaS<\/a> of how relative imports work in Python.<\/p>\nFirst I tested that the local import and SQLAlchemy imports worked before moving on.<\/p>\n
# Modified handler.py\n...\nimport mariadb\n# Added this line to import the LegoSet model class\nfrom .models import LegoSet\n\n<\/code><\/pre>\nBuild and test<\/p>\n
$ .\/up.sh\n$ curl -s http:\/\/192.168.64.4:9999\/function\/legodb\/legosets | jq\n# Still works (it did take some trial and error to get here)\n<\/code><\/pre>\nGetting all the Lego sets from the database<\/h2>\n Edit models.py<\/code><\/p>\nfrom sqlalchemy import create_engine, Column, Integer, String\nfrom sqlalchemy.ext.declarative import declarative_base\nfrom sqlalchemy.orm import sessionmaker\n\nBase = declarative_base()\n\nclass LegoSet(Base):\n __tablename__ = 'legosets'\n id = Column('pkID', Integer, primary_key=True, nullable=False)\n legoID = Column('LegoID', Integer, unique=True, nullable=False)\n description = Column('Description',String(200))\n productURL = Column('ProductURL',String(4096))\n imageURL = Column('ImageURL',String(4096))\n\ndef database_session(host, port, user, password, schema):\n engine = create_engine(f"mariadb+mariadbconnector:\/\/{user}:{password}@{host}:{port}\/{schema}?charset=utf8mb4")\n \n Session = sessionmaker(bind=engine)\n Base.metadata.create_all(engine)\n \n session = Session()\n return session\n\ndef get_all_legosets(session):\n legosets = session.query(LegoSet).all()\n return legosets\n<\/code><\/pre>\nEdit handler.py<\/code><\/p>\nimport os\nimport json\nfrom .models import LegoSet, database_session, get_all_legosets\n\n# GET \/legosets : Returns the list of lego sets\n# POST \/legoset : Add a new lego set to the database\n\ndef handle(event, context):\n response = {'statusCode': 400}\n\n if event.method == 'GET':\n if event.path == '\/legosets':\n legosets = get_list_of_legosets()\n response = {'statusCode': 200, \n 'body': legosets,\n 'headers': {'Content-Type': 'application\/json'}\n }\n elif event.method == 'POST':\n if event.path == '\/legoset':\n response = add_new_legoset(event.body)\n \n return response\n\ndef load_secret(name):\n filepath = os.path.join('\/var\/openfaas\/secrets\/', name)\n with open(filepath) as f:\n secret = f.read()\n return secret\n\ndef create_database_session():\n host = load_secret('database-host')\n user = load_secret('database-user')\n password = load_secret('database-password')\n schema = os.environ.get('database-name')\n port = int(os.environ.get('database-port', '3306'))\n\n session = database_session(host, port, user, password, schema)\n return session\n\ndef get_list_of_legosets():\n session = create_database_session()\n legosets = get_all_legosets(session)\n result = []\n\n for legoset in legosets:\n result.append({\n 'legoID': legoset.legoID,\n 'description': legoset.description,\n 'productURL': legoset.productURL,\n 'imageURL': legoset.imageURL\n })\n\n session.close()\n return {"sets": result}\n\ndef add_new_legoset(body):\n response = None\n try:\n inputJSON = json.loads(body.decode('utf8').replace("'", '"'))\n response = {\n 'statusCode': 200,\n 'body': {'received': inputJSON},\n 'headers': {'Content-Type': 'application\/json'}\n }\n except ValueError:\n response = {\n 'statusCode': 400,\n 'body': {'reason': 'Invalid JSON'},\n 'headers': {'Content-Type': 'application\/json'}\n }\n return response\n<\/code><\/pre>\nTo verify that this does actually work as expected, I added an extra Lego set using MySQLWorkbench.<\/p>\n
Build and test.<\/p>\n
$ curl -s http:\/\/192.168.64.4:9999\/function\/legodb\/legosets | jq\n{\n "sets": [\n {\n "description": "Pirates of Barracuda Bay",\n "imageURL": "https:\/\/live.staticflickr.com\/65535\/49698377257_be3f773feb_b.jpg",\n "legoID": 21322,\n "productURL": "https:\/\/www.lego.com\/en-gb\/product\/pirates-of-barracuda-bay-21322"\n },\n {\n "description": "Medieval Blacksmith",\n "imageURL": "https:\/\/images.brickset.com\/sets\/AdditionalImages\/21325-1\/21325-1_95850.jpg",\n "legoID": 21325,\n "productURL": "https:\/\/www.lego.com\/en-gb\/product\/medieval-blacksmith-21325"\n },\n {\n "description": "Mars Research Shuttle",\n "imageURL": "https:\/\/www.lego.com\/cdn\/cs\/set\/assets\/blt32796f487437e9db\/60226_alt1.jpg",\n "legoID": 60226,\n "productURL": "https:\/\/www.lego.com\/en-gb\/product\/mars-research-shuttle-60226"\n }\n ]\n}\n<\/code><\/pre>\n <\/p>\n
Time to add new Lego sets using our model<\/h2>\n Edit model.py<\/code><\/p>\n# Add this at the bottom\ndef create_legoset(json):\n legoID = json.get('legoID', None)\n if legoID is None:\n return None\n description = json.get('description', None)\n if description is None:\n return None\n productURL = json.get('productURL', None)\n imageURL = json.get('imageURL', None)\n\n legoset = LegoSet(legoID=legoID, description=description, productURL=productURL, imageURL=imageURL)\n return legoset\n<\/code><\/pre>\nEdit handler.py<\/code><\/p>\n# Change the import\nfrom .models import LegoSet, database_session, get_all_legosets, create_legoset\n\n# Replace add_new_legoset\ndef add_new_legoset(body):\n response = None\n try:\n inputJSON = json.loads(body.decode('utf8').replace("'", '"'))\n legoset = create_legoset(inputJSON)\n if legoset is None:\n raise ValueError()\n else:\n session = create_database_session()\n session.add(legoset)\n session.commit()\n session.refresh(legoset)\n newID = legoset.id\n session.close()\n\n response = {\n 'statusCode': 200,\n 'body': {'pkID': newID},\n 'headers': {'Content-Type': 'application\/json'}\n }\n except ValueError:\n response = {\n 'statusCode': 400,\n 'body': {'reason': 'Invalid JSON'},\n 'headers': {'Content-Type': 'application\/json'}\n }\n return response\n<\/code><\/pre>\nBuild and test<\/p>\n
# Add a new lego set to the database\n$ curl -s -H "Content-Type: application\/json" -d '{"legoID":6929,"description":"Star Fleet Voyager","productURL":"https:\/\/brickset.com\/sets\/6929-1\/Star-Fleet-Voyager","imageURL":"https:\/\/images.brickset.com\/sets\/images\/6929-1.jpg"}' "http:\/\/192.168.64.4:9999\/function\/legodb\/legoset" | jq\n{"pkID":8}\n\n# Verify\n$ curl -s http:\/\/192.168.64.4:9999\/function\/legodb\/legosets | jq\n...\n {\n "description": "Star Fleet Voyager",\n "imageURL": "https:\/\/images.brickset.com\/sets\/images\/6929-1.jpg",\n "legoID": 6929,\n "productURL": "https:\/\/brickset.com\/sets\/6929-1\/Star-Fleet-Voyager"\n }\n...\n\n<\/code><\/pre>\nNext up<\/h2>\n We now have a MariaDB instance running on AWS RDS and an OpenFaaS function that serves a RESTful API that allows us to get the list of Lego sets as well as being able to add a new set to the database.<\/p>\n
Somethings that we could add (but I will leave that for the reader):<\/p>\n
\nProtect the API using a secret token<\/li>\n Catch and handle database errors<\/li>\n Sanitise the database inputs<\/li>\n Unit-testing<\/li>\n<\/ul>\nAs it stands right now, our function is not really shareable just yet. Meaning it works on my machine but won’t work on someone else’s machine. Tomorrow I will look at what is required to make this a "install, follow recipe and forget".<\/p>\n
I also want to play around with having a function that will run on a schedule to go and fetch the imageURL and store the image data as a blob inside of the database.<\/p>\n<\/div>\n","protected":false},"excerpt":{"rendered":"
[…]<\/p>\n
Read more →<\/a><\/p>\n","protected":false},"author":2,"featured_media":278,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_mi_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[37],"tags":[36,23],"yoast_head":"\n100 Days of Learning: Day 12 \u2013 Using SQLAlchemy as an ORM for our Lego database - Andr\u00e9 Jacobs<\/title>\n \n \n \n \n \n \n \n \n \n \n \n \n\t \n\t \n\t \n \n \n \n \n \n\t \n\t \n\t \n