add support Apache AGE
This commit is contained in:
+9
-1
@@ -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
|
||||
@@ -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 (если активно), а затем отобразит результаты в отдельных окнах.
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user