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.
- postgresql_access-1.0/LICENSE +21 -0
- postgresql_access-1.0/PKG-INFO +18 -0
- postgresql_access-1.0/README.md +5 -0
- postgresql_access-1.0/pyproject.toml +20 -0
- postgresql_access-1.0/setup.cfg +19 -0
- postgresql_access-1.0/src/postgresql_access/__init__.py +7 -0
- postgresql_access-1.0/src/postgresql_access/certificatedatabase.py +69 -0
- postgresql_access-1.0/src/postgresql_access/database.py +328 -0
- postgresql_access-1.0/src/postgresql_access/main.py +18 -0
- postgresql_access-1.0/src/postgresql_access.egg-info/PKG-INFO +18 -0
- postgresql_access-1.0/src/postgresql_access.egg-info/SOURCES.txt +14 -0
- postgresql_access-1.0/src/postgresql_access.egg-info/dependency_links.txt +1 -0
- postgresql_access-1.0/src/postgresql_access.egg-info/entry_points.txt +2 -0
- postgresql_access-1.0/src/postgresql_access.egg-info/requires.txt +2 -0
- postgresql_access-1.0/src/postgresql_access.egg-info/top_level.txt +1 -0
|
@@ -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
postgresql_access
|