From b134f4c3b928bca8a655bbf68f36b4f4da561f88 Mon Sep 17 00:00:00 2001 From: Me Date: Thu, 17 Dec 2020 01:01:27 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + auto_test.sh | 11 ++++ energyDB.conf | 10 +++ srv/__init__.py | 3 + srv/energyDB.py | 117 ++++++++++++++++++++++++++++++++++ start_server.sh | 14 +++++ test/__init__.py | 0 test/test_EnergyDB.py | 142 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 299 insertions(+) create mode 100644 .gitignore create mode 100755 auto_test.sh create mode 100644 energyDB.conf create mode 100644 srv/__init__.py create mode 100644 srv/energyDB.py create mode 100755 start_server.sh create mode 100644 test/__init__.py create mode 100644 test/test_EnergyDB.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..43319bd --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyd +__pycache__ diff --git a/auto_test.sh b/auto_test.sh new file mode 100755 index 0000000..822a825 --- /dev/null +++ b/auto_test.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +# Start test if one of the files was modified + +while true; do + NOTIFY=$(inotifywait -rq -e modify . | grep '\.py') + if [ _$? == _0 ]; then + pytest --capture=no test + echo ">>>>>>>> Test finished at: $(date)" + fi +done diff --git a/energyDB.conf b/energyDB.conf new file mode 100644 index 0000000..39ffa77 --- /dev/null +++ b/energyDB.conf @@ -0,0 +1,10 @@ +# Configuration for the uvicorn server running the energyDB application + +# Set HTTP port +HTTP_PORT=8444 + +# Bind to address +IP_ADDRESS=0.0.0.0 + +# More uvicorn command line arguments +UVICORN_ARGS=--reload \ No newline at end of file diff --git a/srv/__init__.py b/srv/__init__.py new file mode 100644 index 0000000..93d611e --- /dev/null +++ b/srv/__init__.py @@ -0,0 +1,3 @@ +from .energyDB import app + +energyDB = app \ No newline at end of file diff --git a/srv/energyDB.py b/srv/energyDB.py new file mode 100644 index 0000000..3e036fb --- /dev/null +++ b/srv/energyDB.py @@ -0,0 +1,117 @@ +from typing import Optional, List +from fastapi import FastAPI, Header, HTTPException +from pydantic import BaseModel +import datetime +import databases +import sqlalchemy +from sqlite3 import OperationalError + +DATABASE_URL = "sqlite:///./energyDB.sqlite" +# DATABASE_URL = "sqlite://" +db = databases.Database(DATABASE_URL) +metadata = sqlalchemy.MetaData() +energy = sqlalchemy.Table( + "energy", + metadata, + sqlalchemy.Column("timestamp", sqlalchemy.DateTime, primary_key=True), + sqlalchemy.Column("channel_id", sqlalchemy.Integer(), nullable=False), + sqlalchemy.Column("value", sqlalchemy.Float), + sqlalchemy.UniqueConstraint("timestamp"), +) +engine = sqlalchemy.create_engine( + DATABASE_URL, connect_args={"check_same_thread": False} +) +metadata.create_all(engine) + +class EnergyValue(BaseModel): + timestamp: datetime.datetime + value: float + +class ChannelData(BaseModel): + channel_id: int + data: List[EnergyValue] + msg: Optional[str] + +class BulkData(BaseModel): + bulk: List[ChannelData] + msg: Optional[str] + +class BulkDataRequest(BaseModel): + channel_ids: List[int] + fromTime: datetime.datetime + tillTime: datetime.datetime = datetime.datetime.now() + +app = FastAPI(debug=True) + +@app.on_event("startup") +async def startup(): + """On startup connect to the database""" + await db.connect() + +@app.on_event("shutdown") +async def shutdown(): + """On shutdow diconnect from the database""" + await db.disconnect() + +# def _raiseHttpExceptionOnWrongToken(token : str): +# if token != fake_secret_token: +# raise HTTPException(status_code=400, detail="Invalid X-Token header") + +@app.put("/energy/bulkData") +async def putBulkEnergyData(bulkData: BulkData): + valuesToInsert = [] + for channelData in bulkData.bulk: + for measurement in channelData.data: + valuesToInsert.append({ + "channel_id": channelData.channel_id, + "timestamp": measurement.timestamp, + "value": measurement.value}) + query = energy.insert().values(valuesToInsert) + result = await db.execute(query) + return { + "valuesToInsert": valuesToInsert + } + +@app.get("/energy/bulkData", response_model = BulkData) +async def getBulkEnergyData(bulkDataRequest: BulkDataRequest): + bulkData = [] + for ch in bulkDataRequest.channel_ids: + query = sqlalchemy.select([energy.c.timestamp, energy.c.value]) \ + .where(energy.c.channel_id == ch) \ + .where(energy.c.timestamp >= bulkDataRequest.fromTime) \ + .where(energy.c.timestamp <= bulkDataRequest.tillTime) + data = await db.fetch_all(query) + bulkData.append({"channel_id": ch, "data": data}) + + return { + "bulk": bulkData, + "msg": __name__ + " - " +str(query.compile()) + } + + +@app.get("/energy/{channel_id}", response_model = ChannelData) +async def getChannelData(channel_id: int): + try: + query = sqlalchemy.select([energy.c.timestamp, energy.c.value]).where(energy.c.channel_id == channel_id) + result = await db.fetch_all(query) + return { + "channel_id": channel_id, + "data": result + } + except Exception as ex: + raise HTTPException(status_code=500, detail=f"Internal error: {type(ex)} - \"{ex}\"") + +@app.put("/energy/{channel_id}") #, response_model = EnergyValue) +async def putChannelData(channel_id: int, data: EnergyValue): + query = energy.insert().values( + timestamp=data.timestamp, + channel_id=channel_id, + value=data.value) + result = await db.execute(query) + # # return await db.fetch_all(query) + return { + "timestamp": datetime.datetime.now(), + # "channel" : data.channel, + "value": data.value, + "msg": str(result) + } diff --git a/start_server.sh b/start_server.sh new file mode 100755 index 0000000..014b164 --- /dev/null +++ b/start_server.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +# Start uvicorn server with application srv:energyDB + +ENERGY_DB_CONF=./energyDB.conf + +if [ -f $ENERGY_DB_CONF ]; then + . energyDB.conf +fi + +ARG_HTTP_PORT=${HTTP_PORT:-8000} +ARG_IP_ADDRESS=${IP_ADDRESS:-127.0.0.1} + +/usr/bin/env uvicorn --port $ARG_HTTP_PORT --host $ARG_IP_ADDRESS ${UVICORN_ARGS} srv:energyDB diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_EnergyDB.py b/test/test_EnergyDB.py new file mode 100644 index 0000000..832477e --- /dev/null +++ b/test/test_EnergyDB.py @@ -0,0 +1,142 @@ +from fastapi.testclient import TestClient +import pytest + +from datetime import datetime +import json +import urllib.parse + +from srv import energyDB + +class Test_energyDb: + + bulkTestData = [ + { + "channel_id": 1, + "data": ( + { "timestamp": "2020-12-11T12:00:22", "value": 1100.1 }, + { "timestamp": "2020-12-11T12:10:15", "value": 1109.2 }, + { "timestamp": "2020-12-11T12:20:13", "value": 1119.3 }, + { "timestamp": "2020-12-11T12:30:21", "value": 1131.4 }, + { "timestamp": "2020-12-11T12:40:08", "value": 1143.5 }, + { "timestamp": "2020-12-11T12:50:13", "value": 1152.6 }, + { "timestamp": "2020-12-11T13:00:11", "value": 1160.7 }, + { "timestamp": "2020-12-11T13:10:09", "value": 1169.8 }, + { "timestamp": "2020-12-11T13:20:10", "value": 1181.9 }, + { "timestamp": "2020-12-11T13:30:17", "value": 1190.0 }, + ) + }, + { + "channel_id": 2, + "data": [ + { "timestamp": "2020-12-11T12:01:15", "value": 1200.1 }, + { "timestamp": "2020-12-11T12:21:28", "value": 1219.2 }, + { "timestamp": "2020-12-11T12:41:21", "value": 1243.3 }, + { "timestamp": "2020-12-11T13:01:16", "value": 1260.4 }, + { "timestamp": "2020-12-11T13:21:18", "value": 1281.5 }, + ] + } + ] + + client = TestClient(energyDB) + +# def setup(self): +# self.client = TestClient(energyDB) + +# def teardown(self): +# self.client = None + + def _test_bulkData_put(self): + # response = self.client.put("/energy/bulkData", json=self.bulkTestData); + response = self.client.put("/energy/bulkData", json={"bulk": self.bulkTestData}) + # print(f"dir(response): {dir(response)}") + # print(f"dir(response.request): {dir(response.request)}") + # print("---- request") + # print(f"response.request.method: {response.request.method}") + # print(f"response.request.url: {urllib.parse.unquote(response.request.url)}") + # print(f"response.request.headers: {response.request.headers}") + # requestJson = json.loads(response.request.body) + # print(f"response.request.body: {json.dumps(requestJson, indent=2)}") + print("---- response") + print(f"response.reason: {response.reason}") + responseJson = json.loads(response.text) + print(f"response.text: {json.dumps(responseJson, indent=2)}") + # print(f"response.text: {json.dumps(response.text, indent=2)}") + assert response.status_code == 200 + + def test_bulkData_get(self): + # response = self.client.put("/energy/bulkData", json=self.bulkTestData); + fromTimestamp = datetime.fromisoformat("2020-12-11T12:30:00") + tillTimestamp = datetime.fromisoformat("2020-12-11T12:30:59") + response = self.client.get( + "/energy/bulkData", + json = { + "channel_ids": [1, 2, 3], + "fromTime": fromTimestamp.isoformat(), + # "tillTime": tillTimestamp.isoformat() + }) + # print(f"dir(response): {dir(response)}") + # print(f"dir(response.request): {dir(response.request)}") + print("---- request") + print(f"response.request.method: {response.request.method}") + print(f"response.request.url: {urllib.parse.unquote(response.request.url)}") + print(f"response.request.headers: {response.request.headers}") + print(f"dir(response.request): {dir(response.request)}") + print(f"response.request.body: {response.request.body}") + # requestJson = json.loads(response.request.body) + # print(f"response.request.body: {json.dumps(requestJson, indent=2)}") + print("---- response") + print(f"response.reason: {response.reason}") + responseJson = json.loads(response.text) + print(f"response.text: {json.dumps(responseJson, indent=2)}") + # print(f"response.text: {json.dumps(response.text, indent=2)}") + assert response.status_code == 200 + + @pytest.mark.skip(reason="Ignore me temporarily") + def test_insert_energy(self): + energyData = { + "timestamp": datetime.now().isoformat(), + "value": 234.5, + } + print(f"energyData: {energyData}") + # response = self.client.put("/energies/1", json=energyData) #, headers={"X-Token": "coneofsilence"}) + response = self.client.put( + "/energy/2", + # params=energyData, + json=energyData) #, headers={"X-Token": "coneofsilence"}) + # print(f"dir(response): {dir(response)}") + # print(f"dir(response.request): {dir(response.request)}") + # print("---- request") + # print(f"response.request.method: {response.request.method}") + # print(f"response.request.url: {urllib.parse.unquote(response.request.url)}") + # print(f"response.request.headers: {response.request.headers}") + # print(f"response.request.body: {json.loads(response.request.body)}") + # print("---- response") + # print(f"response.reason: {response.reason}") + # print(f"response.text: {json.loads(response.text)}") + # print(f"request.header: \"{response.request.header}\"") + # print(f"request.body: \"{response.request.body}\"") + assert response.status_code == 200 + # assert response.json()["msg"] == "" + + @pytest.mark.skip(reason="Ignore me temporarily") + def test_get_energy(self): + response = self.client.get("/energy/1") + # print(f"dir(response): {dir(response)}") + # print(f"dir(response.request): {dir(response.request)}") + # print("---- request") + # print(f"response.request.method: {response.request.method}") + # print(f"response.request.url: {urllib.parse.unquote(response.request.url)}") + # print(f"response.request.headers: {response.request.headers}") + # print("---- response") + # print(f"response.reason: {response.reason}") + responseJson = json.loads(response.text) + print(f"response.text: {json.dumps(responseJson, indent=2)}") + data = response.json() + # print(f"data: {type(data)}") + # for k,v in data.items(): + # print(f"key: {k} -> value: {v}") + print(f"data of channel: {data['channel_id']}") + for r in data["data"]: + print(f"r: {r}") + assert response.status_code == 200 + # assert response.json()["msg"] == ""