postgresql-access 1.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 NMRhub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
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.
@@ -0,0 +1,5 @@
1
+ # postgresql_access
2
+ Helper functions for connecting to Postgresql
3
+
4
+ ## keyring
5
+ You must install a [keyring](https://pypi.org/project/keyring/) in your Python environment to use this package.
@@ -0,0 +1,20 @@
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
+
@@ -0,0 +1,19 @@
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
+
@@ -0,0 +1,7 @@
1
+
2
+ import importlib.metadata
3
+ import logging
4
+ postgresql_access_logger = logging.getLogger(__name__)
5
+
6
+ __version__ = importlib.metadata.version('postgresql_access')
7
+
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import pwd
4
+ from typing import Mapping
5
+
6
+ import psycopg2
7
+
8
+ """
9
+ object wrapper for psycopg2
10
+ """
11
+
12
+ def _current_user():
13
+ """get current linux user"""
14
+ return pwd.getpwuid(os.geteuid()).pw_name
15
+
16
+
17
+ class CertificateDatabase:
18
+
19
+ def __init__(self,*,host:str,database:str,user:str,application_name:str,client_cert:str,client_key:str,
20
+ root_cert:str):
21
+ self.app_name = 'Python app'
22
+ self.schema = None
23
+ self.port = 5432
24
+ self.host = host
25
+ self.database = database
26
+ self.user = user
27
+ self.application_name = application_name
28
+ self.client_cert = client_cert
29
+ self.client_key = client_key
30
+ self.root_cert = root_cert
31
+ files = (self.client_cert,self.client_key,self.root_cert)
32
+ missing = [f for f in files if not os.path.exists(f)]
33
+ if missing:
34
+ raise ValueError(f"Missing configuration file(s): {','.join(missing)}")
35
+ permissions = [f for f in files if not os.access(f,os.R_OK)]
36
+ if permissions:
37
+ raise ValueError(f"No read access file(s): {','.join(permissions)}")
38
+
39
+
40
+ def connect(self):
41
+ """Connect to database, set schema if present, return connection"""
42
+ connect_string = f"""host='{self.host}' dbname='{self.database}' user='{self.user}' port={self.port}"""
43
+ try:
44
+ conn = psycopg2.connect(connect_string, application_name=self.application_name,
45
+ sslmode='verify-full',
46
+ sslcert=self.client_cert,
47
+ sslkey=self.client_key,sslrootcert=self.root_cert)
48
+ except psycopg2.OperationalError as oe:
49
+ if 'no password' in str(oe):
50
+ raise ValueError(f"Invalid certificates or key for user {self.user}, or not authorized by server")
51
+ raise
52
+ if self.schema is not None:
53
+ with conn.cursor() as cursor:
54
+ cursor.execute(f"set search_path to {self.schema}")
55
+ conn.commit()
56
+
57
+ return conn
58
+
59
+ @staticmethod
60
+ def create_from_dict(data:Mapping,application_name:str):
61
+ h = data['host']
62
+ d = data['database']
63
+ u = data.get('user',_current_user())
64
+ cc = data['client certificate']
65
+ rc = data['root certificate']
66
+ ck = data['client key']
67
+ return CertificateDatabase(host=h, database=d, user=u, application_name=application_name, client_cert=cc,
68
+ client_key=ck, root_cert=rc)
69
+
@@ -0,0 +1,328 @@
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])
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import logging
4
+ from postgresql_access import postgresql_access_logger
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
+ postgresql_access_logger.setLevel(getattr(logging,args.loglevel))
14
+
15
+
16
+ if __name__ == "__main__":
17
+ main()
18
+
@@ -0,0 +1,18 @@
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.
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ setup.cfg
5
+ src/postgresql_access/__init__.py
6
+ src/postgresql_access/certificatedatabase.py
7
+ src/postgresql_access/database.py
8
+ src/postgresql_access/main.py
9
+ src/postgresql_access.egg-info/PKG-INFO
10
+ src/postgresql_access.egg-info/SOURCES.txt
11
+ src/postgresql_access.egg-info/dependency_links.txt
12
+ src/postgresql_access.egg-info/entry_points.txt
13
+ src/postgresql_access.egg-info/requires.txt
14
+ src/postgresql_access.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ postgresql_access = postgresql_access.main:main
@@ -0,0 +1,2 @@
1
+ psycopg2-binary
2
+ keyring
@@ -0,0 +1 @@
1
+ postgresql_access