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/__init__.py +7 -0
- feffdb/apps.py +77 -0
- feffdb/feffdat.db +0 -0
- feffdb/feffdb.py +318 -0
- feffdb/feffpath.py +559 -0
- feffdb/schema.py +80 -0
- feffdb/simpledb.py +373 -0
- feffdb/utils.py +359 -0
- feffdb/version.py +24 -0
- feffdb-0.1.dist-info/METADATA +62 -0
- feffdb-0.1.dist-info/RECORD +15 -0
- feffdb-0.1.dist-info/WHEEL +5 -0
- feffdb-0.1.dist-info/entry_points.txt +2 -0
- feffdb-0.1.dist-info/licenses/LICENSE +28 -0
- feffdb-0.1.dist-info/top_level.txt +1 -0
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)
|