diff --git a/.gitignore b/.gitignore
index 505a3b1..fd3178c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,4 @@ wheels/
# Virtual environments
.venv
+*.csv
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..8817b7c
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ postgresql
+ true
+ org.postgresql.Driver
+ jdbc:postgresql://192.168.1.44:5432/postgres
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
new file mode 100644
index 0000000..708bfaa
--- /dev/null
+++ b/.idea/sqldialects.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/config.py b/config.py
index f2974e5..4e8ca9f 100644
--- a/config.py
+++ b/config.py
@@ -1,18 +1,56 @@
+from enum import Enum
+
import toml
from dataclasses import dataclass
+class DatabaseType(Enum):
+ PSQL = 1
+ ORCL = 2
+
@dataclass
class DatabaseConfig:
_host: str
+ _database_type: DatabaseType | None
+ _database_name: str
+ _database_ssid: str
+ _database_port: str
def __init__(self, config: dict):
self._host = config["HOST"]
+ type = config["DATABASE_TYPE"]
+
+ match type:
+ case 'PSQL':
+ self._database_type = DatabaseType.PSQL
+ case 'ORCL':
+ self._database_type = DatabaseType.ORCL
+ case _:
+ self._database_type = None
+
+ self._database_name = config["DATABASE_NAME"]
+ self._database_ssid = config["DATABASE_SSID"]
+ self._database_port = config["DATABASE_PORT"]
@property
def host(self) -> str:
return self._host
+ @property
+ def type(self) -> DatabaseType:
+ return self._database_type
+
+ @property
+ def name(self) -> str:
+ return self._database_name
+
+ @property
+ def ssid(self) -> str:
+ return self._database_ssid
+
+ @property
+ def port(self) -> str:
+ return self._database_port
@dataclass
class KeePassConfig:
@@ -21,9 +59,9 @@ class KeePassConfig:
_db_credentials_group: str
def __init__(self, config: dict):
- self._path = config["PATH"]
- self._db_credentials_name = config["DB_CREDENTIALS_NAME"]
- self._db_credentials_group = config["DB_CREDENTIALS_GROUP"]
+ self._path: str = config["PATH"]
+ self._db_credentials_name: str = config["DB_CREDENTIALS_NAME"]
+ self._db_credentials_group: str = config["DB_CREDENTIALS_GROUP"]
@property
def path(self) -> str:
@@ -42,11 +80,12 @@ class Config:
_config: dict
_kee_pass_config: KeePassConfig
_database_config: DatabaseConfig
+
def __init__(self):
with open('config.toml', 'r') as f:
- self._config = toml.load(f)
- self._kee_pass_config = KeePassConfig(self._config['keepass'])
- self._database_config = DatabaseConfig(self._config['database'])
+ self._config: dict = toml.load(f)
+ self._kee_pass_config: KeePassConfig = KeePassConfig(self._config['keepass'])
+ self._database_config: DatabaseConfig = DatabaseConfig(self._config['database'])
@property
def kee_pass(self) -> KeePassConfig:
diff --git a/config.toml b/config.toml
index 08ffc7a..aa07fc0 100644
--- a/config.toml
+++ b/config.toml
@@ -1,5 +1,9 @@
[database]
HOST = "192.168.1.44"
+DATABASE_TYPE = "PSQL"
+DATABASE_NAME = "op_test"
+DATABASE_SSID = "XE"
+DATABASE_PORT = "5432"
[keepass]
PATH = "/Users/frederik/Passwords.kdbx"
diff --git a/db_adapter.py b/db_adapter.py
new file mode 100644
index 0000000..91314c2
--- /dev/null
+++ b/db_adapter.py
@@ -0,0 +1,78 @@
+import sqlalchemy as sq
+import sqlparse
+import pandas as pd
+
+from config import DatabaseConfig, DatabaseType
+from keepass import KeePass
+
+
+def _read_and_sql_file_and_strip_for_comments(filename: str):
+ query: str
+ with open(filename, 'r') as f:
+ query = f.read()
+ query = sqlparse.format(query, strip_comments=True)
+ return query
+
+
+class DBAdapter:
+ _engine: sq.Engine
+ _database_config: DatabaseConfig
+ def __init__(self, keepass: KeePass, database_config: DatabaseConfig):
+ self._database_config = database_config
+ connection_string: str
+ keepass_entry = keepass.get_db_credentials()
+
+ match self._database_config.type:
+ case DatabaseType.PSQL:
+ connection_string = f'postgresql+pg8000://{keepass_entry.name}:{keepass_entry.password}@{self._database_config.host}/{self._database_config.name}'
+ case DatabaseType.ORCL:
+ connection_string = f'oracle:{keepass_entry.name}:{keepass_entry.password}@{self._database_config.host}:{self._database_config.port}:{self._database_config.ssid}'
+ case _:
+ raise Exception(f'Database type {database_config.type} is not supported')
+
+ self._engine: sq.Engine = sq.create_engine(connection_string)
+
+ def _set_transaction_readonly(self, conn: sq.Connection):
+ if not conn.in_transaction():
+ raise Exception('Connection is not in a transaction')
+
+ match self._database_config.type:
+ case DatabaseType.PSQL:
+ conn.execute(sq.text('SET TRANSACTION READ ONLY'))
+ case DatabaseType.ORCL:
+ conn.execute(sq.text('SET TRANSACTION READ ONLY'))
+ case _:
+ raise Exception(f'Database type {self.database_config.type} is not supported for readonly transactions')
+ def run_query(self, query: str, read_only = True) -> sq.CursorResult:
+ result: sq.CursorResult
+ with self._engine.connect() as conn:
+ with conn.begin():
+ try:
+ if read_only:
+ self._set_transaction_readonly(conn)
+ result = conn.execute(sq.text(query))
+ conn.commit()
+ except Exception as e:
+ conn.rollback()
+ raise e
+ return result
+
+ def run_sql_file_one_statement(self, filename: str = "query.sql", read_only = True) -> sq.CursorResult:
+ query = _read_and_sql_file_and_strip_for_comments(filename)
+ return self.run_query(query, read_only)
+
+ def run_sql_file_export_to_csv(self, input_name: str = "query.sql", output_name: str = "export.csv", read_only = True):
+ result: pd.DataFrame
+ query = _read_and_sql_file_and_strip_for_comments(input_name)
+
+ with self._engine.connect() as conn:
+ with conn.begin():
+ try:
+ if read_only:
+ self._set_transaction_readonly(conn)
+ result = pd.read_sql(query, conn)
+ conn.commit()
+ except Exception as e:
+ conn.rollback()
+ raise e
+ result.to_csv(output_name, index=False, sep=';')
diff --git a/keepass.py b/keepass.py
index a60426e..d82f2e5 100644
--- a/keepass.py
+++ b/keepass.py
@@ -1,16 +1,31 @@
from pykeepass import PyKeePass
from config import KeePassConfig
import getpass
-class KeePass:
- _kee_pass: PyKeePass
- _kee_pass_config: KeePassConfig
- _password: str
- def __init__(self, config: KeePassConfig):
- self._kee_pass_config = config
- self._password = getpass.getpass(f'KeePass password for {config.path}: ')
- self._kee_pass = PyKeePass(config.path, password=self._password)
+from dataclasses import dataclass
- def get_db_credentials(self):
+@dataclass
+class KeePassEntry:
+ def __init__(self, name: str, password: str):
+ self._name = name
+ self._password = password
+
+ @property
+ def password(self) -> str:
+ return self._password
+
+ @property
+ def name(self) -> str:
+ return self._name
+
+
+
+class KeePass:
+ def __init__(self, config: KeePassConfig):
+ self._kee_pass_config: KeePassConfig = config
+ self._password: str = getpass.getpass(f'KeePass password for {config.path}: ')
+ self._kee_pass: PyKeePass = PyKeePass(config.path, password=self._password)
+
+ def get_db_credentials(self) -> KeePassEntry:
group = self._kee_pass
if self._kee_pass_config.db_credentials_group.strip() != "" and self._kee_pass_config.db_credentials_group.strip() is not None:
group = self._kee_pass.find_entries(name=self._kee_pass_config.db_credentials_name)
@@ -23,4 +38,4 @@ class KeePass:
if len(group) != 1:
raise Exception(f'Could not find password, found {len(group)} entries')
- return group[0].username, group[0].password
\ No newline at end of file
+ return KeePassEntry(group[0].username, group[0].password)
\ No newline at end of file
diff --git a/main.py b/main.py
index f8c3207..c4c750c 100644
--- a/main.py
+++ b/main.py
@@ -1,4 +1,5 @@
from config import Config, DatabaseConfig, KeePassConfig
+from db_adapter import DBAdapter
from keepass import KeePass
config = Config()
@@ -6,4 +7,7 @@ print(config.kee_pass)
print(config.database)
keepass = KeePass(config.kee_pass)
-print(keepass.get_db_credentials())
\ No newline at end of file
+print(keepass.get_db_credentials())
+
+db_adapter = DBAdapter(keepass, config.database)
+db_adapter.run_sql_file_export_to_csv()
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 6a357cd..a1e50eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,8 +5,11 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
+ "cx-oracle>=8.3.0",
"pandas>=2.2.3",
+ "pg8000>=1.31.2",
"pykeepass>=4.1.0.post1",
"sqlalchemy>=2.0.36",
+ "sqlparse>=0.5.3",
"toml>=0.10.2",
]
diff --git a/query.sql b/query.sql
new file mode 100644
index 0000000..d6b9735
--- /dev/null
+++ b/query.sql
@@ -0,0 +1 @@
+SELECT * FROM simple_healthcheck_counters shc
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
index 163a580..d8bcba2 100644
--- a/uv.lock
+++ b/uv.lock
@@ -34,6 +34,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104 },
]
+[[package]]
+name = "asn1crypto"
+version = "1.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/de/cf/d547feed25b5244fcb9392e288ff9fdc3280b10260362fc45d37a798a6ee/asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c", size = 121080 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c9/7f/09065fd9e27da0eda08b4d6897f1c13535066174cc023af248fc2a8d5e5a/asn1crypto-1.5.1-py2.py3-none-any.whl", hash = "sha256:db4e40728b728508912cbb3d44f19ce188f218e9eba635821bb4b68564f8fd67", size = 105045 },
+]
+
[[package]]
name = "cffi"
version = "1.17.1"
@@ -65,22 +74,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/fb/08b3f4bf05da99aba8ffea52a558758def16e8516bc75ca94ff73587e7d3/construct-2.10.70-py3-none-any.whl", hash = "sha256:c80be81ef595a1a821ec69dc16099550ed22197615f4320b57cc9ce2a672cb30", size = 63020 },
]
+[[package]]
+name = "cx-oracle"
+version = "8.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e8/16/13c265afc984796fe38ee928733569b599cfd657245ddd1afad238b66656/cx_Oracle-8.3.0.tar.gz", hash = "sha256:3b2d215af4441463c97ea469b9cc307460739f89fdfa8ea222ea3518f1a424d9", size = 363886 }
+
[[package]]
name = "database-interacter"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "cx-oracle" },
{ name = "pandas" },
+ { name = "pg8000" },
{ name = "pykeepass" },
{ name = "sqlalchemy" },
+ { name = "sqlparse" },
{ name = "toml" },
]
[package.metadata]
requires-dist = [
+ { name = "cx-oracle", specifier = ">=8.3.0" },
{ name = "pandas", specifier = ">=2.2.3" },
+ { name = "pg8000", specifier = ">=1.31.2" },
{ name = "pykeepass", specifier = ">=4.1.0.post1" },
{ name = "sqlalchemy", specifier = ">=2.0.36" },
+ { name = "sqlparse", specifier = ">=0.5.3" },
{ name = "toml", specifier = ">=0.10.2" },
]
@@ -164,6 +185,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ab/5f/b38085618b950b79d2d9164a711c52b10aefc0ae6833b96f626b7021b2ed/pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a", size = 13098436 },
]
+[[package]]
+name = "pg8000"
+version = "1.31.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "python-dateutil" },
+ { name = "scramp" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0f/d7/0554640cbe3e193184796bedb6de23f797c03958425176faf0e694c06eb0/pg8000-1.31.2.tar.gz", hash = "sha256:1ea46cf09d8eca07fe7eaadefd7951e37bee7fabe675df164f1a572ffb300876", size = 113513 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/09/a0/2b30d52017c4ced8fc107386666ea7573954eb708bf66121f0229df05d41/pg8000-1.31.2-py3-none-any.whl", hash = "sha256:436c771ede71af4d4c22ba867a30add0bc5c942d7ab27fadbb6934a487ecc8f6", size = 54494 },
+]
+
[[package]]
name = "pycparser"
version = "2.22"
@@ -229,6 +263,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/11/c3/005fcca25ce078d2cc29fd559379817424e94885510568bc1bc53d7d5846/pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725", size = 508002 },
]
+[[package]]
+name = "scramp"
+version = "1.4.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asn1crypto" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f9/fa/8f1b99c3f875f334ac782e173ec03c35c246ec7a94fc5dd85153bc1d8285/scramp-1.4.5.tar.gz", hash = "sha256:be3fbe774ca577a7a658117dca014e5d254d158cecae3dd60332dfe33ce6d78e", size = 16169 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/9f/8b2f2749ccfbe4fcef08650896ac47ed919ff25b7ac57b7a1ae7da16c8c3/scramp-1.4.5-py3-none-any.whl", hash = "sha256:50e37c464fc67f37994e35bee4151e3d8f9320e9c204fca83a5d313c121bbbe7", size = 12781 },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -258,6 +304,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/49/21633706dd6feb14cd3f7935fc00b60870ea057686035e1a99ae6d9d9d53/SQLAlchemy-2.0.36-py3-none-any.whl", hash = "sha256:fddbe92b4760c6f5d48162aef14824add991aeda8ddadb3c31d56eb15ca69f8e", size = 1883787 },
]
+[[package]]
+name = "sqlparse"
+version = "0.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 },
+]
+
[[package]]
name = "toml"
version = "0.10.2"