add support Apache AGE

This commit is contained in:
2026-04-08 16:04:23 +03:00
parent dee23c76f7
commit dcf534a0fe
3 changed files with 253 additions and 179 deletions
+9 -1
View File
@@ -7,4 +7,12 @@ POSTGRES_PORT = 5432
NEO_ACTIVE=false
NEO_USER=neo4j
NEO_PASS=pass
NEO_HOST=neo4j://localhost:7687
NEO_HOST=neo4j://localhost:7687
AGE_ACTIVE=false
AGE_USER=postgres
AGE_PASS=pass
AGE_HOST=localhost
AGE_PORT=5432
AGE_DB=university
AGE_GRAPH_NAME=movie_graph
+12 -2
View File
@@ -1,12 +1,13 @@
### Пример приложения для работы с БД
Небольшое десктопприложение на PyQt5 для работы с PostgreSQL и Neo4j.
Небольшое десктоп-приложение на PyQt5 для работы с PostgreSQL, Neo4j и Apache AGE.
### Стек
- PyQt5 — графический интерфейс
- PostgreSQL (psycopg2) — реляционная БД для пользователей
- Neo4j — графовая БД для фильмов
- Apache AGE — графовая модель поверх PostgreSQL для фильмов
- python-dotenv — загрузка параметров подключения из файла `.env`
### Скриншоты
@@ -24,6 +25,7 @@
- Python 3.10+
- Установленный PostgreSQL и доступ к серверу БД
- Установленный Neo4j и загруженный обучающий граф фильмов (Movie Graph example)
- Для Apache AGE: установленное расширение `age` в PostgreSQL и доступ к базе, где оно включено
### Настройка окружения
@@ -43,6 +45,14 @@ NEO_ACTIVE=false # включить Neo4j-часть приложения,
NEO_USER=neo4j
NEO_PASS=your_password
NEO_HOST=neo4j://localhost:7687
AGE_ACTIVE=false # включить Apache AGE, установив true
AGE_USER=postgres
AGE_PASS=your_password
AGE_HOST=localhost
AGE_PORT=5432
AGE_DB=university
AGE_GRAPH_NAME=movie_graph
```
2. Создайте и активируйте виртуальное окружение:
@@ -77,4 +87,4 @@ pip install -r requirements.txt
py main.py
```
Приложение подключится к PostgreSQL, создаст БД/таблицу и тестовые данные (если их ещё нет), выполнит запрос к Neo4j (если активно) и отобразит результаты в двух окнах.
Приложение подключится к PostgreSQL, создаст БД/таблицу и тестовые данные (если их еще нет), выполнит запрос к Neo4j (если активно) и/или к Apache AGE (если активно), а затем отобразит результаты в отдельных окнах.
+232 -176
View File
@@ -1,245 +1,301 @@
import os
import sys
import psycopg2
from dotenv import load_dotenv
from neo4j import GraphDatabase
from psycopg2 import sql
from psycopg2.errors import DuplicateDatabase
from neo4j import GraphDatabase
from PyQt5.QtWidgets import QApplication, QMainWindow, QGridLayout, QWidget, QTableWidget, QTableWidgetItem, \
QHeaderView, QVBoxLayout, QLabel
from PyQt5.QtCore import QSize, Qt
from PyQt5.QtWidgets import QApplication, QGridLayout, QHeaderView, QMainWindow, QTableWidget, QTableWidgetItem, QWidget
import os
from dotenv import load_dotenv
load_dotenv()
NEO_USER = os.getenv('NEO_USER')
NEO_PASS = os.getenv('NEO_PASS')
NEO_HOST = os.getenv('NEO_HOST')
POSTGRES_USER = os.getenv("POSTGRES_USER")
POSTGRES_DB = os.getenv("POSTGRES_DB")
POSTGRES_PASS = os.getenv("POSTGRES_PASS")
POSTGRES_HOST = os.getenv("POSTGRES_HOST")
POSTGRES_PORT = os.getenv("POSTGRES_PORT")
POSTGRES_USER = os.getenv('POSTGRES_USER')
POSTGRES_DB = os.getenv('POSTGRES_DB')
POSTGRES_PASS = os.getenv('POSTGRES_PASS')
POSTGRES_HOST = os.getenv('POSTGRES_HOST')
POSTGRES_PORT = os.getenv('POSTGRES_PORT')
NEO_ACTIVE = os.getenv("NEO_ACTIVE", "false").lower() == "true"
NEO_USER = os.getenv("NEO_USER")
NEO_PASS = os.getenv("NEO_PASS")
NEO_HOST = os.getenv("NEO_HOST")
def connect(config):
""" Connect to the PostgreSQL database server """
AGE_ACTIVE = os.getenv("AGE_ACTIVE", "false").lower() == "true"
AGE_USER = os.getenv("AGE_USER", POSTGRES_USER)
AGE_PASS = os.getenv("AGE_PASS", POSTGRES_PASS)
AGE_HOST = os.getenv("AGE_HOST", POSTGRES_HOST)
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")
USER_SEED_DATA = [
(1, "Ivan", 15),
(2, "Igor", 22),
(3, "Alex", 16),
(4, "Anna", 40),
(5, "Inna", 30),
]
def connect_postgres(dbname):
conn = psycopg2.connect(
dbname=dbname,
user=POSTGRES_USER,
password=POSTGRES_PASS,
host=POSTGRES_HOST,
port=POSTGRES_PORT,
)
conn.autocommit = True
return conn
def create_database(admin_conn, database_name):
try:
# connecting to the PostgreSQL server
with psycopg2.connect(**config) as conn:
print('Connected to the PostgreSQL server.')
return conn
with admin_conn.cursor() as cur:
cur.execute(sql.SQL("CREATE DATABASE {}").format(sql.Identifier(database_name)))
print(f"Database '{database_name}' created.")
except DuplicateDatabase:
print(f"Database '{database_name}' already exists.")
def create_users_table(conn):
try:
with conn.cursor() as cur:
cur.execute(
"""
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY,
name VARCHAR(255),
age INT
)
"""
)
print("Users table is ready.")
except (psycopg2.DatabaseError, Exception) as error:
print(error)
def createUsersDB(config):
""" Connect to the PostgreSQL database server """
def select_users(conn):
try:
# connecting to the PostgreSQL server
conn.autocommit = True
cur = conn.cursor()
cur.execute(sql.SQL("CREATE DATABASE {}").format(
sql.Identifier('university'))
)
print('db created')
except DuplicateDatabase:
print('db already exists')
pass
def createuUiversityDB(config):
""" Connect to the PostgreSQL database server """
try:
# connecting to the PostgreSQL server
conn.autocommit = True
cur = conn.cursor()
cur.execute(sql.SQL("CREATE DATABASE {}").format(
sql.Identifier('university'))
)
print('db created')
except DuplicateDatabase:
print('db already exists')
pass
def createUsersTable(config):
""" Connect to the PostgreSQL database server """
try:
# connecting to the PostgreSQL server
conn.autocommit = True
cur = conn.cursor()
cur.execute(sql.SQL("CREATE TABLE IF NOT EXISTS users(id int, name varchar(255), age int)"))
print('table created')
except (psycopg2.DatabaseError, Exception) as error:
print(error)
def selectUsers(config):
try:
cur = conn.cursor()
cur.execute(sql.SQL("SELECT * FROM users"))
return cur.fetchall()
with conn.cursor() as cur:
cur.execute("SELECT id, name, age FROM users ORDER BY id")
return cur.fetchall()
except (psycopg2.DatabaseError, Exception) as error:
print(error)
return []
def insertUsersData(config, values):
""" Connect to the PostgreSQL database server """
def insert_users_data(conn, values):
try:
conn.autocommit = True
cur = conn.cursor()
existing_users = select_users(conn)
if existing_users:
print("Users are already seeded.")
return
users = selectUsers(conn)
if len(users) == 0:
args = ','.join(cur.mogrify("(%s,%s,%s)", i).decode('utf-8')
for i in values)
# executing the sql statement
cur.execute("INSERT INTO users VALUES " + (args))
print('inserted')
else:
print('already inserted')
print(users)
with conn.cursor() as cur:
args = ",".join(cur.mogrify("(%s,%s,%s)", value).decode("utf-8") for value in values)
cur.execute("INSERT INTO users (id, name, age) VALUES " + args)
print("Users inserted.")
except (psycopg2.DatabaseError, Exception) as error:
print(error)
def fetch_neo_movies():
uri = NEO_HOST
auth = (NEO_USER, NEO_PASS)
with GraphDatabase.driver(uri, auth=auth) as driver:
records, _, _ = driver.execute_query(
"""
MATCH (movie:Movie)
WHERE movie.released >= 1990 AND movie.released < 2000
RETURN movie.title AS title
ORDER BY title
"""
)
return [record["title"] for record in records]
def create_age_movies(cur):
cur.execute("CREATE EXTENSION IF NOT EXISTS age;")
cur.execute("LOAD 'age';")
cur.execute('SET search_path = ag_catalog, "$user", public;')
cur.execute("SELECT 1 FROM ag_catalog.ag_graph WHERE name = %s", (AGE_GRAPH_NAME,))
if cur.fetchone() is None:
cur.execute("SELECT * FROM ag_catalog.create_graph(%s)", (AGE_GRAPH_NAME,))
cur.execute(
sql.SQL("SELECT * FROM cypher({}, $$ {} $$) AS (seeded agtype)").format(
sql.Literal(AGE_GRAPH_NAME),
sql.SQL('''
MERGE (matrix:Movie {title: 'The Matrix', released: 1999})
MERGE (apollo:Movie {title: 'Apollo 13', released: 1995})
MERGE (toy_story:Movie {title: 'Toy Story', released: 1995})
RETURN matrix.title AS seeded
'''),
)
)
def select_age_movies(cur):
cur.execute(
sql.SQL("SELECT * FROM cypher({}, $$ {} $$) AS (title agtype)").format(
sql.Literal(AGE_GRAPH_NAME),
sql.SQL('''
MATCH (movie:Movie)
WHERE movie.released >= 1990 AND movie.released < 2000
RETURN movie.title AS title
ORDER BY title
'''),
)
)
return [str(row[0]).strip('"') for row in cur.fetchall()]
def load_users():
admin_conn = connect_postgres("postgres")
try:
create_database(admin_conn, POSTGRES_DB)
finally:
admin_conn.close()
app_conn = connect_postgres(POSTGRES_DB)
try:
create_users_table(app_conn)
insert_users_data(app_conn, USER_SEED_DATA)
return select_users(app_conn)
finally:
app_conn.close()
class MainWindow(QMainWindow):
# Override class constructor
def __init__(self):
# You must call the super class method
QMainWindow.__init__(self)
super().__init__()
self.setMinimumSize(QSize(640, 480)) # Set sizes
self.setWindowTitle("postgre") # Set the window title
central_widget = QWidget(self) # Create a central widget
self.setCentralWidget(central_widget) # Install the central widget
self.setMinimumSize(QSize(640, 480))
self.setWindowTitle("postgres")
grid_layout = QGridLayout(self) # Create QGridLayout
central_widget.setLayout(grid_layout) # Set this layout in central widget
central_widget = QWidget(self)
self.setCentralWidget(central_widget)
self.table = QTableWidget(self) # Create a table
self.table.setColumnCount(3) # Set three columns
self.table.setRowCount(1) # and one row
grid_layout = QGridLayout(self)
central_widget.setLayout(grid_layout)
self.table = QTableWidget(self)
self.table.setColumnCount(3)
self.table.setRowCount(0)
self.table.horizontalHeader().setSectionResizeMode(1, QHeaderView.Stretch)
# Set the table headers
self.table.setHorizontalHeaderLabels(["ID", "Name", "Age"])
# Set the tooltips to headings
self.table.horizontalHeaderItem(0).setToolTip("ID ")
self.table.horizontalHeaderItem(1).setToolTip("Name ")
self.table.horizontalHeaderItem(2).setToolTip("Age ")
# Set the alignment to the headers
self.table.horizontalHeaderItem(0).setToolTip("ID")
self.table.horizontalHeaderItem(1).setToolTip("Name")
self.table.horizontalHeaderItem(2).setToolTip("Age")
self.table.horizontalHeaderItem(0).setTextAlignment(Qt.AlignHCenter)
self.table.horizontalHeaderItem(1).setTextAlignment(Qt.AlignHCenter)
self.table.horizontalHeaderItem(2).setTextAlignment(Qt.AlignHCenter)
# Do the resize of the columns by content
self.table.resizeColumnsToContents()
grid_layout.addWidget(self.table, 0, 0) # Adding the table to the grid
grid_layout.addWidget(self.table, 0, 0)
def load_data(self, users_data):
for i, user in enumerate(users_data):
row_number = self.table.rowCount()-1
if len(users_data) != self.table.rowCount():
self.table.insertRow(row_number)
self.table.setRowCount(len(users_data))
for row_number, user in enumerate(users_data):
self.table.setItem(row_number, 0, QTableWidgetItem(str(user[0])))
self.table.setItem(row_number, 1, QTableWidgetItem(str(user[1])))
self.table.setItem(row_number, 2, QTableWidgetItem(str(user[2])))
self.table.resizeColumnsToContents()
class NeoWindow(QWidget):
def __init__(self, parent_window):
class GraphWindow(QWidget):
def __init__(self, title, parent_window):
super().__init__()
self.setWindowTitle("neo4j")
self.resize(300, 250)
self.setWindowTitle(title)
grid_layout = QGridLayout(self) # Create QGridLayout
self.setLayout(grid_layout) # Set this layout in central widget
grid_layout = QGridLayout(self)
self.setLayout(grid_layout)
self.table = QTableWidget(self)
self.table.setColumnCount(1)
self.table.setRowCount(1)
self.table.setRowCount(0)
self.table.setHorizontalHeaderLabels(["Info"])
# Set the tooltips to headings
self.table.horizontalHeaderItem(0).setToolTip("Info")
# Set the alignment to the headers
self.table.horizontalHeaderItem(0).setTextAlignment(Qt.AlignHCenter)
grid_layout.addWidget(self.table, 0, 0) # Adding the table to the grid
# Position the window to the right of the parent window
grid_layout.addWidget(self.table, 0, 0)
self.position_relative_to_parent(parent_window)
def position_relative_to_parent(self, parent_window):
# Get the geometry of the parent window
parent_frame = parent_window.frameGeometry()
self.move(parent_frame.x() + parent_frame.width() + 5, parent_frame.y())
# Calculate new position (right of parent window with a small gap)
new_x = parent_frame.x() + parent_frame.width() + 5
new_y = parent_frame.y()
# Move the window to the new position
self.move(new_x, new_y)
def load_data(self, neo_data):
for i, node_data in enumerate(neo_data):
row_number = self.table.rowCount()-1
if len(neo_data) != self.table.rowCount():
self.table.insertRow(row_number)
self.table.setItem(row_number, 0, QTableWidgetItem(str(node_data[0]["title"])))
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)))
self.table.resizeColumnsToContents()
self.table.resizeRowsToContents()
margins = self.layout().contentsMargins()
width = (
self.table.verticalHeader().width()
+ self.table.columnWidth(0)
+ self.table.frameWidth() * 2
+ margins.left()
+ margins.right()
+ 24
)
height = (
self.table.horizontalHeader().height()
+ sum(self.table.rowHeight(row) for row in range(self.table.rowCount()))
+ self.table.frameWidth() * 2
+ margins.top()
+ margins.bottom()
+ 24
)
self.resize(min(max(width, 220), 700), min(max(height, 120), 500))
if __name__ == "__main__":
import sys
conn = psycopg2.connect(
"user={0} password={1} host={2} port={3}".format(
POSTGRES_USER,
POSTGRES_PASS,
POSTGRES_HOST,
POSTGRES_PORT,
)
)
createuUiversityDB(conn)
conn = psycopg2.connect("user={0} dbname={1} password={2} host={3} port={4}".format(POSTGRES_USER, POSTGRES_DB,
POSTGRES_PASS, POSTGRES_HOST, POSTGRES_PORT))
createUsersTable(conn)
values = [(1, 'Ivan', 15), (2, 'Igor', 22), (3, 'Alex', 16), (4, 'Anna', 40), (5, 'Inna', 30)]
insertUsersData(conn, values)
users = selectUsers(conn)
users = load_users()
app = QApplication(sys.argv)
mw = MainWindow()
mw.load_data(users)
mw.show()
main_window = MainWindow()
main_window.load_data(users)
main_window.show()
if os.getenv('NEO_ACTIVE') == 'true':
URI = NEO_HOST
AUTH = (NEO_USER, NEO_PASS)
graph_windows = []
with GraphDatabase.driver(URI, auth=AUTH) as driver:
records, summary, keys = driver.execute_query(
"MATCH (nineties:Movie) WHERE nineties.released >= 1990 AND nineties.released < 2000 RETURN nineties"
)
if NEO_ACTIVE:
try:
neo_window = GraphWindow("neo4j", main_window)
neo_window.load_data(fetch_neo_movies())
neo_window.show()
graph_windows.append(neo_window)
except Exception as error:
print(f"Neo4j load failed: {error}")
neo_window = NeoWindow(mw)
neo_window.load_data(records)
neo_window.show()
if AGE_ACTIVE:
try:
with psycopg2.connect(
dbname=AGE_DB,
user=AGE_USER,
password=AGE_PASS,
host=AGE_HOST,
port=AGE_PORT,
) as age_conn:
age_conn.autocommit = True
with age_conn.cursor() as cur:
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.show()
graph_windows.append(age_window)
except Exception as error:
print(f"Apache AGE load failed: {error}")
sys.exit(app.exec())