from typing import Any, Optional
from typing_extensions import override
from influxdb_client_3 import InfluxDBClient3, Point
from masterpiece.timeseries import TimeSeries, Measurement
from masterpiece_influx.influx_measurement import InfluxMeasurement
[docs]
class Influx(TimeSeries):
"""InfluxDB V3 implementation of a time series database for MasterPiece.
This class provides an interface for interacting with an InfluxDB V3 time series
database. The `host`, `token`, `org`, and `database` attributes are inherited
from the `TimeSeries` superclass and can be initialized through the configuration
system or overridden via constructor parameters.
Attributes:
host (str): Hostname of the InfluxDB server.
token (str): Authentication token for InfluxDB.
org (str): Organization name for InfluxDB.
database (str): Database name for InfluxDB.
influx_client (InfluxDBClient3): The client instance used for database operations.
Example:
Creating a custom instance with specific parameters:
.. code-block:: python
db = Influx(
host="example.com",
token="my-token",
org="my-org",
database="my-db"
)
"""
def __init__(
self,
name: str = "influx",
host: Optional[str] = None,
token: Optional[str] = None,
org: Optional[str] = None,
database: Optional[str] = None,
) -> None:
"""Construct InfluxDB v3 client for writing and reading time series.
If the constructor parameters are not given, then the default class attribute
values defined by the `TimeSeries` super class are used.
Args:
name (str, optional): Name of the object to be created. Defaults to "influx".
host (Optional[str], optional): Hostname of the InfluxDB server. Defaults to class attribute.
token (Optional[str], optional): Authentication token for InfluxDB. Defaults to class attribute.
org (Optional[str], optional): Organization name for InfluxDB. Defaults to class attribute.
database (Optional[str], optional): Database name for InfluxDB. Defaults to class attribute.
"""
super().__init__(name)
# Use constructor parameters if provided, otherwise fall back to class attributes
self.host = host if host is not None else self.host
self.token = token if token is not None else self.token
self.org = org if org is not None else self.org
self.database = database if database is not None else self.database
# Initialize the InfluxDB client
self.influx_client = InfluxDBClient3(
host=self.host,
token=self.token,
org=self.org,
database=self.database,
)
[docs]
@override
def write(self, point: Point) -> None:
self.influx_client.write(record=point)
[docs]
@override
def write_dict(
self, name: str, tags: dict[str, Any], fields: dict[str, Any], ts: str
) -> None:
point: dict[str, Any] = {
"measurement": name,
"tags": tags,
"fields": fields,
"time": ts,
}
self.influx_client.write(record=point)
[docs]
@override
def read_dict(
self,
measurement: str,
start_time: str,
end_time: Optional[str] = None,
tags: Optional[dict[str, Any]] = None,
fields: Optional[list[str]] = None,
) -> list[dict[str, Any]]:
try:
# Select specific fields or all fields
fields_query = ", ".join(fields) if fields else "*"
# Construct the base SQL query
query = (
f"SELECT {fields_query} FROM {measurement} WHERE time >= '{start_time}'"
)
if end_time:
query += f" AND time <= '{end_time}'"
# Add tag filters
if tags:
tag_conditions = " AND ".join(
f"{key} = '{value}'" for key, value in tags.items()
)
query += f" AND {tag_conditions}"
# Order by time
query += " ORDER BY time"
# Execute the query
result = self.influx_client.query(query)
# Convert the result to a list of dictionaries
records = []
for row in result:
records.append(dict(row))
return records
except Exception as e:
raise Exception(f"Failed to read data: {e}")
[docs]
@override
def read_last_value(
self,
measurement: str,
tags: Optional[dict[str, Any]] = None,
fields: Optional[list[str]] = None,
) -> dict[str, Any]:
try:
# Select specific fields or all fields
fields_query = ", ".join(fields) if fields else "*"
# Construct the base SQL query
query = f"SELECT {fields_query} FROM {measurement}"
# Add tag filters
if tags:
tag_conditions = " AND ".join(
f"{key} = '{value}'" for key, value in tags.items()
)
query += f" WHERE {tag_conditions}"
# Order by time in descending order and limit to 1
query += " ORDER BY time DESC LIMIT 1"
# Execute the query
result = self.influx_client.query(query)
# Parse and return the single record, or an empty dict if no result
return dict(result[0]) if result else {}
except Exception as e:
raise Exception(f"Failed to read last value: {e}")
[docs]
@override
def read_point(
self,
measurement: str,
start_time: str,
end_time: Optional[str] = None,
tags: Optional[dict[str, Any]] = None,
fields: Optional[list[str]] = None,
) -> list[Any]:
try:
# Select specific fields or all fields
fields_query = ", ".join(fields) if fields else "*"
# Construct the base SQL query
query = (
f"SELECT {fields_query} FROM {measurement} WHERE time >= '{start_time}'"
)
if end_time:
query += f" AND time <= '{end_time}'"
# Add tag filters
if tags:
tag_conditions = " AND ".join(
f"{key} = '{value}'" for key, value in tags.items()
)
query += f" AND {tag_conditions}"
# Order by time
query += " ORDER BY time"
# Execute the query
result = self.influx_client.query(query)
# Convert the result to a list of Point objects
points = []
for row in result:
point = Point(measurement)
for key, value in row.items():
if key == "time":
point.time(value)
elif isinstance(value, (int, float)):
point.field(key, value)
else:
point.tag(key, value)
points.append(point)
return points
except Exception as e:
raise Exception(f"Failed to read data as Points: {e}")
[docs]
@override
def measurement(self, measurement: str) -> Measurement:
return InfluxMeasurement(measurement)