feffdb 0.1__py3-none-any.whl

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.
feffdb/simpledb.py ADDED
@@ -0,0 +1,373 @@
1
+ #!/usr/bin/env python
2
+ """
3
+ Simple database interface using SQLAlchemy 2.0
4
+ """
5
+
6
+ import logging
7
+ import atexit
8
+ from datetime import datetime
9
+
10
+ from sqlalchemy import MetaData, create_engine, and_
11
+ from sqlalchemy.orm import Session
12
+ from sqlalchemy.sql.sqltypes import INTEGER
13
+
14
+
15
+ def isotime(dtime=None, sep=' '):
16
+ if dtime is None:
17
+ dtime = datetime.now()
18
+ return datetime.isoformat(dtime, sep=sep)
19
+
20
+ def isSimpleDB(dbname, required_tables=('info',)):
21
+ """test if a file is a valid sqlite3 SimpleDB file
22
+ with tables listed in required_tables,
23
+
24
+ All DBs must have an 'info' table with columns 'key' and 'value'
25
+ """
26
+ valid = False
27
+ try:
28
+ engine = create_engine('sqlite:///%s' % dbname)
29
+ metadata = MetaData()
30
+ with engine.connect() as conn:
31
+ metadata.reflect(conn)
32
+ valid = 'info' in metadata.tables
33
+ if valid:
34
+ for tab_name in required_tables:
35
+ valid = valid and tab_name in metadata.tables
36
+ info_keys = metadata.tables['info'].columns.keys()
37
+ for colname in ('key', 'value'):
38
+ valid = valid and colname in info_keys
39
+
40
+ except:
41
+ return False
42
+ finally:
43
+ if engine is not None:
44
+ engine.dispose()
45
+
46
+ return valid
47
+
48
+ class SimpleDB(object):
49
+ """simple, common interface to Postgres/SQLite3 databases
50
+
51
+ Intended as mixin class, with attributes:
52
+
53
+ .dbnname
54
+ .engine
55
+ .metadata
56
+ .tables
57
+ .logfile
58
+
59
+ and methods:
60
+
61
+ connect(dbname, serve, user, password, port, host)
62
+ close()
63
+ execute()
64
+ set_info()
65
+ get_rows()
66
+
67
+ """
68
+ def __init__(self, dbname=None, server='sqlite', user='',
69
+ password='', host='', port=5432, dialect=None, logfile=None,
70
+ read_only=False):
71
+ self.engine = None
72
+ self.metadata = None
73
+ self.logfile = logfile
74
+ if dbname is not None:
75
+ self.connect(dbname, server=server, user=user,
76
+ password=password, port=port,
77
+ host=host, dialect=dialect, read_only=read_only)
78
+
79
+ def connect(self, dbname, server='sqlite', user='',
80
+ password='', port=None, host='localhost', dialect=None,
81
+ read_only=False):
82
+ "connect to an existing database"
83
+
84
+ self.dbname = dbname
85
+ if port not in (None, 'None', ''):
86
+ try:
87
+ port = int(port)
88
+ except:
89
+ pass
90
+ connect_args = {}
91
+ if server.startswith('post') or server.startswith('pg'):
92
+ server ='postgresql'
93
+ if port is None:
94
+ port = 5432
95
+ connect_str= f'{user}:{password}@{host}:{port:d}/{dbname}'
96
+ elif server.startswith('my'):
97
+ server = 'mysql'
98
+ if port is None:
99
+ port = 3306
100
+ connect_str= f'{user}:{password}@{host}:{port:d}/{dbname}'
101
+ else:
102
+ server = 'sqlite'
103
+ connect_str = f'/{dbname}'
104
+ connect_args = {'check_same_thread': False}
105
+ atexit.register(self.close)
106
+
107
+ if dialect is None:
108
+ connect_str = f'{server}://{connect_str}'
109
+ else:
110
+ connect_str = f'{server}+{dialect}://{connect_str}'
111
+
112
+ opts = {'connect_args': connect_args}
113
+ if read_only:
114
+ opts['execution_options'] = {"isolation_level": "AUTOCOMMIT"}
115
+
116
+ self.engine = create_engine(connect_str, **opts)
117
+ self.metadata = MetaData()
118
+ try:
119
+ self.metadata.reflect(bind=self.engine)
120
+ except:
121
+ raise ValueError(f'{dbname:s} is not a valid database')
122
+
123
+ self.tables = self.metadata.tables
124
+
125
+ if self.logfile is not None:
126
+ logging.basicConfig()
127
+ logger = logging.getLogger('sqlalchemy.engine')
128
+ logger.addHandler(logging.FileHandler(self.logfile))
129
+
130
+ def get_session(self):
131
+ return Session(self.engine)
132
+
133
+ def close(self):
134
+ "close session"
135
+ self.engine.dispose()
136
+ with Session(self.engine) as session, session.begin():
137
+ session.flush()
138
+
139
+ def execute(self, query, set_modify_time=False):
140
+ """
141
+ general execute of query, optionally setting 'modify date'
142
+ and committing
143
+ """
144
+ result = None
145
+ with Session(self.engine) as session, session.begin():
146
+ result = session.execute(query)
147
+ if set_modify_time:
148
+ q = self.set_info('modify_time', isotime(), do_execute=False)
149
+ if q is not None:
150
+ session.execute(q)
151
+ session.flush()
152
+ return result
153
+
154
+ def set_info(self, key, value, with_modify_time=True, do_execute=True):
155
+ """set key / value in the info table
156
+
157
+ use do_execute=False to avoid executing, and return the query
158
+ """
159
+ tab = self.tables['info']
160
+ val = self.get_rows('info', where={'key': key}, none_if_empty=True)
161
+ ivals = {'value': value}
162
+ if with_modify_time and 'modify_time' in tab.c:
163
+ ivals['modify_time'] = isotime()
164
+ if val is None:
165
+ ivals['key'] = key
166
+ query = tab.insert().values(**ivals)
167
+ else:
168
+ query = tab.update().where(tab.c.key==key).values(**ivals)
169
+ if do_execute:
170
+ self.execute(query, set_modify_time=True)
171
+ return
172
+ return query
173
+
174
+ def get_info(self, key=None, default=None, prefix=None, as_int=False,
175
+ as_bool=False, order_by='modify_time', full_row=False):
176
+ where = {}
177
+ if key is not None:
178
+ where['key'] = key
179
+ gi_kws = {}
180
+ if order_by in self.tables['info'].c:
181
+ gi_kws['order_by'] = order_by
182
+ allrows = self.get_rows('info', where, **gi_kws)
183
+
184
+ def cast(val, as_int, as_bool):
185
+ if (as_int or as_bool):
186
+ if val is None:
187
+ val = 0
188
+ try:
189
+ val = int(float(val))
190
+ if as_bool:
191
+ val = bool(val)
192
+ except (ValueError, TypeError):
193
+ pass
194
+ return val
195
+
196
+ if prefix is None:
197
+ if len(allrows) == 1:
198
+ xout = allrows[0]
199
+ if not full_row:
200
+ xout = xout.value
201
+ out = cast(xout, as_int, as_bool)
202
+ else:
203
+ out = {}
204
+ for row in allrows:
205
+ xout = row
206
+ if not full_row:
207
+ xout = xout.value
208
+ out[row.key] = cast(xout, as_int, as_bool)
209
+ if len(out) == 0 and (default is not None or
210
+ as_int is not None or
211
+ as_bool is not None):
212
+ if default is None:
213
+ default = 0
214
+ out = cast(default, as_int, as_bool)
215
+ else:
216
+ out = {}
217
+ for row in allrows:
218
+ if row.key.startswith(prefix):
219
+ xout = row
220
+ if not full_row:
221
+ xout = xout.value
222
+ out[row.key] = cast(xout, as_int, as_bool)
223
+
224
+ return out
225
+
226
+ def set_modify_time(self):
227
+ """set modify_time in info table"""
228
+ self.set_info('modify_time', isotime(), do_execute=True)
229
+
230
+ def add_row(self, tablename, **kws):
231
+ """add row to a table with keyword/value pairs == insert()"""
232
+ return self.insert(tablename, **kws)
233
+
234
+ def insert(self, tablename, **kws):
235
+ """insert to a table with keyword/value pairs"""
236
+ tab = self.tables[tablename]
237
+ return self.execute(tab.insert().values(**kws), set_modify_time=True)
238
+
239
+ def table_error(self, message, tablename, funcname):
240
+ raise ValueError(f"{message} for table '{tablename}' in {funcname}()")
241
+
242
+ def handle_where(self, tablename, where=None, funcname=None, **kws):
243
+ if funcname is None:
244
+ funcname = 'handle_where'
245
+ tab = self.tables.get(tablename, None)
246
+ if tab is None:
247
+ self.table_error("no table found", tablename, funcname)
248
+
249
+ filters = []
250
+ if where is None or isinstance(where, bool) and where:
251
+ where = {}
252
+ if len(kws) == 0:
253
+ filters.append(True)
254
+
255
+ if isinstance(where, int):
256
+ if 'id' in tab.c:
257
+ filters.append(tab.c.id==where)
258
+ print(" .(id). ", where)
259
+ else:
260
+ for colname, coldat in tab.columns.items():
261
+ if coldat.primary_key and isinstance(coldat.type, INTEGER):
262
+ filters.append(getattr(tab.c, colname)==where)
263
+ if len(filters) == 0:
264
+ self.table_error("could not interpret integer `where` value",
265
+ tablename, funcname)
266
+ elif isinstance(where, dict):
267
+ where.update(kws)
268
+ for keyname, val in where.items():
269
+ key = getattr(tab.c, keyname, None)
270
+ if key is None:
271
+ key = getattr(tab.c, "%s_id" % keyname, None)
272
+ if key is None:
273
+ self.table_error(f"no column '{keyname}'", tablename, funcname)
274
+ filters.append(key==val)
275
+
276
+ return and_(*filters)
277
+
278
+ def get_rows(self, tablename, where=None, order_by=None, limit_one=False,
279
+ none_if_empty=False, **kws):
280
+ """general-purpose select of row data:
281
+
282
+ Arguments
283
+ ----------
284
+ tablename name of table
285
+ where dict of key/value pairs for where clause [None]
286
+ order_by name of column to order by [None]
287
+ limit_one whether to limit result to 1 row [False[
288
+ none_if_empty whether to return None for an empty row [False]
289
+ kwargs other keyword/value pairs are included in the `where` dictionary
290
+ Returns
291
+ -------
292
+ rows matching `where` (all if `where=None`) optionally ordered by order_by
293
+
294
+ Examples
295
+ --------
296
+ >>> db.get_rows('element', where{'z': 30})
297
+ """
298
+ tab = self.tables.get(tablename, None)
299
+ if tab is None:
300
+ self.table_error("no table found", tablename, 'get_rows')
301
+
302
+ where = self.handle_where(tablename, where=where, funcname='get_rows', **kws)
303
+ query = tab.select().where(where)
304
+
305
+ if order_by is None:
306
+ if 'id' in tab.c:
307
+ order_by = 'id'
308
+ elif order_by not in tab.c:
309
+ oid = f'{order_by}_id'
310
+ if oid in tab.c:
311
+ order_by = oid
312
+ else:
313
+ order_by = None
314
+
315
+ if order_by is not None:
316
+ query = query.order_by(order_by)
317
+
318
+ result = self.execute(query)
319
+ if limit_one:
320
+ result = result.fetchone()
321
+ else:
322
+ result = result.fetchall()
323
+
324
+ if result is not None and len(result) == 0 and none_if_empty:
325
+ result = None
326
+ return result
327
+
328
+ def lookup(self, tablename, **kws):
329
+ """
330
+ simple select of table with any equality filter on columns by name
331
+
332
+ a simple wrapper for
333
+ self.get_rows(tablename, limit_one=False, none_if_empty=False, **kws)
334
+
335
+ """
336
+ return self.get_rows(tablename, limit_one=False, none_if_empty=False, **kws)
337
+
338
+ def update(self, tablename, where=None, **kws):
339
+ """update a row (with where in a table
340
+ using keyword args
341
+
342
+ Arguments
343
+ ----------
344
+ tablename name of table
345
+ where select row to update, either int for id or dict for key/val
346
+
347
+ kws values to update
348
+
349
+
350
+ """
351
+ tab = self.tables.get(tablename, None)
352
+ if tab is None:
353
+ self.table_error("no table found", tablename, 'update')
354
+
355
+ where = self.handle_where(tablename, where=where, funcname='update')
356
+ self.execute(tab.update().where(where).values(**kws), set_modify_time=True)
357
+
358
+
359
+ def delete_rows(self, tablename, where):
360
+ """delete rows from table
361
+
362
+ Arguments
363
+ ----------
364
+ tablename name of table
365
+ where rows to delete, either int for id or dict for key/val
366
+ """
367
+
368
+ tab = self.tables.get(tablename, None)
369
+ if tab is None:
370
+ self.table_error("no table found", tablename, 'delete')
371
+
372
+ where = self.handle_where(tablename, where=where, funcname='delete')
373
+ self.execute(tab.delete().where(where), set_modify_time=True)