4
\$\begingroup\$

I'm fairly new to time series databases in general and Influx in particular.

My objective is to build a simple and general-use API that will allow me to write and read data from an Influx Database.

I used Influx on a previous project and I found that building the API for the database was pretty mechanical, so I decided to create a general API.

I divided my code into 3 modules:

  • Main.py where I will run FastAPI
  • Database.py where I connect with the database and I set the writing and reading actions.
  • Model.py where I create the pydantic model for the API.

Main.py

from fastapi import FastAPI
import uvicorn
import yaml
from yaml.loader import SafeLoader
from Model.Model import WritingData, ReadingData
from Database.Database import InfluxDataBase



with open("config.yaml", "r") as ymlfile:
    cfg = yaml.load(ymlfile,Loader=SafeLoader)
server_URL=cfg["InfluxDB"]["server_URL"]
token=cfg["InfluxDB"]["token"]
org=cfg["InfluxDB"]["org"]

Influx = InfluxDataBase(server_URL,token,org)
app = FastAPI()

@app.post('/write/')
async def call_writing_influx(data: WritingData):
    Influx.write_data(data)

@app.post('/read/')
async def call_reading_influx(data: ReadingData):
    return Influx.read_data(data)
    
if __name__ == "__main__":
    uvicorn.run("main:app", host="127.0.0.1", port=5000, reload=True)

Database

from influxdb_client import InfluxDBClient, Point
from influxdb_client.client.write_api import SYNCHRONOUS
import json

class InfluxDataBase:
    
    def __init__(self,server_URL,token,org) -> None:
        self.client=InfluxDBClient(server_URL, token=token, org=org)
        self.write_api=self.client.write_api(write_options=SYNCHRONOUS)
        self.query_api=self.client.query_api()
        self.server_URL=server_URL
        self.token=token
        self.org=org

    def write_data(self,data) -> None:
        executable_code='Point(data.measurement)'
        n_fields=len(data.field)
        n_tag=len(data.tag)
        for i in range(n_tag):
            executable_code=executable_code+'.tag(list(data.tag.keys())[{}],list(data.tag.values())[{}])'.format(i,i)
        for i in range(n_fields):
            executable_code=executable_code+'.field(list(data.field.keys())[{}],list(data.field.values())[{}])'.format(i,i)
        if data.timestamp is not None:
            executable_code=executable_code+'.time(data.timestamp)'

        Data=eval(executable_code)
        self.write_api.write(bucket=data.bucket_name, record=Data)
    
    
    def read_data(self,data):
        query = f'''
        from(bucket: "{data.bucket_name}")'''+ ''' 
        |> range(start: -{}h, stop: now())'''.format(data.time_interval)+f'''
        |> filter(fn:(r) => r["_measurement"] == "{data.measureament_name}")'''
        for i in range(len(data.tag)):
            if i==0:
                query=query+f'''|> filter(fn:(r) => r["{list(data.tag.keys())[i]}"] == "{list(data.tag.values())[i]}" '''
            elif i<len(data.tag):
                query=query+f''' or r["{list(data.tag.keys())[i]}"] == "{list(data.tag.values())[i]}"'''
        query=query+')'
        if data.field[0]=='All':
            pass
        else:
            for i in range(len(data.field)):
                if i==0:
                    query=query+f'''|> filter(fn:(r) => r["_field"] == "{data.field[i]}" '''
                elif i<len(data.field):
                    query=query+f''' or r["_field"] == "{data.field[i]}"'''
            query=query+')'
        result = self.query_api.query(org=self.org, query=query) 
        results={}
        for table in result:
            for record in table.records:
                results[record.get_field()]=record.get_value()
        return results

Model.py

from pydantic import BaseModel
from typing import List,Dict,Optional
from datetime import datetime

class WritingData(BaseModel):
    bucket_name: str
    measurement: str
    tag: Dict[str,str]
    field: Dict[str,float]
    timestamp: Optional[datetime] = None

class ReadingData(BaseModel):
    bucket_name: str
    time_interval: int
    measureament_name: str
    tag: Dict[str,str]
    field: List[str]

My current code works fine, It does what is intended to do.

There is any good practice politics I'm breaking?

Do you have any suggestions to improve it?

Edit: I updated the read_data() function in Database.py because I realized I didn't implemented the Flux code properly.

\$\endgroup\$
2
  • \$\begingroup\$ By politics do you perhaps mean policies? \$\endgroup\$
    – Reinderien
    Jul 30, 2021 at 2:44
  • \$\begingroup\$ I am having an issue with the Module - Model.Model in the 5th line in Main.Py. It's showing error - Module named Model not Found. Can you please guide me about how can I go about installing it on my IDE. I'm new to APIs as well, thank you in advance! \$\endgroup\$ Feb 8, 2023 at 7:32

1 Answer 1

3
\$\begingroup\$
  • Influx should not be capitalized as it's a variable name
  • None of this code:
with open("config.yaml", "r") as ymlfile:
    cfg = yaml.load(ymlfile,Loader=SafeLoader)
server_URL=cfg["InfluxDB"]["server_URL"]
token=cfg["InfluxDB"]["token"]
org=cfg["InfluxDB"]["org"]

Influx = InfluxDataBase(server_URL,token,org)

should be done at the global level; but I don't know FastAPI and its Starlette backend well enough to suggest a sane global context alternative. This will take some research on your part.

  • Lines like def __init__(self,server_URL,token,org) -> None: have a return typehint but no hints on the parameters; you should add these.
  • Run a linter. Among other things it will tell you to add spaces around the = on statements like self.org=org

Moving on:

The root of all eval

Take a deep breath. Step away from the keyboard for a moment. This code:

    executable_code='Point(data.measurement)'
    n_fields=len(data.field)
    n_tag=len(data.tag)
    for i in range(n_tag):
        executable_code=executable_code+'.tag(list(data.tag.keys())[{}],list(data.tag.values())[{}])'.format(i,i)
    for i in range(n_fields):
        executable_code=executable_code+'.field(list(data.field.keys())[{}],list(data.field.values())[{}])'.format(i,i)
    if data.timestamp is not None:
        executable_code=executable_code+'.time(data.timestamp)'

    Data=eval(executable_code)

is a true nightmare. It's wholly possible to construct your fluent-interface expression dynamically, using loops, something like (untested)

expr = Point(data.measurement)
n_fields = len(data.field)
n_tags = len(data.tag)

for i in range(n_tags):
    expr = expr.tag(
        tuple(data.tag.keys())[i],
        tuple(data.tag.values())[i],
    )

and so on. But the way you're casting a dictionary to an indexed sequence there and elsewhere in your code is patently insane non-advisable; just call items:

expr = Point(data.measurement)

for key, value in data.tag.items():
    expr = expr.tag(key, value)

for key, value in data.field.items():
    expr = expr.field(key, value)

if data.timestamp is not None:
    expr = expr.time(data.timestamp)

self.write_api.write(bucket=data.bucket_name, record=expr)
\$\endgroup\$
2
  • \$\begingroup\$ Thank you so much!!!!!! Just what I was looking for, I knew my code was ugly and there must be a cleaner way of doing it. Thank you for the insights!!!! By the way, you mentioned that a piece of my code shouldn't be done at a global level, can you explain me why? \$\endgroup\$ Jul 30, 2021 at 14:20
  • 1
    \$\begingroup\$ Global code like this makes it more difficult to unit test, for one thing. Also, what if you wanted two separate in-process FastAPI instances, each with a different database? \$\endgroup\$
    – Reinderien
    Jul 31, 2021 at 12:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.