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.
Files changed (26) hide show
  1. postgresql_access-2.0/PKG-INFO +28 -0
  2. postgresql_access-2.0/README.md +11 -0
  3. postgresql_access-2.0/pyproject.toml +28 -0
  4. postgresql_access-2.0/setup.cfg +4 -0
  5. postgresql_access-2.0/src/alchemy/satest.py +17 -0
  6. postgresql_access-2.0/src/postgresql_access/__init__.py +9 -0
  7. postgresql_access-2.0/src/postgresql_access/database.py +236 -0
  8. postgresql_access-2.0/src/postgresql_access.egg-info/PKG-INFO +28 -0
  9. {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/SOURCES.txt +4 -2
  10. postgresql_access-2.0/src/postgresql_access.egg-info/requires.txt +10 -0
  11. {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/top_level.txt +2 -0
  12. postgresql_access-2.0/src/tests/atest.py +37 -0
  13. postgresql_access-2.0/src/tests/grouptest.py +37 -0
  14. postgresql_access-1.0/PKG-INFO +0 -18
  15. postgresql_access-1.0/README.md +0 -5
  16. postgresql_access-1.0/pyproject.toml +0 -20
  17. postgresql_access-1.0/setup.cfg +0 -19
  18. postgresql_access-1.0/src/postgresql_access/__init__.py +0 -7
  19. postgresql_access-1.0/src/postgresql_access/database.py +0 -328
  20. postgresql_access-1.0/src/postgresql_access.egg-info/PKG-INFO +0 -18
  21. postgresql_access-1.0/src/postgresql_access.egg-info/requires.txt +0 -2
  22. {postgresql_access-1.0 → postgresql_access-2.0}/LICENSE +0 -0
  23. {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access/certificatedatabase.py +0 -0
  24. {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access/main.py +0 -0
  25. {postgresql_access-1.0 → postgresql_access-2.0}/src/postgresql_access.egg-info/dependency_links.txt +0 -0
  26. {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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
- setup.cfg
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,10 @@
1
+ keyring
2
+
3
+ [alchemy]
4
+ sqlalchemy
5
+
6
+ [psycopg2]
7
+ psycopg2-binary
8
+
9
+ [psycopg3]
10
+ psycopg[binary]
@@ -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()
@@ -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.
@@ -1,5 +0,0 @@
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.
@@ -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
-
@@ -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,7 +0,0 @@
1
-
2
- import importlib.metadata
3
- import logging
4
- postgresql_access_logger = logging.getLogger(__name__)
5
-
6
- __version__ = importlib.metadata.version('postgresql_access')
7
-
@@ -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.
@@ -1,2 +0,0 @@
1
- psycopg2-binary
2
- keyring
File without changes