PaferaPy Async 0.1
ASGI framework focused on simplicity and efficiency
All Classes Namespaces Files Functions Variables
modelbase.py
Go to the documentation of this file.
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4import os
5import datetime
6import hashlib
7import json
8import base64
9
10from pprint import pprint
11
12import dateutil.parser
13
14import pafera.db
15
16from pafera.types import *
17from pafera.utils import *
18
19# *********************************************************************
20class ModelBase(object):
21 """Base class for all database models. Derived classes should add
22
23 _dbfields List of all model fields
24 _dbindexes List of indexes to add to the database
25 _dblinks List of linked objects for the database model manager
26 _dbdisplay List of default fields to display for the database
27 model manager
28 _dbflags Enable security, tracking changes, and tracking changed
29 values
30 """
31
32 SECURE_FIELDS = [
33 'dbowner',
34 'dbaccess',
35 'dbgroup',
36 'dbgroupaccess',
37 'dbaclid',
38 ]
39
40 # -------------------------------------------------------------------
41 def __init__(self):
42 """Initialize all fields at creation like a good programmer should
43 """
44 for k, v in self._dbfields.items():
45 if v[0].find('PRIMARY KEY') != -1:
46 setattr(self, k, None)
47 elif v[0].find('INT') != -1:
48 setattr(self, k, 0)
49 elif v[0].find('FLOAT') != -1:
50 setattr(self, k, 0.0)
51 elif v[0] == 'DATETIME':
52 setattr(self, k, datetime.datetime.now())
53 else:
54 setattr(self, k, '')
55
56 self._changed = {}
57
58 # -------------------------------------------------------------------
59 def __hash__(self):
60 """We take the easy route where the hash for an object is just its
61 database ID.
62 """
63 return getattr(self, self._dbid)
64
65 # -------------------------------------------------------------------
66 def __eq__(self, o):
67 """Two objects are equivalent if their models are the same and their
68 IDs are the same.
69 """
70 return (self.__class__ == o.__class__) and (self.id == o.id) and (self.id and o.id)
71
72 # -------------------------------------------------------------------
73 def __cmp__(self, o):
74 """Comparing means simply subracting the database ids. Of course,
75 this only works if you use integer IDs.
76 """
77 return getattr(self, self._dbid) - getattr(o, self._dbid)
78
79 # -------------------------------------------------------------------
80 def __str__(self):
81 """Simply calls the toJSON() function.
82 """
83 return str(self.ToJSON())
84
85 # -------------------------------------------------------------------
86 def __repr__(self):
87 """Simply outputs the toJSON() function with the model name.
88 """
89 return str(self.__class__) + '(' + str(self.ToJSON()) + ')'
90
91 # -------------------------------------------------------------------
92 def HasSameValues(self, o):
93 """While operator = only checks class and ids, this checks every field
94 to see if two objects or an object and a dict have the same values.
95 Handy for implementing copy-on-change systems
96 """
97 if isinstance(o, dict):
98 for k, v in self._dbfields.items():
99 if getattr(self, k, None) != o.get(k, None):
100 return 0
101 else:
102 for k, v in self._dbfields.items():
103 if getattr(self, k, None) != getattr(o, k, None):
104 return 0
105
106 return 1
107
108 # -------------------------------------------------------------------
109 def _HashPassword(self, password, salt):
110 """Default password function. Override if you need more security.
111 """
112 return base64.b64encode(hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100000)).decode('ascii')
113
114 # ------------------------------------------------------------------
115 def Set(self, **kwargs):
116 """We use this method instead of direct attribute access in order to
117 keep track of what values have been changed. Only updated values
118 are saved to the database, increasing efficiency and output.
119
120 The special field _updatechanged allows initialization of values
121 without triggering changed fields. It's used by the library itself
122 to turn database rows into initialized objects. Set it to 0 if you
123 want to disable tracking, otherwise the default behavior will be
124 changed.
125 """
126 _updatechanged = kwargs.get('_updatechanged')
127
128 if _updatechanged == None:
129 _updatechanged = 1
130
131 for k, v in self._dbfields.items():
132 if k not in kwargs:
133 continue
134
135 value = kwargs[k]
136
137 # Check if field has a validator
138 if len(v) > 2:
139 v[2](k, value)
140
141 if _updatechanged:
142 self._changed[k] = getattr(self, k)
143
144 if v[0].find('INT') != -1:
145 setattr(self, k, int(value if value else 0))
146 elif v[0].find('FLOAT') != -1:
147 setattr(self, k, float(value if value else 0))
148 elif v[0].find('NEWLINELIST') != -1:
149 if value and not isinstance(value, list):
150 value = [x.strip() for x in filter(None, value.split('\n'))]
151
152 setattr(self, k, value if value else [])
153 elif (v[0] == 'DICT' or v[0] == 'TRANSLATION') and isinstance(value, str):
154 try:
155 setattr(self, k, json.loads(value))
156 except Exception as e:
157 setattr(self, k, {})
158 elif v[0] == 'LIST' and isinstance(value, str):
159 try:
160 setattr(self, k, json.loads(value))
161 except Exception as e:
162 setattr(self, k, [])
163 elif v[0] == 'DATETIME':
164 if isinstance(value, datetime.datetime):
165 setattr(self, k, value)
166 elif isinstance(value, int) or isinstance(value, float):
167 setattr(self, k, datetime.datetime.fromtimestamp(value))
168 else:
169 setattr(self, k, dateutil.parser.parse(value))
170 else:
171 setattr(self, k, value)
172
173 if self._dbflags & pafera.db.DB_SECURE:
174 for r in self.SECURE_FIELDS:
175 if r not in kwargs:
176 continue
177
178 v = fields[r]
179
180 if getattr(self, r, None) is not v and updatechanged:
181 self._changed[r] = getattr(self, r)
182
183 setattr(self, r, fields[r])
184
185 # ------------------------------------------------------------------
186 def UpdateFields(self, fieldnames):
187 """This is a convenience method to update fields without going
188 through the Set() method. Useful for working on mutable
189 collections like lists and dicts.
190
191 fieldnames should be a list of fields separated by a comma and
192 space as in 'id, name, business'
193 """
194 fieldnames = fieldnames.split(', ')
195
196 for r in fieldnames:
197 self._changed[r] = getattr(self, r)
198
199 # ------------------------------------------------------------------
200 def OffsetTime(self, timeoffset):
201 """Change all datetime fields to the new time offset. Useful for
202 switching between local and GMT times.
203 """
204 for k, v in self._dbfields.items():
205 if v[0] == 'DATETIME':
206 d = getattr(self, k, None)
207
208 if d:
209 self.d = datetime.datetime.fromtimestamp(d.timestamp() + timeoffset)
210
211 # ------------------------------------------------------------------
212 def SetPassword(self, field, password):
213 """This special function hashes the password before saving the field.
214 You should explicitly call this to avoid situations where you're
215 hashing an existing hash instead of the raw password itself.
216 """
217 salt = os.urandom(32)
218 key = self._HashPassword(password, salt)
219
220 setattr(self, field, base64.b64encode(salt).decode('ascii') + '' + key)
221 self._changed[field] = ''
222
223 # ------------------------------------------------------------------
224 def CheckPassword(self, field, password):
225 """This special function checks to see if the password matches the
226 hash stored in the field.
227 """
228 oldpassword = getattr(self, field)
229
230 salt, hashed, empty = oldpassword.split('=')
231
232 salt += '='
233 salt = base64.b64decode(salt.encode('ascii'))
234 hashed += '='
235
236 return self._HashPassword(
237 password,
238 salt
239 ) == hashed
240
241 # ------------------------------------------------------------------
242 def ToJSON(self, fields = ''):
243 """Converts this object into a format suitable for inclusion in JSON.
244 """
245 values = {}
246
247 if fields:
248 fields = fields.split(', ')
249
250 for k, v in self._dbfields.items():
251 if fields and k not in fields:
252 continue
253
254 if k == 'rid':
255 rid = getattr(self, k)
256 values['idcode'] = ToShortCode(rid) if rid else ''
257
258 if 'PASSWORD' in v[0]:
259 values[k] = ''
260 elif 'DATETIME' in v[0]:
261 values[k] = getattr(self, k).isoformat()
262 else:
263 values[k] = getattr(self, k)
264
265 return values
266
267 # ------------------------------------------------------------------
268 def SetOwner(self, ownerid):
269 """Special security functions that are useful only if you have enabled
270 security in _dbflags for the model.
271 """
272 self.dbowner = ownerid
273
274 # ------------------------------------------------------------------
275 def SetGroup(self, groupid):
276 """Special security functions that are useful only if you have enabled
277 security in _dbflags for the model.
278 """
279 self.dbgroup = groupid
280
281 # ------------------------------------------------------------------
282 def SetAccess(self, access):
283 """Special security functions that are useful only if you have enabled
284 security in _dbflags for the model.
285 """
286 self.dbaccess = access
287
288 # ------------------------------------------------------------------
289 def GetACL(self, db):
290 """Since we store ACLs as a database lookup, be sure to use these getters
291 and setters if you plan on using ACLs.
292 """
293 if not self.dbaclid:
294 return {}
295
296 if self.dbacl:
297 return self.dbacl
298
299 self.dbacl = db.GetACL(self.dbaclid)
300
301 return self.dbacl
302
303 # ------------------------------------------------------------------
304 def SetACL(self, db, acl):
305 """ACLs in Pafera are defined as a set of rules similar to cascading
306 style sheets. The default access for the model comes first, then
307 is overridden by group access, then overridden by user access.
308
309 Note that for group access, if *one* group has access, then the
310 values for the other groups are ignored.
311 """
312 if not acl or (not ArrayV(acl, 'users') and not ArrayV(acl, 'groups')):
313 self.dbaclid = 0
314 self.dbacl = {
315 'users': {},
316 'groups': {},
317 }
318 return
319
320 self.dbacl = acl
321
322 acljson = json.dumps(acl, sort_keys = 1)
323
324 r = db.Execute('SELECT id FROM system_acl WHERE rules = ?', acljson)
325
326 if r:
327 self.dbaclid = r[0]
328 else:
329 self.dbaclid = self.Execute('INSERT INTO system_acl(rules) VALUES(?)', acljson)
330
331# *********************************************************************
332class DBList(object):
333 """Iterator class for database access that supports automatic chunking.
334 Using this class, you can examine billions of rows of data without
335 worrying about memory usage."""
336
337 # -------------------------------------------------------------------
338 def __init__(self, db, model, **kwargs):
339 """Same parameters as db.Find(), mainly because that function only
340 sets up the parameters and lets this class do the heavy lifting.
341 """
342 super(DBList, self).__init__()
343
344 params = kwargs.get('params')
345
346 if not isinstance(params, list):
347 if params != None:
348 params = [params]
349 else:
350 params = []
351
352 self.db = db
353 self.model = model
354 self.cond = StrV(kwargs, 'cond')
355 self.params = params
356 self.orderby = StrV(kwargs, 'orderby')
357 self.fields = StrV(kwargs, 'fields', '*')
358 self.start = IntV(kwargs, 'start', 0, 2147483647, 0)
359 self.limit = IntV(kwargs, 'limit', 1, 2147483647, 100)
360 self.randompos = 0
361 self.pos = -1
362 self.length = 0
363 self.cachepos = 0
364 self.cache = []
365 self.randomlist = []
366
367 self.enablesecurity = self.model._dbflags & pafera.db.DB_SECURE
368
369 # -------------------------------------------------------------------
370 def __iter__(self):
371 """convenience functions to allow use in iterator loops.
372 """
373 self.pos = -1
374 return self
375
376 # -------------------------------------------------------------------
377 def __next__(self):
378 """convenience functions to allow use in iterator loops.
379 """
380 if self.pos == -1:
381 self.Count()
382 self.pos = self.start - 1
383
384 if not self.length:
385 raise StopIteration
386
387 if self.pos < self.length - 1 and self.pos < (self.start + self.limit - 1):
388 self.pos += 1
389 else:
390 raise StopIteration
391
392 return self[self.pos]
393
394 # -------------------------------------------------------------------
395 def __bool__(self):
396 """Conversion function to check if the dataset is empty.
397 """
398 if self.pos == -1:
399 self.Count()
400
401 return self.length != 0
402
403 # -------------------------------------------------------------------
404 def __repr__(self):
405 """Returns the parameters for this iterator. Handy for debugging.
406 """
407 if self.pos == -1:
408 self.Count()
409
410 return f"""DBList({self.model})
411 Filter: {self.cond}
412 Params: {self.params}
413 Size: {self.length}
414"""
415
416 # -------------------------------------------------------------------
417 def Count(self):
418 """Returns the length of the underlying dataset.
419 """
420 if self.cond:
421 if self.params:
422 r = self.db.Execute(f"SELECT COUNT(*) FROM {self.model._dbtable} {self.cond}", self.params)[0]
423 else:
424 r = self.db.Execute(f"SELECT COUNT(*) FROM {self.model._dbtable} {self.cond}")[0]
425 else:
426 r = self.db.Execute(f"SELECT COUNT(*) FROM {self.model._dbtable}")[0]
427
428 if r:
429 self.length = int(r[0])
430
431 return self.length
432
433 # -------------------------------------------------------------------
434 def Filter(self, cond, params):
435 """Resets the condition and parameters for the query used. It saves a
436 little bit of time as opposed to creating a new object.
437 """
438 self.cond = cond
439 self.params = params
440 self.cache = []
441 self.pos = -1
442 self.cachepos = 0
443 return self
444
445 # -------------------------------------------------------------------
446 def OrderBy(self, order):
447 """Resets the order for the query used. It saves a
448 little bit of time as opposed to creating a new object.
449 """
450 self.orderby = order
451 self.cache = []
452 self.pos = -1
453 self.cachepos = 0
454 return self
455
456 # -------------------------------------------------------------------
457 def __len__(self):
458 """Total number of rows for the current query.
459 """
460 if not self.cache:
461 self.Count()
462
463 return self.length
464
465 # -------------------------------------------------------------------
466 def __getitem__(self, pos):
467 """Returns the item at pos, retrieving it from the database if
468 necessary.
469 """
470 if self.randomlist:
471 self.randompos = self.randomlist[pos]
472 self._RefreshCache(self.randompos)
473 return self.cache[0]
474
475 if (not self.cache
476 or pos < self.cachepos
477 or pos > self.cachepos + len(self.cache) - 1
478 ):
479 self._RefreshCache(pos)
480
481 return self.cache[pos - self.cachepos]
482
483 # -------------------------------------------------------------------
484 def __delitem__(self, pos):
485 """Deletes the item from the database. Note that this will alter
486 the length of the dataset.
487 """
488 if self.randomlist:
489 self.randompos = self.randomlist[pos]
490 self._RefreshCache(self.randompos)
491 self.db.Delete(self.cache[self.randompos - self.cachepos])
492 del self.cache[self.randompos - self.cachepos]
493 self.length -= 1
494 return self
495
496 if not self.cache or pos < self.cachepos or pos > self.cachepos + len(self.cache) - 1:
497 self._RefreshCache(pos)
498
499 self.db.Delete(self.cache[pos - self.cachepos])
500 del self.cache[pos - self.cachepos]
501 self.length -= 1
502 return self
503
504 # -------------------------------------------------------------------
505 def _RefreshCache(self, pos):
506 """Fetch new data from the database. You should not need to call this
507 directly as __getitem__() automatically does it for you.
508 """
509 query = f"SELECT {self.fields} FROM {self.model._dbtable} "
510
511 if self.cond:
512 query = query + self.cond
513
514 if self.orderby:
515 query = query + ' ORDER BY ' + self.orderby
516
517 if ' LIMIT ' not in query:
518 if self.randomlist:
519 query = query + f" LIMIT {pos}, 1"
520 else:
521 query = query + f" LIMIT {pos}, {self.limit}"
522
523 self.cache = []
524
525 for r in self.db.Query(query, self.params):
526 if isinstance(self.model, pafera.modelbase.ModelBase):
527 o = self.model.__class__()
528 else:
529 o = self.model()
530
531 r['_updatechanged'] = 0
532
533 o.Set(**r)
534
535 if self.enablesecurity:
536 access = self.db.GetAccess(o)
537
538 if not (access & pafera.db.DB_CAN_VIEW):
539 continue
540
541 self.cache.append(o)
542
543 self.cachepos = pos
544
545 # -------------------------------------------------------------------
546 def SetRandom(self, active):
547 """Enables random ordering for the returned elements. Note that this
548 will cause severe database activity if you use it on a dataset that
549 won't fit in memory, so is a convenience for small datasets in
550 local memory more than anything else.
551 """
552 if active:
553 self.randomlist = list(range(0, self.Count()))
554 random.shuffle(self.randomlist)
555 else:
556 self.randomlist = []
557
Iterator class for database access that supports automatic chunking.
Definition: modelbase.py:332
def Count(self)
Returns the length of the underlying dataset.
Definition: modelbase.py:417
def __repr__(self)
Returns the parameters for this iterator.
Definition: modelbase.py:404
def __init__(self, db, model, **kwargs)
Same parameters as db.Find(), mainly because that function only sets up the parameters and lets this ...
Definition: modelbase.py:338
def __getitem__(self, pos)
Returns the item at pos, retrieving it from the database if necessary.
Definition: modelbase.py:466
def __len__(self)
Total number of rows for the current query.
Definition: modelbase.py:457
def __bool__(self)
Conversion function to check if the dataset is empty.
Definition: modelbase.py:395
def __next__(self)
convenience functions to allow use in iterator loops.
Definition: modelbase.py:377
def OrderBy(self, order)
Resets the order for the query used.
Definition: modelbase.py:446
def SetRandom(self, active)
Enables random ordering for the returned elements.
Definition: modelbase.py:546
def Filter(self, cond, params)
Resets the condition and parameters for the query used.
Definition: modelbase.py:434
def __delitem__(self, pos)
Deletes the item from the database.
Definition: modelbase.py:484
def _RefreshCache(self, pos)
Definition: modelbase.py:505
def __iter__(self)
convenience functions to allow use in iterator loops.
Definition: modelbase.py:370
Base class for all database models.
Definition: modelbase.py:20
def HasSameValues(self, o)
While operator = only checks class and ids, this checks every field to see if two objects or an objec...
Definition: modelbase.py:92
def GetACL(self, db)
Since we store ACLs as a database lookup, be sure to use these getters and setters if you plan on usi...
Definition: modelbase.py:289
def SetAccess(self, access)
Special security functions that are useful only if you have enabled security in _dbflags for the mode...
Definition: modelbase.py:282
def __repr__(self)
Simply outputs the toJSON() function with the model name.
Definition: modelbase.py:86
def SetPassword(self, field, password)
This special function hashes the password before saving the field.
Definition: modelbase.py:212
def SetGroup(self, groupid)
Special security functions that are useful only if you have enabled security in _dbflags for the mode...
Definition: modelbase.py:275
def UpdateFields(self, fieldnames)
This is a convenience method to update fields without going through the Set() method.
Definition: modelbase.py:186
def _HashPassword(self, password, salt)
Definition: modelbase.py:109
def __init__(self)
Initialize all fields at creation like a good programmer should.
Definition: modelbase.py:41
def Set(self, **kwargs)
We use this method instead of direct attribute access in order to keep track of what values have been...
Definition: modelbase.py:115
def SetOwner(self, ownerid)
Special security functions that are useful only if you have enabled security in _dbflags for the mode...
Definition: modelbase.py:268
def __eq__(self, o)
Two objects are equivalent if their models are the same and their IDs are the same.
Definition: modelbase.py:66
def OffsetTime(self, timeoffset)
Change all datetime fields to the new time offset.
Definition: modelbase.py:200
def SetACL(self, db, acl)
ACLs in Pafera are defined as a set of rules similar to cascading style sheets.
Definition: modelbase.py:304
def __hash__(self)
We take the easy route where the hash for an object is just its database ID.
Definition: modelbase.py:59
def CheckPassword(self, field, password)
This special function checks to see if the password matches the hash stored in the field.
Definition: modelbase.py:224
def ToJSON(self, fields='')
Converts this object into a format suitable for inclusion in JSON.
Definition: modelbase.py:242
def __cmp__(self, o)
Comparing means simply subracting the database ids.
Definition: modelbase.py:73
def __str__(self)
Simply calls the toJSON() function.
Definition: modelbase.py:80
Definition: db.py:1
def StrV(d, k, default='')
Utility function to get a string from a dict or object given its name.
Definition: types.py:221
def ArrayV(d, k, default=[])
Utility function to get an array from a dict or object given its name.
Definition: types.py:241
def IntV(d, k, min=None, max=None, default=0)
Utility function to get an int from a dict or object given its name.
Definition: types.py:169
def ToShortCode(val, chars='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_')
Turns a 32-bit value into a six character alphanumeric code.
Definition: utils.py:36