diff --git a/app/api.py b/app/api.py new file mode 100644 index 0000000..ab261ab --- /dev/null +++ b/app/api.py @@ -0,0 +1,41 @@ +from services import post_services, get_services + +''' +API Functions +====================================================================== +These functions are what are exposed/referenced in the swagger.yml +file -- they are essentially wrapper functions. The main work is done +in the files in the /services/ folder. +These functions are acting as very light controllers essentially. +''' + +# The each value represents a light meter. +READINGS = [0, 0, 0] + +def post_a_reading(light_meter, the_reading): + if light_meter == 1: + READINGS[0] = the_reading.get("reading") + elif light_meter == 2: + READINGS[1] = the_reading.get("reading") + elif light_meter == 3: + READINGS[2] = the_reading.get("reading") + return post_services.add_latest_reading(light_meter,the_reading) + +def get_latest(light_meter): + # YOU ARE UP TO HERE. NEED TO REFACTOR BACK INTO DATABASE FUNCTION + # BELOW THIS FUNCTION. YAML FILE NEEDS CLEANING UP TOO. + if light_meter == 1: + return READINGS[0] + elif light_meter == 2: + return READINGS[1] + elif light_meter == 3: + return READINGS[2] + +def get_latest_reading(light_meter): + return get_services.get_in_mem_reading(light_meter) + +def get_all_readings(light_meter): + return get_services.get_all_readings_from_table(light_meter) + +def get_all_readings_for_every_meter(): + return get_services.get_all_readings_from_database() diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..2d3e6f6 --- /dev/null +++ b/app/app.py @@ -0,0 +1,16 @@ +from flask import Flask, render_template +import connexion +import config + +# The application instance. +app = config.connex_app + +# The yml file configures the app's endpoints. +app.add_api("swagger.yml") + +@app.route("/") +def home (): + return render_template("home.html") + +if __name__ == "__main__": + app.run(host="0.0.0.0", debug=True) diff --git a/app/build_database.py b/app/build_database.py new file mode 100644 index 0000000..059a07d --- /dev/null +++ b/app/build_database.py @@ -0,0 +1,36 @@ +import os +from datetime import datetime +from config import db +from models.meters import Meter1, Meter2, Meter3 + +def get_timestamp(): + return datetime.now().strftime(("%Y-%m-%d %H:%M:%S")) + +# The initialisation data for the database +READINGS1 =[ {"time":datetime.now(), "reading": 0} ] +READINGS2 =[ {"time":datetime.now(), "reading": 0} ] +READINGS3 =[ {"time":datetime.now(), "reading": 0} ] + +# Deletes the database if it already exists +if os.path.exists("readings.db"): + os.remove("readings.db") + +# Creates the database +db.create_all() + +# Iterates over the READINGS1 structure and populates the database +for info in READINGS1: + r = Meter1(time=info.get("time"), reading=info.get("reading")) + db.session.add(r) + +# Iterates over the READINGS2 structure and populates the datebase +for info in READINGS2: + r = Meter2(time=info.get("time"), reading=info.get("reading")) + db.session.add(r) + +# Iterates over the READINGS3 structure and populates the datebase +for info in READINGS3: + r = Meter3(time=info.get("time"), reading=info.get("reading")) + db.session.add(r) + +db.session.commit() diff --git a/app/config.py b/app/config.py new file mode 100644 index 0000000..b55e515 --- /dev/null +++ b/app/config.py @@ -0,0 +1,24 @@ +import os +import connexion +from flask_sqlalchemy import SQLAlchemy +from flask_marshmallow import Marshmallow + +basedir = os.path.abspath(os.path.dirname(__file__)) + +# Creates the Connexion application instance +connex_app = connexion.App(__name__, specification_dir=basedir) + +# Gets the underlying Flask app instance +app = connex_app.app +database_uri = "sqlite:////" + os.path.join(basedir, "readings.db") + +# Configures the SQLAlchemy part of the app instance +app.config["SQLALCHEMY_ECHO"] = True # Set to false in prod. +app.config["SQLALCHEMY_DATABASE_URI"] = database_uri +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +# Creates the SQLAlchemy db instance +db = SQLAlchemy(app) + +# Initialises Marshmallow +ma = Marshmallow(app) diff --git a/app/models/meters.py b/app/models/meters.py new file mode 100644 index 0000000..7d0382d --- /dev/null +++ b/app/models/meters.py @@ -0,0 +1,50 @@ +from datetime import datetime +from config import db, ma + +''' +Note on Duplication Levels +====================================================================== +While the code in this file seems very duplicated, it is just the +result of using SQL-Alchemy (ORM) and the repetitive nature of the +project as a whole. At the time of writing, the expected amount of +light meters is three and each one must take their own readings. +But, they must keep their readings separate from each other. This +means a table in the database for each meter. This is the main cause +for the repetitive/duplicated code. +Because this project has fixed requirements, the hard-coded nature is +a trade-off because of this. If the project increases the amount of +light meters it uses, this will probably need to be refactored. +''' + +class Meter1(db.Model): + __tablename__ = "meter1" + id = db.Column(db.Integer, primary_key=True) + time = db.Column(db.DateTime, default=datetime.utcnow) + reading = db.Column(db.Integer) + +class Meter1Schema(ma.ModelSchema): + class Meta: + model = Meter1 + sqla_session = db.session + +class Meter2(db.Model): + __tablename__ = "meter2" + id = db.Column(db.Integer, primary_key=True) + time = db.Column(db.DateTime) + reading = db.Column(db.Integer) + +class Meter2Schema(ma.ModelSchema): + class Meta: + model = Meter2 + sqla_session = db.session + +class Meter3(db.Model): + __tablename__ = "meter3" + id = db.Column(db.Integer, primary_key=True) + time = db.Column(db.DateTime) + reading = db.Column(db.Integer) + +class Meter3Schema(ma.ModelSchema): + class Meta: + model = Meter3 + sqla_session = db.session diff --git a/app/readings.db b/app/readings.db new file mode 100644 index 0000000..4cb822b Binary files /dev/null and b/app/readings.db differ diff --git a/app/services/get_services.py b/app/services/get_services.py new file mode 100644 index 0000000..86c678d --- /dev/null +++ b/app/services/get_services.py @@ -0,0 +1,84 @@ +from flask import make_response, abort +from config import db +from models.meters import (Meter1, Meter1Schema, Meter2, Meter2Schema, + Meter3, Meter3Schema) + +''' +Get Services Note +====================================================================== +The functions in this file are for retrieving data stored at the (this) +server. If you want to store any readings taken with the light meters +at Ritherdon, you will need to head to the /post_services.py/ file. +It should be in the same directory at this: /services/. +''' + +def get_in_mem_reading(meter): + # Need to refactor in use an in-mem database. + # Currently grabbing from database. + if meter == 1: + return get_m1_latest() + elif meter == 2: + return get_m2_latest() + elif meter == 3: + return get_m3_latest() + +def get_all_readings_from_table(name): + if name == 1: + return get_all_readings_for_meter1() + elif name == 2: + return get_all_readings_for_meter2() + elif name == 3: + return get_all_readings_for_meter3() + +def get_all_readings_from_database(): + return get_all_readings() + +''' +The Nitty-Gritty Functions +====================================================================== +The functions below are the main functions within this file. The files +above act as "header functions" for the methods in /api.py/. I find it +easier to see what the method names are when this file and /api.py/ +are open side-by-side. At the very least it reduces the amount I need +to scroll up and down the file to find what I am after. +''' + +def get_m1_latest(): + reading = Meter1.query.order_by(Meter1.id.desc()).first() + meter_schema = Meter1Schema() + return meter_schema.dump(reading) + +def get_m2_latest(): + reading = Meter2.query.order_by(Meter2.id.desc()).first() + meter_schema = Meter2Schema() + return meter_schema.dump(reading) + +def get_m3_latest(): + reading = Meter3.query.order_by(Meter3.id.desc()).first() + meter_schema = Meter3Schema() + return meter_schema.dump(reading) + +def get_all_readings_for_meter1(): + readings = Meter1.query.order_by(Meter1.id.desc()).all() + schema = Meter1Schema(many=True) + data = schema.dump(readings) + return data + +def get_all_readings_for_meter2(): + readings = Meter2.query.order_by(Meter2.id.desc()).all() + schema = Meter2Schema(many=True) + data = schema.dump(readings) + return data + +def get_all_readings_for_meter3(): + readings = Meter3.query.order_by(Meter3.id.desc()).all() + schema = Meter3Schema(many=True) + data = schema.dump(readings) + return data + +def get_all_readings(): + m1 = get_all_readings_for_meter1() + m2 = get_all_readings_for_meter2() + m3 = get_all_readings_for_meter3() + readings = {"meter1": m1, "meter2": m2, "meter3": m3} + return readings diff --git a/app/services/post_services.py b/app/services/post_services.py new file mode 100644 index 0000000..9315d9a --- /dev/null +++ b/app/services/post_services.py @@ -0,0 +1,42 @@ +from flask import make_response, abort +from config import db +from models.meters import (Meter1, Meter1Schema, Meter2, Meter2Schema, + Meter3, Meter3Schema) + +''' +Post Services Note +====================================================================== +The functions in this file are for storing the readings taken from the +light meters in Ritherdon. It you are wanting to retrieve data from +the (this) server, you will need to head to the /get_services.py/ file. +It should be in the same directory as this: /services/. +''' + +def add_latest_reading(meter,reading): + if meter == 1: + return add_reading_to_meter1(reading) + elif meter == 2: + return add_reading_to_meter2(reading) + elif meter == 3: + return add_reading_to_meter3(reading) + +def add_reading_to_meter1(the_reading): + schema = Meter1Schema() + new_reading = schema.load(the_reading, session=db.session) + db.session.add(new_reading) + db.session.commit() + return make_response("Reading successfully stored in database.", 201) + +def add_reading_to_meter2(the_reading): + schema = Meter2Schema() + new_reading = schema.load(the_reading, session=db.session) + db.session.add(new_reading) + db.session.commit() + return make_response("Reading successfully stored database.", 201) + +def add_reading_to_meter3(the_reading): + schema = Meter3Schema() + new_reading = schema.load(the_reading, session=db.session) + db.session.add(new_reading) + db.session.commit() + return make_response("Reading successfully stored database.", 201) diff --git a/app/swagger.yml b/app/swagger.yml new file mode 100644 index 0000000..59c19c9 --- /dev/null +++ b/app/swagger.yml @@ -0,0 +1,237 @@ +swagger: "2.0" +info: + description: >- + This A.P.I. is part of the, at time of writing, unnamed project by + Nicola Ellis. The project's code-name is Roaming Light A.P.I. + version: "0.0.1 - Alpha" + title: Return to Ritherdon Project A.P.I +consumes: + - application/json +produces: + - application/json + +basePath: /api + +paths: + /readings/add/{light_meter}: + post: + operationId: api.post_a_reading + tags: + - Store Readings + summary: >- + Posts a reading from one of the project's light meters and store + it in the database. + description: >- + Use this U.R.L. to post a new reading from of the light meters + used in the project. At the time of writing, there are three + light meters taking readings in the Ritherdon factory. + It is important to note which light meter is posting the + reading. Each light meter has its own table in the database + and it will corrupt the data integrity if you post a reading + from a meter into the wrong table. At the time of writing, + there are three meters taking light reading and posting them + to here. + parameters: + - name: light_meter + in: path + description: >- + The Id. of the light meter which has taken the reading. At + the time of writing, it should be a number between 1-3. + type: integer + required: True + - name: the_reading + in: body + description: >- + The data object which represents a single light reading + from light meter 1. It conveys the time the reading was + taken and the light value it recorded. Remember, each light + has their own table in the database so make sure you do not + post the light meter reading to the wrong URL. It will + corrupt the data integrity. + required: True + schema: + type: object + properties: + time: + type: string + format: date-time + example: 2019-10-19 17:04:57.880010 + description: >- + The date and time the reading was taken. Make sure + you use the UTC time format. This is because the + A.P.I. has made a design decision to standardise on + it. + reading: + type: integer + example: 23 + description: >- + This represents the amount of light the light meter + recorded. This is the most important piece of data + you will post in this data-object. + responses: + 201: + description: >- + The reading was successfully added to the database. + + /readings/latest/{light_meter}: + get: + operationId: api.get_latest_reading + tags: + - Request Readings + summary: >- + Returns the latest reading from the specified light meter. + description: >- + Use this U.R.L. to retrieve the latest reading from the + light meter you specified (in the U.R.L.). At the time of + writing, the project has only three light meters and are + labelled 1-3. + parameters: + - name: light_meter + in: path + description: >- + This is the Id. of the light meter which you are retrieving + the reading for. The Id. consists of a number between 1-3 + at the time of writing. + type: integer + required: True + responses: + 200: + description: >- + If the server can successfully retrieve the latest reading + for the specified light meter, you should receive a JASON + object like the one below. It should include the Id. of + the light meter it was taken with, the time the reading + was taken and the reading itself. + schema: + type: object + properties: + id: + type: integer + example: 2 + description: >- + This is the Id. of the light meter which took the + reading. It should be between 1-3 at the time of + writing. + time: + type: string + example: 2019-10-19 17:04:57 + description: >- + The time and date the reading was taken. The A.P.I. + has standardised on the U.T.C. format. + reading: + type: integer + example: 34 + description: >- + This is the actual reading taken from the specified + light meter, at the time specified in this response + body (I.E. JSON object). + + /readings/all/{light_meter}: + get: + operationId: api.get_all_readings + tags: + - Request Readings + summary: >- + Returns every reading taken by the specified light meter. + description: >- + Use this U.R.L. to retrieve the all the reading from the + light meter you specified (in the U.R.L.). At the time of + writing, the project has only three light meters and are + labelled 1-3. + parameters: + - name: light_meter + in: path + description: >- + This is the Id. of the light meter which you are retrieving + the readings for. The Id. consists of a number between 1-3 + at the time of writing. + type: integer + required: True + responses: + 200: + description: >- + If the server can successfully retrieve all the readings + for the specified light meter, you should receive an array + of JSON objects like the one below. It should include the + database Id. of the reading, the time the reading was + taken and the reading itself. + schema: + type: object + properties: + id: + type: integer + example: 2 + description: >- + This is the database Id. of the reading. + time: + type: string + example: 2019-10-19 17:04:57 + description: >- + This is the time and date the reading was taken. + The A.P.I. has standardised on the U.T.C. format. + reading: + type: integer + example: 34 + description: >- + This is the actual reading taken from the specified + light meter, at the time specified in this response + body (I.E. JSON object). + + /readings/all: + get: + operationId: api.get_all_readings_for_every_meter + tags: + - Request Readings + summary: >- + Returns every reading taken by every light meter in the + project. + description: >- + Use this U.R.L. to retrieve all the readings from every light + meter in the project. There is no example of the data returned + because I can't seem to replicate it as a YAML schema which + is understood by Swagger. Because of this, it is registered as + a free-form object. To see what the data looks like when it is + returned, I recommend you use the "Try it out!" button to see a + working example. + responses: + 200: + description: >- + All the readings were successfully retrieved from the + database and delivered. + schema: + type: object + additionalProperties: True + +# This is the in-mem. database URL. + /readings/late/{light_meter}: + get: + operationId: api.get_latest + tags: + - Request Readings + summary: >- + Returns latest from in-mem database. + description: >- + latest in-mem db reading + parameters: + - name: light_meter + in: path + description: >- + This is the Id. of the light meter which you are retrieving + the readings for. The Id. consists of a number between 1-3 + at the time of writing. + type: integer + required: True + responses: + 200: + description: >- + In-mem db latest value + schema: + type: object + properties: + reading: + type: integer + example: 34 + description: >- + This is the actual reading taken from the specified + light meter, at the time specified in this response + body (I.E. JSON object). diff --git a/app/templates/home.html b/app/templates/home.html new file mode 100644 index 0000000..2db6bf0 --- /dev/null +++ b/app/templates/home.html @@ -0,0 +1,12 @@ + + + + + Application Home Page + + +

+ Hello World! +

+ + diff --git a/proj-env/bin/chardetect b/proj-env/bin/chardetect new file mode 100755 index 0000000..3477a13 --- /dev/null +++ b/proj-env/bin/chardetect @@ -0,0 +1,10 @@ +#!/mnt/dev-shed/midpoint/proj-env/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from chardet.cli.chardetect import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/proj-env/bin/connexion b/proj-env/bin/connexion new file mode 100755 index 0000000..1525334 --- /dev/null +++ b/proj-env/bin/connexion @@ -0,0 +1,10 @@ +#!/mnt/dev-shed/midpoint/proj-env/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from connexion.cli import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/proj-env/bin/flask b/proj-env/bin/flask new file mode 100755 index 0000000..b397699 --- /dev/null +++ b/proj-env/bin/flask @@ -0,0 +1,10 @@ +#!/mnt/dev-shed/midpoint/proj-env/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from flask.cli import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/proj-env/bin/jsonschema b/proj-env/bin/jsonschema new file mode 100755 index 0000000..de8e820 --- /dev/null +++ b/proj-env/bin/jsonschema @@ -0,0 +1,10 @@ +#!/mnt/dev-shed/midpoint/proj-env/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from jsonschema.cli import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/proj-env/bin/openapi-spec-validator b/proj-env/bin/openapi-spec-validator new file mode 100755 index 0000000..ed12244 --- /dev/null +++ b/proj-env/bin/openapi-spec-validator @@ -0,0 +1,10 @@ +#!/mnt/dev-shed/midpoint/proj-env/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from openapi_spec_validator.__main__ import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/proj-env/bin/virtualenv b/proj-env/bin/virtualenv new file mode 100755 index 0000000..a337242 --- /dev/null +++ b/proj-env/bin/virtualenv @@ -0,0 +1,10 @@ +#!/mnt/dev-shed/midpoint/proj-env/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys + +from virtualenv import main + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..4d1f49f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,28 @@ +certifi==2019.9.11 +chardet==3.0.4 +Click==7.0 +clickclick==1.2.2 +connexion==2.3.0 +Flask==1.1.1 +flask-marshmallow==0.10.1 +Flask-SQLAlchemy==2.4.0 +Flask-WTF==0.14.2 +idna==2.8 +inflection==0.3.1 +itsdangerous==1.1.0 +Jinja2==2.10.1 +jsonschema==2.6.0 +MarkupSafe==1.1.1 +marshmallow==3.2.1 +marshmallow-sqlalchemy==0.19.0 +openapi-spec-validator==0.2.8 +pkg-resources==0.0.0 +PyYAML==5.1.2 +requests==2.22.0 +six==1.12.0 +SQLAlchemy==1.3.8 +swagger-ui-bundle==0.0.5 +urllib3==1.25.5 +virtualenv==16.7.9 +Werkzeug==0.16.0 +WTForms==2.2.1