postgresql-access 1.0__tar.gz → 2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- postgresql_access-2.0/PKG-INFO +28 -0
- postgresql_access-2.0/README.md +11 -0
- postgresql_access-2.0/pyproject.toml +28 -0
- postgresql_access-2.0/setup.cfg +4 -0
- postgresql_access-2.0/src/alchemy/satest.py +17 -0
- postgresql_access-2.0/src/postgresql_access/__init__.py +9 -0
- postgresql_access-2.0/src/postgresql_access/database.py +236 -0
- postgresql_access-2.0/src/postgresql_access.egg-info/PKG-INFO +28 -0
- {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/SOURCES.txt +4 -2
- postgresql_access-2.0/src/postgresql_access.egg-info/requires.txt +10 -0
- {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/top_level.txt +2 -0
- postgresql_access-2.0/src/tests/atest.py +37 -0
- postgresql_access-2.0/src/tests/grouptest.py +37 -0
- postgresql_access-1.0/PKG-INFO +0 -18
- postgresql_access-1.0/README.md +0 -5
- postgresql_access-1.0/pyproject.toml +0 -20
- postgresql_access-1.0/setup.cfg +0 -19
- postgresql_access-1.0/src/postgresql_access/__init__.py +0 -7
- postgresql_access-1.0/src/postgresql_access/database.py +0 -328
- postgresql_access-1.0/src/postgresql_access.egg-info/PKG-INFO +0 -18
- postgresql_access-1.0/src/postgresql_access.egg-info/requires.txt +0 -2
- {postgresql_access-1.0 → postgresql_access-2.0}/LICENSE +0 -0
- {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access/certificatedatabase.py +0 -0
- {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access/main.py +0 -0
- {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/dependency_links.txt +0 -0
- {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/entry_points.txt +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postgresql_access
|
|
3
|
+
Version: 2.0
|
|
4
|
+
Author-email: Gerard <gweatherby@uchc.edu>
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: keyring
|
|
10
|
+
Provides-Extra: psycopg2
|
|
11
|
+
Requires-Dist: psycopg2-binary; extra == "psycopg2"
|
|
12
|
+
Provides-Extra: psycopg3
|
|
13
|
+
Requires-Dist: psycopg[binary]; extra == "psycopg3"
|
|
14
|
+
Provides-Extra: alchemy
|
|
15
|
+
Requires-Dist: sqlalchemy; extra == "alchemy"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# postgresql_access
|
|
19
|
+
Helper functions for connecting to Postgresql. Either psycopg2 or psycopg3 should be installed.
|
|
20
|
+
|
|
21
|
+
## install options
|
|
22
|
+
* pip install postgresql_access
|
|
23
|
+
* pip install postgresql_access[psycopg2]
|
|
24
|
+
* pip install postgresql_access[psycopg3]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## keyring
|
|
28
|
+
You must install a [keyring](https://pypi.org/project/keyring/) in your Python environment to use this package.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# postgresql_access
|
|
2
|
+
Helper functions for connecting to Postgresql. Either psycopg2 or psycopg3 should be installed.
|
|
3
|
+
|
|
4
|
+
## install options
|
|
5
|
+
* pip install postgresql_access
|
|
6
|
+
* pip install postgresql_access[psycopg2]
|
|
7
|
+
* pip install postgresql_access[psycopg3]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## keyring
|
|
11
|
+
You must install a [keyring](https://pypi.org/project/keyring/) in your Python environment to use this package.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "postgresql_access"
|
|
3
|
+
version = "2.0"
|
|
4
|
+
dependencies = [
|
|
5
|
+
"keyring"
|
|
6
|
+
]
|
|
7
|
+
requires-python = ">=3.8"
|
|
8
|
+
|
|
9
|
+
authors = [
|
|
10
|
+
{ name = "Gerard", email = "gweatherby@uchc.edu" }
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
dynamic = []
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
postgresql_access = "postgresql_access.main:main"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
psycopg2 = ["psycopg2-binary"]
|
|
22
|
+
psycopg3 = ["psycopg[binary]"]
|
|
23
|
+
alchemy = ["sqlalchemy"]
|
|
24
|
+
|
|
25
|
+
[build-system]
|
|
26
|
+
requires = ["setuptools>=80", "wheel"]
|
|
27
|
+
build-backend = "setuptools.build_meta"
|
|
28
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import logging
|
|
4
|
+
_logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main():
|
|
8
|
+
logging.basicConfig()
|
|
9
|
+
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
10
|
+
parser.add_argument('-l', '--loglevel', default='WARN', help="Python logging level")
|
|
11
|
+
|
|
12
|
+
args = parser.parse_args()
|
|
13
|
+
_logger.setLevel(getattr(logging,args.loglevel))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if __name__ == "__main__":
|
|
17
|
+
main()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import importlib.metadata
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
postgresql_access_logger = logging.getLogger(__name__)
|
|
5
|
+
|
|
6
|
+
__version__ = importlib.metadata.version('postgresql_access')
|
|
7
|
+
|
|
8
|
+
from postgresql_access.database import AbstractDatabase, DatabaseConfig, DatabaseDict, ReadOnlyCursor, NewTransactionCursor
|
|
9
|
+
from postgresql_access.database import SelfCloseCursor, SelfCloseConnection
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import configparser
|
|
4
|
+
import getpass
|
|
5
|
+
import os
|
|
6
|
+
import pwd
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import Mapping, Any
|
|
9
|
+
|
|
10
|
+
import keyring
|
|
11
|
+
|
|
12
|
+
# Compatibility layer for psycopg2 and psycopg3
|
|
13
|
+
try:
|
|
14
|
+
import psycopg
|
|
15
|
+
psycopg_module = 'psycopg3'
|
|
16
|
+
connect = psycopg.connect
|
|
17
|
+
OperationalError = psycopg.OperationalError
|
|
18
|
+
except ImportError:
|
|
19
|
+
import psycopg2 as psycopg
|
|
20
|
+
psycopg_module = 'psycopg2'
|
|
21
|
+
connect = psycopg.connect
|
|
22
|
+
OperationalError = psycopg.OperationalError
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AbstractDatabase(ABC):
|
|
26
|
+
__DATABASE = 'database'
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self._app_name = 'Python app'
|
|
30
|
+
self.schema = None
|
|
31
|
+
self.port = 5432
|
|
32
|
+
self._sslmode = None
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def host(self) -> str: pass
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def database_name(self) -> str: pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def user(self) -> str: pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def password(self) -> str: pass
|
|
45
|
+
|
|
46
|
+
def set_app_name(self, name):
|
|
47
|
+
self._app_name = name
|
|
48
|
+
|
|
49
|
+
def application_name(self):
|
|
50
|
+
return self._app_name
|
|
51
|
+
|
|
52
|
+
def require_ssl(self):
|
|
53
|
+
self._sslmode = 'require'
|
|
54
|
+
|
|
55
|
+
def connect_fail(self, database, user, password, schema):
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def connect_success(self, database, user, password, schema):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
def build_connection_kwargs(self, dbname, user, password):
|
|
62
|
+
kwargs = dict(
|
|
63
|
+
host=self.host(),
|
|
64
|
+
dbname=dbname,
|
|
65
|
+
user=user,
|
|
66
|
+
password=password,
|
|
67
|
+
port=self.port,
|
|
68
|
+
application_name=self._app_name,
|
|
69
|
+
)
|
|
70
|
+
if self._sslmode:
|
|
71
|
+
kwargs['sslmode'] = self._sslmode
|
|
72
|
+
return kwargs
|
|
73
|
+
|
|
74
|
+
def connect(self, *, database_name: str = None, application_name: str = None, schema: str = None, **kwargs):
|
|
75
|
+
if application_name is not None:
|
|
76
|
+
self.set_app_name(application_name)
|
|
77
|
+
dbname = database_name or self.database_name()
|
|
78
|
+
user = self.user()
|
|
79
|
+
password = self.password()
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
conn = connect(**self.build_connection_kwargs(dbname, user, password), **kwargs)
|
|
83
|
+
self.connect_success(dbname, user, password, schema)
|
|
84
|
+
except OperationalError:
|
|
85
|
+
self.connect_fail(dbname, user, password, schema)
|
|
86
|
+
raise
|
|
87
|
+
|
|
88
|
+
if schema:
|
|
89
|
+
with conn.cursor() as cursor:
|
|
90
|
+
cursor.execute(f"SET search_path TO {schema}")
|
|
91
|
+
conn.commit()
|
|
92
|
+
|
|
93
|
+
return conn
|
|
94
|
+
|
|
95
|
+
class DatabaseSimple(AbstractDatabase):
|
|
96
|
+
def __init__(self, *, host: str, port: int = 5432, user: str, database_name: str):
|
|
97
|
+
super().__init__()
|
|
98
|
+
self._host = host
|
|
99
|
+
self.port = port
|
|
100
|
+
self.username = user
|
|
101
|
+
self._dbname = database_name
|
|
102
|
+
|
|
103
|
+
def host(self) -> str: return self._host
|
|
104
|
+
def database_name(self) -> str: return self._dbname
|
|
105
|
+
|
|
106
|
+
def user(self) -> str:
|
|
107
|
+
if not self.username:
|
|
108
|
+
u = input("database user: ")
|
|
109
|
+
self.username = u.strip()
|
|
110
|
+
return self.username
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def service_name(self):
|
|
114
|
+
return f"Database {self.host()}.{self.database_name()}"
|
|
115
|
+
|
|
116
|
+
def set_password(self, password) -> None:
|
|
117
|
+
keyring.set_password(self.service_name, self.user(), password)
|
|
118
|
+
|
|
119
|
+
def password(self) -> str:
|
|
120
|
+
if (pw := keyring.get_password(self.service_name, self.user())) is not None:
|
|
121
|
+
return pw
|
|
122
|
+
pw = getpass.getpass(f"Enter password for {self.service_name} {self.user()}")
|
|
123
|
+
return pw
|
|
124
|
+
|
|
125
|
+
def connect_success(self, database, user, password, schema):
|
|
126
|
+
self.set_password(password)
|
|
127
|
+
super().connect_success(database, user, password, schema)
|
|
128
|
+
|
|
129
|
+
class DatabaseDict(DatabaseSimple):
|
|
130
|
+
def __init__(self, *, dictionary: Mapping):
|
|
131
|
+
host = dictionary['host']
|
|
132
|
+
user = dictionary.get('user', None)
|
|
133
|
+
if user == 'linux user':
|
|
134
|
+
user = pwd.getpwuid(os.geteuid()).pw_name
|
|
135
|
+
dbname = dictionary['database']
|
|
136
|
+
port = int(dictionary.get('port', 5432))
|
|
137
|
+
super().__init__(host=host, port=port, user=user, database_name=dbname)
|
|
138
|
+
|
|
139
|
+
class DatabaseConfig(DatabaseDict):
|
|
140
|
+
def __init__(self, *, config: 'configparser.ConfigParser', section_key: str = 'database',
|
|
141
|
+
application_name: str = None):
|
|
142
|
+
config_section = config[section_key]
|
|
143
|
+
super().__init__(dictionary=config_section)
|
|
144
|
+
self.set_app_name(application_name)
|
|
145
|
+
|
|
146
|
+
class SelfCloseConnection:
|
|
147
|
+
def __init__(self, conn): self._conn = conn
|
|
148
|
+
def __enter__(self): return self._conn
|
|
149
|
+
def __exit__(self, exc_type, exc_val, exc_tb): self._conn.close()
|
|
150
|
+
|
|
151
|
+
class SelfCloseCursor:
|
|
152
|
+
def __init__(self, conn): self._conn = conn
|
|
153
|
+
def __enter__(self): return self._conn.cursor()
|
|
154
|
+
def __exit__(self, exc_type, exc_val, exc_tb): self._conn.close()
|
|
155
|
+
@property
|
|
156
|
+
def connection(self): return self._conn
|
|
157
|
+
|
|
158
|
+
class ReadOnlyCursor:
|
|
159
|
+
def __init__(self, conn, cursor_factory=None):
|
|
160
|
+
self._conn = conn
|
|
161
|
+
self._factory = cursor_factory
|
|
162
|
+
|
|
163
|
+
def __enter__(self):
|
|
164
|
+
self.existing_readonly = self._conn.readonly
|
|
165
|
+
self._conn.readonly = True
|
|
166
|
+
self._curs = self._conn.cursor(cursor_factory=self._factory)
|
|
167
|
+
return self._curs
|
|
168
|
+
|
|
169
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
170
|
+
self._curs.close()
|
|
171
|
+
self._conn.rollback()
|
|
172
|
+
self._conn.readonly = self.existing_readonly
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def connection(self): return self._conn
|
|
176
|
+
|
|
177
|
+
class NewTransactionCursor:
|
|
178
|
+
def __init__(self, conn, cursor_factory=None):
|
|
179
|
+
self._conn = conn
|
|
180
|
+
self._factory = cursor_factory
|
|
181
|
+
|
|
182
|
+
def __enter__(self):
|
|
183
|
+
self._conn.rollback()
|
|
184
|
+
return self._conn.cursor(cursor_factory=self._factory)
|
|
185
|
+
|
|
186
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
187
|
+
if exc_type is not None:
|
|
188
|
+
self._conn.rollback()
|
|
189
|
+
else:
|
|
190
|
+
self._conn.commit()
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def connection(self): return self._conn
|
|
194
|
+
|
|
195
|
+
class Qobject:
|
|
196
|
+
def __str__(self) -> str:
|
|
197
|
+
return ''.join(f"{k}: {v}\n" for k, v in self.__dict__.items() if not k.startswith('_'))
|
|
198
|
+
|
|
199
|
+
def query_to_object(cursor, query: str) -> list:
|
|
200
|
+
cursor.execute(query)
|
|
201
|
+
return cursor_to_objects(cursor)
|
|
202
|
+
|
|
203
|
+
def cursor_to_objects(cursor) -> list:
|
|
204
|
+
rval = []
|
|
205
|
+
rows = cursor.fetchall()
|
|
206
|
+
cols = [d[0] for d in cursor.description]
|
|
207
|
+
for r in rows:
|
|
208
|
+
qo = Qobject()
|
|
209
|
+
for col, val in zip(cols, r):
|
|
210
|
+
setattr(qo, col, val)
|
|
211
|
+
rval.append(qo)
|
|
212
|
+
return rval
|
|
213
|
+
|
|
214
|
+
def update_object_in_database(cursor: Any, obj, table: str, key: str) -> None:
|
|
215
|
+
query = f"UPDATE {table} SET "
|
|
216
|
+
sets, values, keyvalue = [], [], None
|
|
217
|
+
for field, value in obj.__dict__.items():
|
|
218
|
+
if field != key:
|
|
219
|
+
sets.append(f"{field} = %s")
|
|
220
|
+
values.append(value)
|
|
221
|
+
else:
|
|
222
|
+
keyvalue = value
|
|
223
|
+
if keyvalue is None:
|
|
224
|
+
raise ValueError(f"Key attribute {key} not found")
|
|
225
|
+
query += ','.join(sets) + f" WHERE {key} = %s"
|
|
226
|
+
values.append(keyvalue)
|
|
227
|
+
cursor.execute(query, values)
|
|
228
|
+
if cursor.rowcount != 1:
|
|
229
|
+
raise ValueError(f"No update for {table}.{key} value {keyvalue}")
|
|
230
|
+
|
|
231
|
+
def row_estimate(connection, table: str) -> int:
|
|
232
|
+
with connection.cursor() as curs:
|
|
233
|
+
curs.execute("""SELECT reltuples::bigint FROM pg_catalog.pg_class WHERE relname = %s""", (table,))
|
|
234
|
+
row = curs.fetchone()
|
|
235
|
+
return int(row[0])
|
|
236
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postgresql_access
|
|
3
|
+
Version: 2.0
|
|
4
|
+
Author-email: Gerard <gweatherby@uchc.edu>
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: keyring
|
|
10
|
+
Provides-Extra: psycopg2
|
|
11
|
+
Requires-Dist: psycopg2-binary; extra == "psycopg2"
|
|
12
|
+
Provides-Extra: psycopg3
|
|
13
|
+
Requires-Dist: psycopg[binary]; extra == "psycopg3"
|
|
14
|
+
Provides-Extra: alchemy
|
|
15
|
+
Requires-Dist: sqlalchemy; extra == "alchemy"
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# postgresql_access
|
|
19
|
+
Helper functions for connecting to Postgresql. Either psycopg2 or psycopg3 should be installed.
|
|
20
|
+
|
|
21
|
+
## install options
|
|
22
|
+
* pip install postgresql_access
|
|
23
|
+
* pip install postgresql_access[psycopg2]
|
|
24
|
+
* pip install postgresql_access[psycopg3]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
## keyring
|
|
28
|
+
You must install a [keyring](https://pypi.org/project/keyring/) in your Python environment to use this package.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
|
-
|
|
4
|
+
src/alchemy/satest.py
|
|
5
5
|
src/postgresql_access/__init__.py
|
|
6
6
|
src/postgresql_access/certificatedatabase.py
|
|
7
7
|
src/postgresql_access/database.py
|
|
@@ -11,4 +11,6 @@ src/postgresql_access.egg-info/SOURCES.txt
|
|
|
11
11
|
src/postgresql_access.egg-info/dependency_links.txt
|
|
12
12
|
src/postgresql_access.egg-info/entry_points.txt
|
|
13
13
|
src/postgresql_access.egg-info/requires.txt
|
|
14
|
-
src/postgresql_access.egg-info/top_level.txt
|
|
14
|
+
src/postgresql_access.egg-info/top_level.txt
|
|
15
|
+
src/tests/atest.py
|
|
16
|
+
src/tests/grouptest.py
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import configparser
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import keyring
|
|
7
|
+
from keyring import backend
|
|
8
|
+
|
|
9
|
+
from postgresql_access import postgresql_access_logger
|
|
10
|
+
from postgresql_access.database import DatabaseConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
# for kr in backend.get_all_keyring():
|
|
15
|
+
# print(kr)
|
|
16
|
+
kr = keyring.get_keyring()
|
|
17
|
+
print(kr)
|
|
18
|
+
logging.basicConfig()
|
|
19
|
+
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
20
|
+
parser.add_argument('-l', '--loglevel', default='WARN', help="Python logging level")
|
|
21
|
+
parser.add_argument('config',help="ini style config")
|
|
22
|
+
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
postgresql_access_logger.setLevel(getattr(logging,args.loglevel))
|
|
25
|
+
cp = configparser.ConfigParser()
|
|
26
|
+
with open(args.config) as f:
|
|
27
|
+
cp.read_file(f)
|
|
28
|
+
|
|
29
|
+
db = DatabaseConfig(config=cp)
|
|
30
|
+
c = db.connect(application_name="access test")
|
|
31
|
+
with c.cursor() as cursor:
|
|
32
|
+
cursor.execute('select now()')
|
|
33
|
+
print(cursor.fetchone()[0])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import configparser
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import keyring
|
|
7
|
+
from keyring import backend
|
|
8
|
+
|
|
9
|
+
from postgresql_access import postgresql_access_logger
|
|
10
|
+
from postgresql_access.database import DatabaseConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
# for kr in backend.get_all_keyring():
|
|
15
|
+
# print(kr)
|
|
16
|
+
kr = keyring.get_keyring()
|
|
17
|
+
print(kr)
|
|
18
|
+
logging.basicConfig()
|
|
19
|
+
parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
|
20
|
+
parser.add_argument('-l', '--loglevel', default='WARN', help="Python logging level")
|
|
21
|
+
parser.add_argument('config',help="ini style config")
|
|
22
|
+
|
|
23
|
+
args = parser.parse_args()
|
|
24
|
+
postgresql_access_logger.setLevel(getattr(logging,args.loglevel))
|
|
25
|
+
cp = configparser.ConfigParser()
|
|
26
|
+
with open(args.config) as f:
|
|
27
|
+
cp.read_file(f)
|
|
28
|
+
|
|
29
|
+
db = DatabaseConfig(config=cp)
|
|
30
|
+
c = db.connect(application_name="access test")
|
|
31
|
+
with c.cursor() as cursor:
|
|
32
|
+
cursor.execute('select now()')
|
|
33
|
+
print(cursor.fetchone()[0])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
main()
|
postgresql_access-1.0/PKG-INFO
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: postgresql_access
|
|
3
|
-
Version: 1.0
|
|
4
|
-
Home-page: https://github.com/NMRhub/postgresql_access.git
|
|
5
|
-
Author: Gerard
|
|
6
|
-
Author-email: gweatherby@uchc.edu
|
|
7
|
-
License: MIT license
|
|
8
|
-
Requires-Python: >=3.8
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Requires-Dist: psycopg2-binary
|
|
12
|
-
Requires-Dist: keyring
|
|
13
|
-
|
|
14
|
-
# postgresql_access
|
|
15
|
-
Helper functions for connecting to Postgresql
|
|
16
|
-
|
|
17
|
-
## keyring
|
|
18
|
-
You must install a [keyring](https://pypi.org/project/keyring/) in your Python environment to use this package.
|
postgresql_access-1.0/README.md
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "postgresql_access"
|
|
3
|
-
version = "1.0"
|
|
4
|
-
dependencies = ['psycopg2-binary',
|
|
5
|
-
'keyring']
|
|
6
|
-
requires-python= ">= 3.8"
|
|
7
|
-
authors = [
|
|
8
|
-
{name = "Gerard"},
|
|
9
|
-
{email = "gweatherby@uchc.edu"}
|
|
10
|
-
]
|
|
11
|
-
dynamic = ["license"]
|
|
12
|
-
readme = "README.md"
|
|
13
|
-
|
|
14
|
-
[project.scripts]
|
|
15
|
-
postgresql_access = "postgresql_access.main:main"
|
|
16
|
-
|
|
17
|
-
[build-system]
|
|
18
|
-
requires = ["setuptools"]
|
|
19
|
-
build-backend = "setuptools.build_meta"
|
|
20
|
-
|
postgresql_access-1.0/setup.cfg
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
[metadata]
|
|
2
|
-
description = TBD
|
|
3
|
-
license = MIT license
|
|
4
|
-
url = https://github.com/NMRhub/postgresql_access.git
|
|
5
|
-
|
|
6
|
-
[options]
|
|
7
|
-
package_dir =
|
|
8
|
-
= src
|
|
9
|
-
packages =
|
|
10
|
-
postgresql_access
|
|
11
|
-
install_requires =
|
|
12
|
-
|
|
13
|
-
[build_ext]
|
|
14
|
-
debug = 1
|
|
15
|
-
|
|
16
|
-
[egg_info]
|
|
17
|
-
tag_build =
|
|
18
|
-
tag_date = 0
|
|
19
|
-
|
|
@@ -1,328 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
import configparser
|
|
3
|
-
import getpass
|
|
4
|
-
import os
|
|
5
|
-
import pwd
|
|
6
|
-
from abc import ABC, abstractmethod
|
|
7
|
-
from typing import Mapping
|
|
8
|
-
|
|
9
|
-
import keyring
|
|
10
|
-
import psycopg2
|
|
11
|
-
import psycopg2.extensions
|
|
12
|
-
|
|
13
|
-
"""
|
|
14
|
-
object wrapper for psycopg2
|
|
15
|
-
database utility functions / classes
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
class AbstractDatabase(ABC):
|
|
19
|
-
"""OO wrapper for psycopg2 and Facade to connect PasswordCache"""
|
|
20
|
-
__DATABASE = 'database'
|
|
21
|
-
"""Password cache context"""
|
|
22
|
-
|
|
23
|
-
def __init__(self):
|
|
24
|
-
self._app_name = 'Python app'
|
|
25
|
-
self.schema = None
|
|
26
|
-
self.port = 5432
|
|
27
|
-
self._sslmode = None
|
|
28
|
-
|
|
29
|
-
@abstractmethod
|
|
30
|
-
def host(self) -> str:
|
|
31
|
-
pass
|
|
32
|
-
|
|
33
|
-
@abstractmethod
|
|
34
|
-
def database_name(self) -> str:
|
|
35
|
-
pass
|
|
36
|
-
|
|
37
|
-
@abstractmethod
|
|
38
|
-
def user(self) -> str:
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
@abstractmethod
|
|
42
|
-
def password(self) -> str:
|
|
43
|
-
pass
|
|
44
|
-
|
|
45
|
-
def set_app_name(self, name):
|
|
46
|
-
"""set application name"""
|
|
47
|
-
self._app_name = name
|
|
48
|
-
|
|
49
|
-
def application_name(self):
|
|
50
|
-
return self._app_name
|
|
51
|
-
|
|
52
|
-
def require_ssl(self):
|
|
53
|
-
"""Require SSL connection"""
|
|
54
|
-
self._sslmode = 'require'
|
|
55
|
-
|
|
56
|
-
def connect_fail(self, database, user, password, schema):
|
|
57
|
-
"""Overridable callback when connect fails"""
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
def connect_success(self, database, user, password, schema):
|
|
61
|
-
"""Overridable callback when connect fails"""
|
|
62
|
-
pass
|
|
63
|
-
|
|
64
|
-
def connect(self, *, database_name: str = None, application_name: str = None, schema: str = None,
|
|
65
|
-
**kwargs):
|
|
66
|
-
"""Connect to database, set schema if present, return connection
|
|
67
|
-
:param database_name: use instead of self.database_name()
|
|
68
|
-
:param application_name: name to use for connection string
|
|
69
|
-
:param schema: use instead of self.schema
|
|
70
|
-
:return database connection
|
|
71
|
-
"""
|
|
72
|
-
if application_name is not None:
|
|
73
|
-
appname = application_name
|
|
74
|
-
else:
|
|
75
|
-
appname = self.application_name()
|
|
76
|
-
if schema is not None:
|
|
77
|
-
sch = schema
|
|
78
|
-
else:
|
|
79
|
-
sch = self.schema
|
|
80
|
-
if database_name is not None:
|
|
81
|
-
dbname = database_name
|
|
82
|
-
else:
|
|
83
|
-
dbname = self.database_name()
|
|
84
|
-
user = self.user()
|
|
85
|
-
password = self.password()
|
|
86
|
-
connect_string = f"host='{self.host()}' dbname='{dbname}' user='{user}' password='{password}' port={self.port}"
|
|
87
|
-
if self._sslmode:
|
|
88
|
-
connect_string += f" sslmode='{self._sslmode}'"
|
|
89
|
-
try:
|
|
90
|
-
conn = psycopg2.connect(connect_string, application_name=appname,**kwargs)
|
|
91
|
-
self.connect_success(dbname, user, password, sch)
|
|
92
|
-
except psycopg2.OperationalError:
|
|
93
|
-
self.connect_fail(dbname, user, password, sch)
|
|
94
|
-
raise
|
|
95
|
-
if sch is not None:
|
|
96
|
-
with conn.cursor() as cursor:
|
|
97
|
-
cursor.execute("set search_path to {}".format(sch))
|
|
98
|
-
conn.commit()
|
|
99
|
-
|
|
100
|
-
return conn
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
class DatabaseSimple(AbstractDatabase):
|
|
104
|
-
"""
|
|
105
|
-
Create by specifying parameters
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
def __init__(self, *, host: str, port: int = 5432, user: str, database_name: str):
|
|
109
|
-
super().__init__()
|
|
110
|
-
self._host = host
|
|
111
|
-
self.port = port
|
|
112
|
-
self.username = user
|
|
113
|
-
self._dbname = database_name
|
|
114
|
-
self.ctx = None
|
|
115
|
-
self._pobj = None
|
|
116
|
-
|
|
117
|
-
def host(self) -> str:
|
|
118
|
-
return self._host
|
|
119
|
-
|
|
120
|
-
def database_name(self) -> str:
|
|
121
|
-
return self._dbname
|
|
122
|
-
|
|
123
|
-
def user(self) -> str:
|
|
124
|
-
if not self.username:
|
|
125
|
-
u = input("database user: ")
|
|
126
|
-
self.username = u.strip()
|
|
127
|
-
return self.username
|
|
128
|
-
|
|
129
|
-
@property
|
|
130
|
-
def service_name(self):
|
|
131
|
-
return f"Database {self.host()}.{self.database_name()}"
|
|
132
|
-
|
|
133
|
-
def set_password(self,password) -> None:
|
|
134
|
-
""""Set password explicitly"""
|
|
135
|
-
keyring.set_password(self.service_name,self.user(),password)
|
|
136
|
-
|
|
137
|
-
def password(self) -> str:
|
|
138
|
-
"""Get password from keyring or prompt"""
|
|
139
|
-
if (pw := keyring.get_password(self.service_name,self.user())) is not None:
|
|
140
|
-
return pw
|
|
141
|
-
pw = getpass.getpass(f"Enter password for {self.service_name} {self.user()}")
|
|
142
|
-
return pw
|
|
143
|
-
|
|
144
|
-
def connect_success(self, database, user, password, schema):
|
|
145
|
-
self.set_password(password)
|
|
146
|
-
super().connect_success(database, user, password, schema)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
class DatabaseDict(DatabaseSimple):
|
|
150
|
-
"""
|
|
151
|
-
Create from dictionary with host/user/database keys
|
|
152
|
-
"""
|
|
153
|
-
|
|
154
|
-
def __init__(self, *, dictionary: Mapping):
|
|
155
|
-
host = dictionary['host']
|
|
156
|
-
user = dictionary.get('user', None)
|
|
157
|
-
if user == 'linux user':
|
|
158
|
-
user = pwd.getpwuid(os.geteuid()).pw_name
|
|
159
|
-
dbname = dictionary['database']
|
|
160
|
-
port = int(dictionary.get('port', 5432))
|
|
161
|
-
super().__init__(host=host, port=port, user=user, database_name=dbname)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
class DatabaseConfig(DatabaseDict):
|
|
165
|
-
|
|
166
|
-
def __init__(self, *, config: 'configparser.ConfigParser', section_key: str = 'database',
|
|
167
|
-
application_name:str = None):
|
|
168
|
-
config_section = config[section_key]
|
|
169
|
-
super().__init__(dictionary=config_section)
|
|
170
|
-
self.set_app_name(application_name)
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
class SelfCloseConnection:
|
|
174
|
-
"""A ContextManger connection which closes itself when it goes out of scope"""
|
|
175
|
-
|
|
176
|
-
def __init__(self, conn):
|
|
177
|
-
self._conn = conn
|
|
178
|
-
|
|
179
|
-
def __enter__(self):
|
|
180
|
-
return self._conn
|
|
181
|
-
|
|
182
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
183
|
-
self._conn.close()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
class SelfCloseCursor:
|
|
187
|
-
"""A ContextManger cursor which closes the connection when it goes out of scope"""
|
|
188
|
-
|
|
189
|
-
def __init__(self, conn) -> None:
|
|
190
|
-
self._conn = conn
|
|
191
|
-
|
|
192
|
-
def __enter__(self):
|
|
193
|
-
return self._conn.cursor()
|
|
194
|
-
|
|
195
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
196
|
-
self._conn.close()
|
|
197
|
-
|
|
198
|
-
@property
|
|
199
|
-
def connection(self):
|
|
200
|
-
"""Return connection"""
|
|
201
|
-
return self._conn
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class ReadOnlyCursor:
|
|
205
|
-
"""A ContextManager cursor which sets the session to readonly. Fails if current transaction is in place"""
|
|
206
|
-
|
|
207
|
-
def __init__(self, conn, cursor_factory=None) -> None:
|
|
208
|
-
self._conn = conn
|
|
209
|
-
self._factory = cursor_factory
|
|
210
|
-
|
|
211
|
-
def __enter__(self):
|
|
212
|
-
self.existing_readonly = self._conn.readonly
|
|
213
|
-
self._conn.readonly = True
|
|
214
|
-
self._curs = self._conn.cursor(cursor_factory=self._factory)
|
|
215
|
-
return self._curs
|
|
216
|
-
|
|
217
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
218
|
-
self._curs.close()
|
|
219
|
-
self._conn.rollback()
|
|
220
|
-
self._conn.readonly = self.existing_readonly
|
|
221
|
-
|
|
222
|
-
@property
|
|
223
|
-
def connection(self):
|
|
224
|
-
"""Return connection"""
|
|
225
|
-
return self._conn
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
class NewTransactionCursor:
|
|
229
|
-
"""A ContextManger cursor which starts a new transaction (rollbacks any current SQL) statements,
|
|
230
|
-
and commits in on normal exit"""
|
|
231
|
-
|
|
232
|
-
def __init__(self, conn, cursor_factory=None) -> None:
|
|
233
|
-
self._conn = conn
|
|
234
|
-
self._factory = cursor_factory
|
|
235
|
-
|
|
236
|
-
def __enter__(self):
|
|
237
|
-
self._conn.rollback()
|
|
238
|
-
return self._conn.cursor(cursor_factory=self._factory)
|
|
239
|
-
|
|
240
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
241
|
-
if exc_type is not None:
|
|
242
|
-
self._conn.rollback()
|
|
243
|
-
else:
|
|
244
|
-
self._conn.commit()
|
|
245
|
-
|
|
246
|
-
@property
|
|
247
|
-
def connection(self):
|
|
248
|
-
"""Return connection"""
|
|
249
|
-
return self._conn
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
class Qobject:
|
|
253
|
-
"""Auto object generated from results of query"""
|
|
254
|
-
|
|
255
|
-
def __str__(self) -> str:
|
|
256
|
-
rval = ""
|
|
257
|
-
for k, v in self.__dict__.items():
|
|
258
|
-
if not k.startswith('_'):
|
|
259
|
-
rval += k + ": " + str(v) + '\n'
|
|
260
|
-
return rval
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def query_to_object(cursor, query: str) -> list:
|
|
264
|
-
"""
|
|
265
|
-
Convert generic query into list of objects
|
|
266
|
-
:param cursor:
|
|
267
|
-
:param query:
|
|
268
|
-
:return: list of Objects with fields named after columns in query
|
|
269
|
-
"""
|
|
270
|
-
cursor.execute(query)
|
|
271
|
-
return cursor_to_objects(cursor)
|
|
272
|
-
|
|
273
|
-
def cursor_to_objects(cursor) -> list:
|
|
274
|
-
"""
|
|
275
|
-
Convert cursor results to list of objects
|
|
276
|
-
:param cursor: cursor that has just executed query
|
|
277
|
-
:return: list of Objects with fields named after columns in query
|
|
278
|
-
"""
|
|
279
|
-
rval = []
|
|
280
|
-
rows = cursor.fetchall()
|
|
281
|
-
cols = [d[0] for d in cursor.description]
|
|
282
|
-
ncols = len(cols)
|
|
283
|
-
for r in rows:
|
|
284
|
-
qo = Qobject()
|
|
285
|
-
for i in range(ncols):
|
|
286
|
-
v = r[i]
|
|
287
|
-
setattr(qo, cols[i], v)
|
|
288
|
-
rval.append(qo)
|
|
289
|
-
return rval
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
def update_object_in_database(cursor: psycopg2.extensions.cursor, object, table: str, key: str) -> None:
|
|
293
|
-
"""update an object whose fields match columns names into database
|
|
294
|
-
:param cursor: open cursor with write access
|
|
295
|
-
:param object: data source
|
|
296
|
-
:param table: name of table to update
|
|
297
|
-
:param key: primary key column name (only single key supported)
|
|
298
|
-
:raises ValueError if key value not found on object or single row not updated
|
|
299
|
-
"""
|
|
300
|
-
query = "update {} set ".format(table)
|
|
301
|
-
sets = []
|
|
302
|
-
values = []
|
|
303
|
-
keyvalue = None
|
|
304
|
-
for field, value in object.__dict__.items():
|
|
305
|
-
if field != key:
|
|
306
|
-
sets.append('{} = %s'.format(field))
|
|
307
|
-
values.append(value)
|
|
308
|
-
else:
|
|
309
|
-
keyvalue = value
|
|
310
|
-
if keyvalue is None:
|
|
311
|
-
raise ValueError("Key attribute {} not found on {}".format(key, object))
|
|
312
|
-
query += ','.join(sets)
|
|
313
|
-
query += ' where {} = %s'.format(key)
|
|
314
|
-
values.append(keyvalue)
|
|
315
|
-
cursor.execute(query, values)
|
|
316
|
-
if cursor.rowcount != 1:
|
|
317
|
-
raise ValueError("No update for {}.{} value {}".format(table, key, keyvalue))
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
def row_estimate(connection, table: str) -> int:
|
|
321
|
-
"""A quick estimate about how many rows
|
|
322
|
-
are in a table. +/ 10%"""
|
|
323
|
-
with connection.cursor() as curs:
|
|
324
|
-
curs.execute("""SELECT reltuples::bigint
|
|
325
|
-
FROM pg_catalog.pg_class
|
|
326
|
-
WHERE relname = %s""", (table,))
|
|
327
|
-
row = curs.fetchone()
|
|
328
|
-
return int(row[0])
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: postgresql_access
|
|
3
|
-
Version: 1.0
|
|
4
|
-
Home-page: https://github.com/NMRhub/postgresql_access.git
|
|
5
|
-
Author: Gerard
|
|
6
|
-
Author-email: gweatherby@uchc.edu
|
|
7
|
-
License: MIT license
|
|
8
|
-
Requires-Python: >=3.8
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
License-File: LICENSE
|
|
11
|
-
Requires-Dist: psycopg2-binary
|
|
12
|
-
Requires-Dist: keyring
|
|
13
|
-
|
|
14
|
-
# postgresql_access
|
|
15
|
-
Helper functions for connecting to Postgresql
|
|
16
|
-
|
|
17
|
-
## keyring
|
|
18
|
-
You must install a [keyring](https://pypi.org/project/keyring/) in your Python environment to use this package.
|
|
File without changes
|
{postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access/certificatedatabase.py
RENAMED
|
File without changes
|
|
File without changes
|
{postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/entry_points.txt
RENAMED
|
File without changes
|