from typing import Optional, List, Dict from fastapi import FastAPI, Header, HTTPException from pydantic import BaseModel import datetime import os # import sqlalchemy from .db import EnergyDB import logging API_VERSION_MAJOR = 1 API_VERSION_MINOR = 0 DB_URL = os.getenv("DATABASE_URL") #, default="sqlite://") if len(DB_URL) == 0: raise Exception("Environment variable DATABASE_URL missed!") print(f"DB URL: {DB_URL}") db = EnergyDB(DB_URL) app = FastAPI(debug=True) class InfoData(BaseModel): info: Dict class EnergyValue(BaseModel): timestamp: datetime.datetime value: float class ChannelData(BaseModel): channel_id: Optional[int] channel: Optional[str] 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() class ChannelInfo(BaseModel): name: str class Channels(BaseModel): channels: List[ChannelInfo] def _restApiPath(subPath : str) -> str: """Return the full REST API path Arguments: subPath {str} -- The sub-URL Returns: str -- The full REST API URL """ REST_API_ROOT = f"/energy/v{API_VERSION_MAJOR}" if (subPath.startswith("/")): return REST_API_ROOT + subPath else: return REST_API_ROOT + "/" + subPath @app.get(_restApiPath("/version")) async def restApiGetVersion() -> dict: """Return the version information Returns: dict -- The version information of energyDB and SQLAlchemy """ return { "version": f"{API_VERSION_MAJOR}.{API_VERSION_MINOR}", } @app.get(_restApiPath("/info"), response_model = InfoData) async def apiGetInfo(): info = {} info["db_url"] = db.url() e = sqlalchemy.Table("energy", db.metadata(), autoload=True, autoload_with=db.engine()) info["db_fields"] = [c.name for c in e.columns] result = db.execute("select * from energy") info["db_dir"] = str(dir(db)) info["result_type"] = str(type(result)) info["result_dir"] = str(dir(result)) info["result_count"] = str(result.rowcount) info["db_contents"] = [str(row) for row in result.fetchall()] result = db.execute("SELECT name FROM sqlite_master WHERE type='table'") info["tables"] = [str(row) for row in result.fetchall()] result = db.execute("SELECT sql FROM sqlite_master WHERE type='table'") info["energy.sql"] = str(result.fetchone()[0]).replace("\n", "").replace("\t","") result = db.execute("SELECT COUNT(*) FROM energy") info["rows"] = str(result.fetchone()) # info["rows"] = [str(row) for row in result.fetchall()] info["tables"] = [t.name for t in db.metadata().sorted_tables] for t in db.metadata().sorted_tables: # info[f"columns_{t}"] = str(dir(t)) info[f"columns_{t}"] = t.schema return { "info": info } @app.get(_restApiPath("/bulkData"), response_model = BulkData) async def getBulkEnergyData(bulkDataRequest: BulkDataRequest): bulkData = [] trace = [] exception = None try: for ch in bulkDataRequest.channel_ids: data = [] table_energy = db.table("energy") query = sqlalchemy.select([table_energy.c.timestamp, table_energy.c.value]) \ .select_from(table_energy) \ .where(sqlalchemy.sql.and_( table_energy.c.channel_id == ch, table_energy.c.timestamp >= bulkDataRequest.fromTime, table_energy.c.timestamp <= bulkDataRequest.tillTime ) ) for row in db.execute(query).fetchall(): data.append(dict(row.items())) bulkData.append({"channel_id": ch, "data": data}) except Exception as e: raise HTTPException( status_code=404, detail=f"Database error: {type(e)} - {str(e)}" # detail=f"Database error: {str(e)}\nQuery: {str(query)}" ) return { "bulk": bulkData, "msg": None #__name__ + " - " + str(query) } @app.put(_restApiPath("/bulkData")) async def putBulkEnergyData(bulkData: BulkData): valuesToInsert = [] result = "ok" # rows_before = {} # rows_after = {} try: # rowCounter = 0 # dbResult = db.execute( db.tables["energy"].select() ) # for row in dbResult.fetchall(): # rows_before[f"row_{rowCounter}"] = str(row) # rowCounter += 1 for channelData in bulkData.bulk: if channelData.channel_id is None: try: table_channels = db.table("channels") channel_id = db.execute( sqlalchemy.select([table_channels.c.id]) \ .select_from(table_channels) \ .where(table_channels.c.name == channelData.channel)) except: raise HTTPException( status_code = 500, detail = f"Database error: {type(ex)} - \"{ex}\"" ) for measurement in channelData.data: valuesToInsert.append({ "channel_id": channelData.channel_id, "timestamp": measurement.timestamp, "value": measurement.value }) db.execute(db.table("energy").insert(), valuesToInsert) # rowCounter = 0 # dbResult = db.execute( db.tables["energy"].select() ) # for row in dbResult.fetchall(): # rows_after[f"row_{rowCounter}"] = str(row) # rowCounter += 1 except Exception as e: result = f"Exception \"{str(e)}\"" return { "result": result, # "rows_before": rows_before, # "rows_after": rows_after, } @app.put(_restApiPath("/channels")) async def putChannels(channel_info: Channels): result = "ok" query = "???" rows_before = {} rows_after = {} try: r = db.getChannels() rows_before = r["channels"] channelNames = [c.name for c in channel_info.channels] res = db.addChannels(channelNames) result = res["result"] query = res["query"] r = db.getChannels() rows_after = r["channels"] except Exception as e: result = f"Exception \"{str(e)}\"" return { "result": result, "channels": str(channel_info.channels), "channelNames": channelNames, "query": query, "rows_before": rows_before, "rows_after": rows_after, } @app.get(_restApiPath("/channels")) async def getChannels() -> dict: """Return a list of all channels Raises: HTTPException: with status 500 on a database error Returns: dict -- the list of all channels """ try: result = db.getChannels() return {"channels": [ch["name"] for ch in result["channels"]]} except Exception as e: raise HTTPException(status_code = 500, detail = str(e)) @app.get(_restApiPath("/channels/{channel}/id")) async def getChannelId(channel: str): # -> dict: try: chId = db.getChannelId(channel) return { "channel": channel, "id": chId, # "query": str(r["query"]) } except Exception as e: raise HTTPException(status_code=500,detail=f"Database error: {type(e)} - {str(e)}") # @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) # } @app.get(_restApiPath("/{channel_id}/count")) # response_model = ChannelData) async def getChannelRowCount(channel_id: int): info = {} info["columns"] = str( db.table("energy").columns) # countQuery = sqlalchemy.select([sqlalchemy.func.count()]).select_from(db.tables["energy"]) # info["stmt"] = str(countQuery) # countResult = db.execute( countQuery ) # info["count"] = countResult.fetchone()[0] # info["tables"] = [t.name for t in metadata.sorted_tables] # for t in metadata.tables: # info[f"columns_{t}"] = str(type(t)) # # info[f"columns_{t}"] = list(sqlalchemy.inspect(t).columns) return { "info": info }