Add InfluxDB 3 Core support.
This commit is contained in:
+6
-1
@@ -15,4 +15,9 @@ AGE_PASS=pass
|
||||
AGE_HOST=localhost
|
||||
AGE_PORT=5432
|
||||
AGE_DB=university
|
||||
AGE_GRAPH_NAME=movie_graph
|
||||
AGE_GRAPH_NAME=movie_graph
|
||||
|
||||
INFLUX_ACTIVE=false
|
||||
INFLUXDB3_HOST=http://localhost:8181
|
||||
INFLUXDB3_AUTH_TOKEN=your_token
|
||||
INFLUXDB3_DATABASE=sensors
|
||||
@@ -1,14 +1,14 @@
|
||||
### Пример приложения для работы с БД
|
||||
|
||||
Небольшое десктоп-приложение на PyQt5 для работы с PostgreSQL, Neo4j и Apache AGE.
|
||||
Небольшое десктоп-приложение на PyQt5 для работы с PostgreSQL, Neo4j, Apache AGE и InfluxDB 3 Core.
|
||||
|
||||
### Стек
|
||||
|
||||
- PyQt5 — графический интерфейс
|
||||
- PostgreSQL (psycopg2) — реляционная БД для пользователей
|
||||
- Neo4j — графовая БД для фильмов
|
||||
- Apache AGE — графовая модель поверх PostgreSQL для фильмов
|
||||
- python-dotenv — загрузка параметров подключения из файла `.env`
|
||||
- Neo4j — графовая БД
|
||||
- Apache AGE — графовая БД в рамках PostgreSQL
|
||||
- InfluxDB 3 Core — БД временных рядов
|
||||
|
||||
### Скриншоты
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- Установленный PostgreSQL и доступ к серверу БД
|
||||
- Установленный Neo4j и загруженный обучающий граф фильмов (Movie Graph example)
|
||||
- Для Apache AGE: установленное расширение `age` в PostgreSQL и доступ к базе, где оно включено
|
||||
- Для InfluxDB 3 Core: запущенный сервер, заранее созданная база и токен с правами на запись и чтение
|
||||
|
||||
### Настройка окружения
|
||||
|
||||
@@ -53,6 +54,11 @@ AGE_HOST=localhost
|
||||
AGE_PORT=5432
|
||||
AGE_DB=university
|
||||
AGE_GRAPH_NAME=movie_graph
|
||||
|
||||
INFLUX_ACTIVE=false # включить InfluxDB 3 Core, установив true
|
||||
INFLUXDB3_HOST=http://localhost:8181
|
||||
INFLUXDB3_AUTH_TOKEN=your_token
|
||||
INFLUXDB3_DATABASE=sensors
|
||||
```
|
||||
|
||||
2. Создайте и активируйте виртуальное окружение:
|
||||
@@ -87,4 +93,4 @@ pip install -r requirements.txt
|
||||
py main.py
|
||||
```
|
||||
|
||||
Приложение подключится к PostgreSQL, создаст БД/таблицу и тестовые данные (если их еще нет), выполнит запрос к Neo4j (если активно) и/или к Apache AGE (если активно), а затем отобразит результаты в отдельных окнах.
|
||||
Приложение подключится к PostgreSQL, создаст БД/таблицу и тестовые данные (если их еще нет), выполнит запрос к Neo4j, Apache AGE и/или InfluxDB 3 Core (если они активны), а затем отобразит результаты в отдельных окнах.
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
|
||||
import psycopg2
|
||||
from dotenv import load_dotenv
|
||||
from influxdb_client_3 import InfluxDBClient3
|
||||
from neo4j import GraphDatabase
|
||||
from psycopg2 import sql
|
||||
from psycopg2.errors import DuplicateDatabase
|
||||
@@ -30,6 +31,11 @@ AGE_PORT = os.getenv("AGE_PORT", POSTGRES_PORT)
|
||||
AGE_DB = os.getenv("AGE_DB", POSTGRES_DB)
|
||||
AGE_GRAPH_NAME = os.getenv("AGE_GRAPH_NAME", "movie_graph")
|
||||
|
||||
INFLUX_ACTIVE = os.getenv("INFLUX_ACTIVE", "false").lower() == "true"
|
||||
INFLUXDB3_HOST = os.getenv("INFLUXDB3_HOST", "http://localhost:8181")
|
||||
INFLUXDB3_AUTH_TOKEN = os.getenv("INFLUXDB3_AUTH_TOKEN")
|
||||
INFLUXDB3_DATABASE = os.getenv("INFLUXDB3_DATABASE", "sensors")
|
||||
|
||||
USER_SEED_DATA = [
|
||||
(1, "Ivan", 15),
|
||||
(2, "Igor", 22),
|
||||
@@ -154,6 +160,48 @@ def select_age_movies(cur):
|
||||
return [str(row[0]).strip('"') for row in cur.fetchall()]
|
||||
|
||||
|
||||
def connect_influx():
|
||||
return InfluxDBClient3(
|
||||
host=INFLUXDB3_HOST,
|
||||
token=INFLUXDB3_AUTH_TOKEN,
|
||||
database=INFLUXDB3_DATABASE,
|
||||
)
|
||||
|
||||
|
||||
def insert_influx_sensor_data(client):
|
||||
client.write(
|
||||
"""temperature,location=room1,sensor_id=s01 value=22.5
|
||||
temperature,location=room2,sensor_id=s02 value=24.1
|
||||
temperature,location=room1,sensor_id=s01 value=23.0
|
||||
humidity,location=room1,sensor_id=s01 value=55.2
|
||||
humidity,location=room2,sensor_id=s02 value=60.8"""
|
||||
)
|
||||
|
||||
|
||||
def select_influx_sensor_data(client):
|
||||
table = client.query(
|
||||
query="""
|
||||
SELECT measurement, location, sensor_id, value, CAST(time AS STRING) AS time
|
||||
FROM (
|
||||
SELECT 'temperature' AS measurement, location, sensor_id, value, time FROM temperature
|
||||
UNION ALL
|
||||
SELECT 'humidity' AS measurement, location, sensor_id, value, time FROM humidity
|
||||
)
|
||||
ORDER BY time, measurement
|
||||
""",
|
||||
language="sql",
|
||||
mode="all",
|
||||
)
|
||||
rows = table.to_pylist()
|
||||
|
||||
if not rows:
|
||||
return ["measurement", "location", "sensor_id", "value", "time"], []
|
||||
|
||||
headers = list(rows[0].keys())
|
||||
values = [tuple(row.get(header) for header in headers) for row in rows]
|
||||
return headers, values
|
||||
|
||||
|
||||
def load_users():
|
||||
admin_conn = connect_postgres("postgres")
|
||||
try:
|
||||
@@ -180,7 +228,7 @@ class MainWindow(QMainWindow):
|
||||
central_widget = QWidget(self)
|
||||
self.setCentralWidget(central_widget)
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
grid_layout = QGridLayout()
|
||||
central_widget.setLayout(grid_layout)
|
||||
|
||||
self.table = QTableWidget(self)
|
||||
@@ -206,40 +254,49 @@ class MainWindow(QMainWindow):
|
||||
self.table.resizeColumnsToContents()
|
||||
|
||||
|
||||
class GraphWindow(QWidget):
|
||||
def __init__(self, title, parent_window):
|
||||
class DataWindow(QWidget):
|
||||
def __init__(self, title, headers, parent_window, window_index=0):
|
||||
super().__init__()
|
||||
|
||||
self.setWindowTitle(title)
|
||||
self.parent_window = parent_window
|
||||
self.window_index = window_index
|
||||
|
||||
grid_layout = QGridLayout(self)
|
||||
self.setLayout(grid_layout)
|
||||
|
||||
self.table = QTableWidget(self)
|
||||
self.table.setColumnCount(1)
|
||||
self.table.setColumnCount(len(headers))
|
||||
self.table.setRowCount(0)
|
||||
self.table.setHorizontalHeaderLabels(["Info"])
|
||||
self.table.horizontalHeaderItem(0).setToolTip("Info")
|
||||
self.table.horizontalHeaderItem(0).setTextAlignment(Qt.AlignHCenter)
|
||||
self.table.setHorizontalHeaderLabels(headers)
|
||||
for index, header in enumerate(headers):
|
||||
self.table.horizontalHeaderItem(index).setToolTip(header)
|
||||
self.table.horizontalHeaderItem(index).setTextAlignment(Qt.AlignHCenter)
|
||||
|
||||
grid_layout.addWidget(self.table, 0, 0)
|
||||
self.position_relative_to_parent(parent_window)
|
||||
self.position_relative_to_parent()
|
||||
|
||||
def position_relative_to_parent(self, parent_window):
|
||||
parent_frame = parent_window.frameGeometry()
|
||||
self.move(parent_frame.x() + parent_frame.width() + 5, parent_frame.y())
|
||||
def position_relative_to_parent(self):
|
||||
parent = self.parent_window.frameGeometry()
|
||||
screen = QApplication.primaryScreen().availableGeometry()
|
||||
self.move(
|
||||
max(screen.x(), min(parent.right() + 6 + self.window_index * (self.width() + 5), screen.right() - self.width() - 10)),
|
||||
max(screen.y(), min(parent.y(), screen.bottom() - self.height() - 10)),
|
||||
)
|
||||
|
||||
def load_data(self, rows):
|
||||
self.table.setRowCount(len(rows))
|
||||
for row_number, item in enumerate(rows):
|
||||
self.table.setItem(row_number, 0, QTableWidgetItem(str(item)))
|
||||
for row_number, row in enumerate(rows):
|
||||
values = row if isinstance(row, (list, tuple)) else [row]
|
||||
for column_number, value in enumerate(values):
|
||||
self.table.setItem(row_number, column_number, QTableWidgetItem(str(value)))
|
||||
self.table.resizeColumnsToContents()
|
||||
self.table.resizeRowsToContents()
|
||||
|
||||
margins = self.layout().contentsMargins()
|
||||
width = (
|
||||
self.table.verticalHeader().width()
|
||||
+ self.table.columnWidth(0)
|
||||
+ sum(self.table.columnWidth(column) for column in range(self.table.columnCount()))
|
||||
+ self.table.frameWidth() * 2
|
||||
+ margins.left()
|
||||
+ margins.right()
|
||||
@@ -254,7 +311,11 @@ class GraphWindow(QWidget):
|
||||
+ 24
|
||||
)
|
||||
|
||||
self.resize(min(max(width, 220), 700), min(max(height, 120), 500))
|
||||
screen_geometry = QApplication.primaryScreen().availableGeometry()
|
||||
max_width = max(screen_geometry.width() - 40, 220)
|
||||
max_height = max(screen_geometry.height() - 40, 120)
|
||||
self.resize(min(max(width, 220), max_width), min(max(height, 120), max_height))
|
||||
self.position_relative_to_parent()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -269,8 +330,8 @@ if __name__ == "__main__":
|
||||
|
||||
if NEO_ACTIVE:
|
||||
try:
|
||||
neo_window = GraphWindow("neo4j", main_window)
|
||||
neo_window.load_data(fetch_neo_movies())
|
||||
neo_window = DataWindow("neo4j", ["Info"], main_window, len(graph_windows))
|
||||
neo_window.load_data([(title,) for title in fetch_neo_movies()])
|
||||
neo_window.show()
|
||||
graph_windows.append(neo_window)
|
||||
except Exception as error:
|
||||
@@ -291,11 +352,24 @@ if __name__ == "__main__":
|
||||
create_age_movies(cur)
|
||||
age_movies = select_age_movies(cur)
|
||||
|
||||
age_window = GraphWindow("apache age", main_window)
|
||||
age_window.load_data(age_movies)
|
||||
age_window = DataWindow("apache age", ["Info"], main_window, len(graph_windows))
|
||||
age_window.load_data([(title,) for title in age_movies])
|
||||
age_window.show()
|
||||
graph_windows.append(age_window)
|
||||
except Exception as error:
|
||||
print(f"Apache AGE load failed: {error}")
|
||||
|
||||
if INFLUX_ACTIVE:
|
||||
try:
|
||||
with connect_influx() as influx_client:
|
||||
insert_influx_sensor_data(influx_client)
|
||||
influx_headers, influx_rows = select_influx_sensor_data(influx_client)
|
||||
|
||||
influx_window = DataWindow("influxdb 3 core", influx_headers, main_window, len(graph_windows))
|
||||
influx_window.load_data(influx_rows)
|
||||
influx_window.show()
|
||||
graph_windows.append(influx_window)
|
||||
except Exception as error:
|
||||
print(f"InfluxDB 3 Core load failed: {error}")
|
||||
|
||||
sys.exit(app.exec())
|
||||
@@ -2,3 +2,4 @@ psycopg2-binary
|
||||
neo4j
|
||||
PyQt5
|
||||
python-dotenv
|
||||
influxdb3-python
|
||||
|
||||
Reference in New Issue
Block a user