Add InfluxDB 3 Core support.
This commit is contained in:
@@ -16,3 +16,8 @@ AGE_HOST=localhost
|
|||||||
AGE_PORT=5432
|
AGE_PORT=5432
|
||||||
AGE_DB=university
|
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 — графический интерфейс
|
- PyQt5 — графический интерфейс
|
||||||
- PostgreSQL (psycopg2) — реляционная БД для пользователей
|
- PostgreSQL (psycopg2) — реляционная БД для пользователей
|
||||||
- Neo4j — графовая БД для фильмов
|
- Neo4j — графовая БД
|
||||||
- Apache AGE — графовая модель поверх PostgreSQL для фильмов
|
- Apache AGE — графовая БД в рамках PostgreSQL
|
||||||
- python-dotenv — загрузка параметров подключения из файла `.env`
|
- InfluxDB 3 Core — БД временных рядов
|
||||||
|
|
||||||
### Скриншоты
|
### Скриншоты
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
- Установленный PostgreSQL и доступ к серверу БД
|
- Установленный PostgreSQL и доступ к серверу БД
|
||||||
- Установленный Neo4j и загруженный обучающий граф фильмов (Movie Graph example)
|
- Установленный Neo4j и загруженный обучающий граф фильмов (Movie Graph example)
|
||||||
- Для Apache AGE: установленное расширение `age` в PostgreSQL и доступ к базе, где оно включено
|
- Для Apache AGE: установленное расширение `age` в PostgreSQL и доступ к базе, где оно включено
|
||||||
|
- Для InfluxDB 3 Core: запущенный сервер, заранее созданная база и токен с правами на запись и чтение
|
||||||
|
|
||||||
### Настройка окружения
|
### Настройка окружения
|
||||||
|
|
||||||
@@ -53,6 +54,11 @@ AGE_HOST=localhost
|
|||||||
AGE_PORT=5432
|
AGE_PORT=5432
|
||||||
AGE_DB=university
|
AGE_DB=university
|
||||||
AGE_GRAPH_NAME=movie_graph
|
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. Создайте и активируйте виртуальное окружение:
|
2. Создайте и активируйте виртуальное окружение:
|
||||||
@@ -87,4 +93,4 @@ pip install -r requirements.txt
|
|||||||
py main.py
|
py main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Приложение подключится к PostgreSQL, создаст БД/таблицу и тестовые данные (если их еще нет), выполнит запрос к Neo4j (если активно) и/или к Apache AGE (если активно), а затем отобразит результаты в отдельных окнах.
|
Приложение подключится к PostgreSQL, создаст БД/таблицу и тестовые данные (если их еще нет), выполнит запрос к Neo4j, Apache AGE и/или InfluxDB 3 Core (если они активны), а затем отобразит результаты в отдельных окнах.
|
||||||
@@ -3,6 +3,7 @@ import sys
|
|||||||
|
|
||||||
import psycopg2
|
import psycopg2
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
from influxdb_client_3 import InfluxDBClient3
|
||||||
from neo4j import GraphDatabase
|
from neo4j import GraphDatabase
|
||||||
from psycopg2 import sql
|
from psycopg2 import sql
|
||||||
from psycopg2.errors import DuplicateDatabase
|
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_DB = os.getenv("AGE_DB", POSTGRES_DB)
|
||||||
AGE_GRAPH_NAME = os.getenv("AGE_GRAPH_NAME", "movie_graph")
|
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 = [
|
USER_SEED_DATA = [
|
||||||
(1, "Ivan", 15),
|
(1, "Ivan", 15),
|
||||||
(2, "Igor", 22),
|
(2, "Igor", 22),
|
||||||
@@ -154,6 +160,48 @@ def select_age_movies(cur):
|
|||||||
return [str(row[0]).strip('"') for row in cur.fetchall()]
|
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():
|
def load_users():
|
||||||
admin_conn = connect_postgres("postgres")
|
admin_conn = connect_postgres("postgres")
|
||||||
try:
|
try:
|
||||||
@@ -180,7 +228,7 @@ class MainWindow(QMainWindow):
|
|||||||
central_widget = QWidget(self)
|
central_widget = QWidget(self)
|
||||||
self.setCentralWidget(central_widget)
|
self.setCentralWidget(central_widget)
|
||||||
|
|
||||||
grid_layout = QGridLayout(self)
|
grid_layout = QGridLayout()
|
||||||
central_widget.setLayout(grid_layout)
|
central_widget.setLayout(grid_layout)
|
||||||
|
|
||||||
self.table = QTableWidget(self)
|
self.table = QTableWidget(self)
|
||||||
@@ -206,40 +254,49 @@ class MainWindow(QMainWindow):
|
|||||||
self.table.resizeColumnsToContents()
|
self.table.resizeColumnsToContents()
|
||||||
|
|
||||||
|
|
||||||
class GraphWindow(QWidget):
|
class DataWindow(QWidget):
|
||||||
def __init__(self, title, parent_window):
|
def __init__(self, title, headers, parent_window, window_index=0):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(title)
|
||||||
|
self.parent_window = parent_window
|
||||||
|
self.window_index = window_index
|
||||||
|
|
||||||
grid_layout = QGridLayout(self)
|
grid_layout = QGridLayout(self)
|
||||||
self.setLayout(grid_layout)
|
self.setLayout(grid_layout)
|
||||||
|
|
||||||
self.table = QTableWidget(self)
|
self.table = QTableWidget(self)
|
||||||
self.table.setColumnCount(1)
|
self.table.setColumnCount(len(headers))
|
||||||
self.table.setRowCount(0)
|
self.table.setRowCount(0)
|
||||||
self.table.setHorizontalHeaderLabels(["Info"])
|
self.table.setHorizontalHeaderLabels(headers)
|
||||||
self.table.horizontalHeaderItem(0).setToolTip("Info")
|
for index, header in enumerate(headers):
|
||||||
self.table.horizontalHeaderItem(0).setTextAlignment(Qt.AlignHCenter)
|
self.table.horizontalHeaderItem(index).setToolTip(header)
|
||||||
|
self.table.horizontalHeaderItem(index).setTextAlignment(Qt.AlignHCenter)
|
||||||
|
|
||||||
grid_layout.addWidget(self.table, 0, 0)
|
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):
|
def position_relative_to_parent(self):
|
||||||
parent_frame = parent_window.frameGeometry()
|
parent = self.parent_window.frameGeometry()
|
||||||
self.move(parent_frame.x() + parent_frame.width() + 5, parent_frame.y())
|
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):
|
def load_data(self, rows):
|
||||||
self.table.setRowCount(len(rows))
|
self.table.setRowCount(len(rows))
|
||||||
for row_number, item in enumerate(rows):
|
for row_number, row in enumerate(rows):
|
||||||
self.table.setItem(row_number, 0, QTableWidgetItem(str(item)))
|
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.resizeColumnsToContents()
|
||||||
self.table.resizeRowsToContents()
|
self.table.resizeRowsToContents()
|
||||||
|
|
||||||
margins = self.layout().contentsMargins()
|
margins = self.layout().contentsMargins()
|
||||||
width = (
|
width = (
|
||||||
self.table.verticalHeader().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
|
+ self.table.frameWidth() * 2
|
||||||
+ margins.left()
|
+ margins.left()
|
||||||
+ margins.right()
|
+ margins.right()
|
||||||
@@ -254,7 +311,11 @@ class GraphWindow(QWidget):
|
|||||||
+ 24
|
+ 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__":
|
if __name__ == "__main__":
|
||||||
@@ -269,8 +330,8 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
if NEO_ACTIVE:
|
if NEO_ACTIVE:
|
||||||
try:
|
try:
|
||||||
neo_window = GraphWindow("neo4j", main_window)
|
neo_window = DataWindow("neo4j", ["Info"], main_window, len(graph_windows))
|
||||||
neo_window.load_data(fetch_neo_movies())
|
neo_window.load_data([(title,) for title in fetch_neo_movies()])
|
||||||
neo_window.show()
|
neo_window.show()
|
||||||
graph_windows.append(neo_window)
|
graph_windows.append(neo_window)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
@@ -291,11 +352,24 @@ if __name__ == "__main__":
|
|||||||
create_age_movies(cur)
|
create_age_movies(cur)
|
||||||
age_movies = select_age_movies(cur)
|
age_movies = select_age_movies(cur)
|
||||||
|
|
||||||
age_window = GraphWindow("apache age", main_window)
|
age_window = DataWindow("apache age", ["Info"], main_window, len(graph_windows))
|
||||||
age_window.load_data(age_movies)
|
age_window.load_data([(title,) for title in age_movies])
|
||||||
age_window.show()
|
age_window.show()
|
||||||
graph_windows.append(age_window)
|
graph_windows.append(age_window)
|
||||||
except Exception as error:
|
except Exception as error:
|
||||||
print(f"Apache AGE load failed: {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())
|
sys.exit(app.exec())
|
||||||
@@ -2,3 +2,4 @@ psycopg2-binary
|
|||||||
neo4j
|
neo4j
|
||||||
PyQt5
|
PyQt5
|
||||||
python-dotenv
|
python-dotenv
|
||||||
|
influxdb3-python
|
||||||
|
|||||||
Reference in New Issue
Block a user