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"