tinymongo 1.0.0__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.
tinymongo/tinymongo.py ADDED
@@ -0,0 +1,1007 @@
1
+ """Acts like a Pymongo client to TinyDB"""
2
+ # coding: utf-8
3
+
4
+ from __future__ import absolute_import
5
+
6
+ import copy
7
+ from functools import reduce
8
+ import logging
9
+ import os
10
+ from math import ceil
11
+ from uuid import uuid1
12
+
13
+ from tinydb import Query, TinyDB, where
14
+ from .storage_backends import get_storage_class, storage_extension
15
+ # from .results import InsertOneResult, InsertManyResult, UpdateResult, DeleteResult
16
+ # from .errors import DuplicateKeyError
17
+ from .results import InsertOneResult, InsertManyResult, UpdateResult, DeleteResult
18
+ from .errors import DuplicateKeyError
19
+ try:
20
+ basestring
21
+ except NameError:
22
+ basestring = str
23
+
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def Q(query, key):
29
+ return reduce(
30
+ lambda partial_query, field: partial_query[field], key.split("."), query
31
+ )
32
+
33
+
34
+ class TinyMongoClient(object):
35
+ """Represents the Tiny `db` client"""
36
+
37
+ def __init__(self, foldername=u"tinydb", backend="tinydb", **kwargs):
38
+ """Initialize container folder and choose a storage backend."""
39
+ self._foldername = foldername
40
+ self._backend = backend or "tinydb"
41
+ try:
42
+ os.makedirs(foldername, exist_ok=True)
43
+ except OSError as x:
44
+ logger.info("{}".format(x))
45
+
46
+ @property
47
+ def _storage(self):
48
+ """Return the TinyDB storage class for the configured backend."""
49
+ return get_storage_class(self._backend)
50
+
51
+ def __getitem__(self, key):
52
+ """Gets a new or existing database based in key"""
53
+ return self._get_db(key)
54
+
55
+ def _get_db_path(self, key):
56
+ return os.path.join(self._foldername, key + storage_extension(self._backend))
57
+
58
+ def _get_db(self, key):
59
+ return TinyMongoDatabase(key, self._get_db_path(key), self._storage)
60
+
61
+ def close(self):
62
+ """Do nothing"""
63
+ pass
64
+
65
+ def __getattr__(self, name):
66
+ """Gets a new or existing database based in attribute."""
67
+ if name.startswith("_"):
68
+ raise AttributeError("{} object has no attribute {}".format(type(self).__name__, name))
69
+ return self._get_db(name)
70
+
71
+
72
+ class TinyMongoDatabase(object):
73
+ """Representation of a Pymongo database"""
74
+
75
+ def __init__(self, database, path, storage):
76
+ """Initialize a TinyDB file named as the db name in the given folder."""
77
+ self._path = path
78
+ self._foldername = os.path.dirname(path) or "."
79
+ self._storage = storage
80
+ self.tinydb = TinyDB(path, storage=storage)
81
+
82
+ def _refresh_table(self):
83
+ """Reload the TinyDB database from disk to pick up external writes."""
84
+ try:
85
+ self.tinydb.close()
86
+ except Exception:
87
+ pass
88
+ self.tinydb = TinyDB(self._path, storage=self._storage)
89
+
90
+ def __getattr__(self, name):
91
+ """Gets a new or existing collection"""
92
+ return TinyMongoCollection(name, self)
93
+
94
+ def __getitem__(self, name):
95
+ """Gets a new or existing collection"""
96
+ return TinyMongoCollection(name, self)
97
+
98
+ def collection_names(self):
99
+ """Get a list of all the collection names in this database"""
100
+ return list(self.tinydb.tables())
101
+
102
+
103
+ class TinyMongoCollection(object):
104
+ """
105
+ This class represents a collection and all of the operations that are
106
+ commonly performed on a collection
107
+ """
108
+
109
+ def __init__(self, table, parent=None):
110
+ """
111
+ Initilialize the collection
112
+
113
+ :param table: the table name
114
+ :param parent: the parent db name
115
+ """
116
+ self.tablename = table
117
+ self.table = None
118
+ self.parent = parent
119
+
120
+ def __repr__(self):
121
+ """Return collection name"""
122
+ return self.tablename
123
+
124
+ def __getattr__(self, name):
125
+ """
126
+ If attr is not found return self
127
+ :param name:
128
+ :return:
129
+ """
130
+ # if self.table is None:
131
+ # self.tablename += u"." + name
132
+ if self.table is None:
133
+ self.build_table()
134
+ return self
135
+
136
+ def build_table(self):
137
+ """
138
+ Builds a new tinydb table at the parent database
139
+ :return:
140
+ """
141
+ self.table = self.parent.tinydb.table(self.tablename)
142
+
143
+ def _refresh_table(self):
144
+ """Reload the TinyDB database from disk and reset the table object."""
145
+ self.parent._refresh_table()
146
+ self.table = self.parent.tinydb.table(self.tablename)
147
+
148
+ def _acquire_collection_lock(self):
149
+ lock_path = os.path.join(self.parent._foldername, ".tinymongo.lock")
150
+ try:
151
+ from .parquet_storage import _local_rlocks, _acquire_rlock, portalocker
152
+ import threading
153
+
154
+ rlock = _local_rlocks.setdefault(lock_path, threading.RLock())
155
+ first_acquire = _acquire_rlock(rlock)
156
+ portalocker_lock = None
157
+ if first_acquire and portalocker is not None:
158
+ portalocker_lock = portalocker.Lock(lock_path, timeout=30)
159
+ portalocker_lock.acquire()
160
+ return rlock, portalocker_lock
161
+ except Exception:
162
+ return None, None
163
+
164
+ def _release_collection_lock(self, rlock, portalocker_lock):
165
+ if portalocker_lock is not None:
166
+ try:
167
+ portalocker_lock.release()
168
+ except Exception:
169
+ pass
170
+ if rlock is not None:
171
+ try:
172
+ rlock.release()
173
+ except Exception:
174
+ pass
175
+
176
+ def count(self):
177
+ """
178
+ Counts the documents in the collection.
179
+ :return: Integer representing the number of documents in the collection.
180
+ """
181
+ return self.find().count()
182
+
183
+ def count_documents(self, filter=None):
184
+ """
185
+ Counts the documents in the collection.
186
+ :return: Integer representing the number of documents in the collection.
187
+ """
188
+ return self.find(filter).count()
189
+
190
+ def drop(self, **kwargs):
191
+ """
192
+ Removes a collection from the database.
193
+ **kwargs only because of the optional "writeConcern" field, but does nothing in the TinyDB database.
194
+ :return: Returns True when successfully drops a collection. Returns False when collection to drop does not
195
+ exist.
196
+ """
197
+ if self.table:
198
+ self.parent.tinydb.drop_table(self.tablename)
199
+ return True
200
+ # from tinydb.database import TinyDB
201
+ # TinyDB().
202
+ else:
203
+ return False
204
+
205
+ def insert(self, docs, *args, **kwargs):
206
+ """Backwards compatibility with insert"""
207
+ if isinstance(docs, list):
208
+ return self.insert_many(docs, *args, **kwargs)
209
+ else:
210
+ return self.insert_one(docs, *args, **kwargs)
211
+
212
+ def insert_one(self, doc, *args, **kwargs):
213
+ """
214
+ Inserts one document into the collection
215
+ If contains '_id' key it is used, else it is generated.
216
+ :param doc: the document
217
+ :return: InsertOneResult
218
+ """
219
+ if self.table is None:
220
+ self.build_table()
221
+
222
+ rlock, portalocker_lock = self._acquire_collection_lock()
223
+ try:
224
+ self._refresh_table()
225
+ if not isinstance(doc, dict):
226
+ raise ValueError(u'"doc" must be a dict')
227
+
228
+ # Respect explicit falsy ids (0, False) — only generate when _id
229
+ # is missing or None.
230
+ if "_id" in doc and doc["_id"] is not None:
231
+ _id = doc[u"_id"] = doc["_id"]
232
+ else:
233
+ _id = doc[u"_id"] = generate_id()
234
+
235
+ bypass_document_validation = kwargs.get("bypass_document_validation")
236
+ if bypass_document_validation is True:
237
+ # insert doc without validation of duplicated `_id`
238
+ eid = self.table.insert(doc)
239
+ else:
240
+ existing = self.find_one({"_id": _id})
241
+ if existing is None:
242
+ eid = self.table.insert(doc)
243
+ else:
244
+ raise DuplicateKeyError(
245
+ u"_id:{0} already exists in collection:{1}".format(
246
+ _id, self.tablename
247
+ )
248
+ )
249
+
250
+ return InsertOneResult(eid=eid, inserted_id=_id)
251
+ finally:
252
+ self._release_collection_lock(rlock, portalocker_lock)
253
+
254
+ def insert_many(self, docs, *args, **kwargs):
255
+ """
256
+ Inserts several documents into the collection
257
+ :param docs: a list of documents
258
+ :return: InsertManyResult
259
+ """
260
+ if self.table is None:
261
+ self.build_table()
262
+
263
+ rlock, portalocker_lock = self._acquire_collection_lock()
264
+ try:
265
+ self._refresh_table()
266
+ if not isinstance(docs, list):
267
+ raise ValueError(u'"insert_many" requires a list input')
268
+
269
+ bypass_document_validation = kwargs.get("bypass_document_validation")
270
+ if bypass_document_validation is not True:
271
+ # get all _id in once, to reduce I/O. (without projection)
272
+ existing = [doc["_id"] for doc in self.find({})]
273
+
274
+ _ids = list()
275
+ for doc in docs:
276
+
277
+ # Respect explicit falsy ids (0, False) — only generate when
278
+ # _id is missing or None.
279
+ if "_id" in doc and doc["_id"] is not None:
280
+ _id = doc[u"_id"] = doc["_id"]
281
+ else:
282
+ _id = doc[u"_id"] = generate_id()
283
+
284
+ if bypass_document_validation is not True:
285
+ if _id in existing:
286
+ raise DuplicateKeyError(
287
+ u"_id:{0} already exists in collection:{1}".format(
288
+ _id, self.tablename
289
+ )
290
+ )
291
+ existing.append(_id)
292
+
293
+ _ids.append(_id)
294
+
295
+ results = self.table.insert_multiple(docs)
296
+
297
+ return InsertManyResult(
298
+ eids=[eid for eid in results],
299
+ inserted_ids=[inserted_id for inserted_id in _ids],
300
+ )
301
+ finally:
302
+ self._release_collection_lock(rlock, portalocker_lock)
303
+
304
+ def parse_query(self, query):
305
+ """
306
+ Creates a tinydb Query() object from the query dict
307
+
308
+ :param query: object containing the dictionary representation of the
309
+ query
310
+ :return: composite Query()
311
+ """
312
+ logger.debug(u"query to parse2: {}".format(query))
313
+
314
+ # this should find all records
315
+ if query == {} or query is None:
316
+ return Query()._id != u"-1" # noqa
317
+
318
+ q = None
319
+ # find the final result of the generator
320
+ for c in self.parse_condition(query):
321
+ if q is None:
322
+ q = c
323
+ else:
324
+ q = q & c
325
+
326
+ logger.debug(u"new query item2: {}".format(q))
327
+
328
+ return q
329
+
330
+ def parse_condition(self, query, prev_key=None, last_prev_key=None):
331
+ """
332
+ Creates a recursive generator for parsing some types of Query()
333
+ conditions
334
+
335
+ :param query: Query object
336
+ :param prev_key: The key at the next-higher level
337
+ :return: generator object, the last of which will be the complete
338
+ Query() object containing all conditions
339
+ """
340
+ # use this to determine gt/lt/eq on prev_query
341
+ logger.debug(u"query: {} prev_query: {}".format(query, prev_key))
342
+
343
+ q = Query()
344
+ conditions = None
345
+
346
+ # deal with the {'name': value} case by injecting a previous key
347
+ if not prev_key:
348
+ temp_query = copy.deepcopy(query)
349
+ k, v = temp_query.popitem()
350
+ prev_key = k
351
+
352
+ # deal with the conditions
353
+ for key, value in query.items():
354
+ logger.debug(u"conditions: {} {}".format(key, value))
355
+
356
+ if key == u"$gte":
357
+ conditions = (
358
+ (Q(q, prev_key) >= value)
359
+ if not conditions and prev_key != "$not"
360
+ else (conditions & (Q(q, prev_key) >= value))
361
+ if prev_key != "$not"
362
+ else (q[last_prev_key] < value)
363
+ )
364
+ elif key == u"$gt":
365
+ conditions = (
366
+ (Q(q, prev_key) > value)
367
+ if not conditions and prev_key != "$not"
368
+ else (conditions & (Q(q, prev_key) > value))
369
+ if prev_key != "$not"
370
+ else (q[last_prev_key] <= value)
371
+ )
372
+ elif key == u"$lte":
373
+ conditions = (
374
+ (Q(q, prev_key) <= value)
375
+ if not conditions and prev_key != "$not"
376
+ else (conditions & (Q(q, prev_key) <= value))
377
+ if prev_key != "$not"
378
+ else (q[last_prev_key] > value)
379
+ )
380
+ elif key == u"$lt":
381
+ conditions = (
382
+ (Q(q, prev_key) < value)
383
+ if not conditions and prev_key != "$not"
384
+ else (conditions & (Q(q, prev_key) < value))
385
+ if prev_key != "$not"
386
+ else (q[last_prev_key] >= value)
387
+ )
388
+ elif key == u"$ne":
389
+ conditions = (
390
+ (Q(q, prev_key) != value)
391
+ if not conditions and prev_key != "$not"
392
+ else (conditions & (Q(q, prev_key) != value))
393
+ if prev_key != "$not"
394
+ else (q[last_prev_key] == value)
395
+ )
396
+ elif key == u"$not":
397
+ if not isinstance(value, dict) and not isinstance(value, list):
398
+ conditions = (
399
+ (Q(q, prev_key) != value)
400
+ if not conditions and prev_key != "$not"
401
+ else (conditions & (Q(q, prev_key) != value))
402
+ if prev_key != "$not"
403
+ else (q[last_prev_key] >= value)
404
+ )
405
+ else:
406
+ # let the value's condition be parsed below
407
+ pass
408
+ elif key == u"$regex":
409
+ value = value.replace("\\\\\\", "|||")
410
+ value = value.replace("\\\\", "|||")
411
+ regex = value.replace("\\", "")
412
+ regex = regex.replace("|||", "\\")
413
+ currCond = where(prev_key).matches(regex)
414
+ conditions = currCond if not conditions else (conditions & currCond)
415
+ elif key == u"$nin":
416
+ # Build a conjunctive condition: field != each value
417
+ vals = value if isinstance(value, list) else [value]
418
+ nin_cond = None
419
+ for v in vals:
420
+ term = Q(q, prev_key) != v
421
+ nin_cond = term if nin_cond is None else (nin_cond & term)
422
+ conditions = nin_cond if not conditions else (conditions & nin_cond)
423
+ elif key in ["$and", "$or", "$in", "$all"]:
424
+ pass
425
+ else:
426
+
427
+ # don't want to use the previous key if this is a secondary key
428
+ # (fixes multiple item query that includes $ codes)
429
+ if not isinstance(value, dict) and not isinstance(value, list):
430
+ conditions = (
431
+ ((Q(q, key) == value) | (Q(q, key).any([value])))
432
+ if not conditions
433
+ else (
434
+ conditions
435
+ & ((Q(q, key) == value) | (Q(q, key).any([value])))
436
+ )
437
+ )
438
+ prev_key = key
439
+
440
+ logger.debug(u"c: {}".format(conditions))
441
+ if isinstance(value, dict):
442
+ # yield from self.parse_condition(value, key)
443
+ for parse_condition in self.parse_condition(value, key, prev_key):
444
+ yield parse_condition
445
+ elif isinstance(value, list):
446
+ if key == "$and":
447
+ grouped_conditions = None
448
+ for spec in value:
449
+ for parse_condition in self.parse_condition(spec):
450
+ grouped_conditions = (
451
+ parse_condition
452
+ if not grouped_conditions
453
+ else grouped_conditions & parse_condition
454
+ )
455
+ yield grouped_conditions
456
+ elif key == "$or":
457
+ grouped_conditions = None
458
+ for spec in value:
459
+ for parse_condition in self.parse_condition(spec):
460
+ grouped_conditions = (
461
+ parse_condition
462
+ if not grouped_conditions
463
+ else grouped_conditions | parse_condition
464
+ )
465
+ yield grouped_conditions
466
+ elif key == "$in":
467
+ # use `any` to find with list, before comparing to single string
468
+ grouped_conditions = Q(q, prev_key).any(value)
469
+ for val in value:
470
+ for parse_condition in self.parse_condition({prev_key: val}):
471
+ grouped_conditions = (
472
+ parse_condition
473
+ if not grouped_conditions
474
+ else grouped_conditions | parse_condition
475
+ )
476
+ yield grouped_conditions
477
+ elif key == "$all":
478
+ yield Q(q, prev_key).all(value)
479
+ elif isinstance(key, str) and key.startswith("$"):
480
+ if conditions is not None:
481
+ yield conditions
482
+ continue
483
+ else:
484
+ yield Q(q, prev_key).any([value])
485
+ else:
486
+ yield conditions
487
+
488
+ def update(self, query, doc, *args, **kwargs):
489
+ """Backwards compatibility with update"""
490
+ if isinstance(doc, list):
491
+ return [self.update_one(query, item, *args, **kwargs) for item in doc]
492
+ else:
493
+ return self.update_many(query, doc, *args, **kwargs)
494
+
495
+ def update_one(self, query, doc, *args, **kwargs):
496
+ """
497
+ Updates one element of the collection
498
+
499
+ :param query: dictionary representing the mongo query
500
+ :param doc: dictionary representing the item to be updated
501
+ :return: UpdateResult
502
+ """
503
+ if self.table is None:
504
+ self.build_table()
505
+
506
+ rlock, portalocker_lock = self._acquire_collection_lock()
507
+ try:
508
+ self._refresh_table()
509
+ if u"$set" in doc:
510
+ doc = doc[u"$set"]
511
+
512
+ allcond = self.parse_query(query)
513
+ item = self.table.get(allcond)
514
+
515
+ if item is None:
516
+ return UpdateResult(raw_result=[])
517
+
518
+ if u"_id" not in doc:
519
+ doc[u"_id"] = item[u"_id"]
520
+
521
+ try:
522
+ result = self.table.update(doc, where(u"_id") == item[u"_id"])
523
+ except Exception:
524
+ result = []
525
+
526
+ return UpdateResult(raw_result=result)
527
+ finally:
528
+ self._release_collection_lock(rlock, portalocker_lock)
529
+
530
+ def update_many(self, query, doc, *args, **kwargs):
531
+ """
532
+ Updates all elements matching the query
533
+
534
+ :param query: dictionary representing the mongo query
535
+ :param doc: dictionary or update document
536
+ :return: UpdateResult
537
+ """
538
+ if self.table is None:
539
+ self.build_table()
540
+
541
+ rlock, portalocker_lock = self._acquire_collection_lock()
542
+ try:
543
+ self._refresh_table()
544
+ if u"$set" in doc:
545
+ doc = doc[u"$set"]
546
+
547
+ allcond = self.parse_query(query)
548
+ try:
549
+ result = self.table.update(doc, allcond)
550
+ except Exception:
551
+ result = []
552
+
553
+ return UpdateResult(raw_result=result)
554
+ finally:
555
+ self._release_collection_lock(rlock, portalocker_lock)
556
+
557
+ def replace_one(self, query, replacement, *args, **kwargs):
558
+ """
559
+ Replaces one document matching the query with the replacement document.
560
+ """
561
+ if self.table is None:
562
+ self.build_table()
563
+
564
+ rlock, portalocker_lock = self._acquire_collection_lock()
565
+ try:
566
+ self._refresh_table()
567
+ allcond = self.parse_query(query)
568
+ item = self.table.get(allcond)
569
+ if item is None:
570
+ return UpdateResult(raw_result=[])
571
+
572
+ replacement[u"_id"] = item[u"_id"]
573
+
574
+ try:
575
+ self.table.remove(where(u"_id") == item[u"_id"])
576
+ self.table.insert(replacement)
577
+ result = [item[u"_id"]]
578
+ except Exception:
579
+ result = []
580
+
581
+ return UpdateResult(raw_result=result)
582
+ finally:
583
+ self._release_collection_lock(rlock, portalocker_lock)
584
+
585
+ def find_one_and_update(self, query, update, *args, **kwargs):
586
+ """
587
+ Mimics MongoDB's findOneAndUpdate by returning the document before update.
588
+ """
589
+ if self.table is None:
590
+ self.build_table()
591
+
592
+ allcond = self.parse_query(query)
593
+ item = self.table.get(allcond)
594
+ if item is None:
595
+ return None
596
+
597
+ self.update_one(query, update, *args, **kwargs)
598
+ return item
599
+
600
+ def find(self, _filter=None, sort=None, skip=None, limit=None, *args, **kwargs):
601
+ """
602
+ Finds all matching results
603
+
604
+ :param _filter: dictionary representing the mongo query
605
+ :type _filter: Optional[dict]
606
+ :return: cursor containing the search results
607
+ """
608
+ if self.table is None:
609
+ self.build_table()
610
+
611
+ if _filter is None:
612
+ result = self.table.all()
613
+ else:
614
+ allcond = self.parse_query(_filter)
615
+
616
+ try:
617
+ result = self.table.search(allcond)
618
+ except (AttributeError, TypeError):
619
+ result = []
620
+
621
+ result = TinyMongoCursor(result, sort=sort, skip=skip, limit=limit)
622
+
623
+ return result
624
+
625
+ def find_one(self, _filter=None):
626
+ """
627
+ Finds one matching query element
628
+
629
+ :param query: dictionary representing the mongo query
630
+ :return: the resulting document (if found)
631
+ """
632
+
633
+ if self.table is None:
634
+ self.build_table()
635
+
636
+ allcond = self.parse_query(_filter)
637
+
638
+ return self.table.get(allcond)
639
+
640
+ def remove(self, spec_or_id, multi=True, *args, **kwargs):
641
+ """Backwards compatibility with remove"""
642
+ if multi:
643
+ return self.delete_many(spec_or_id)
644
+ return self.delete_one(spec_or_id)
645
+
646
+ def delete_one(self, query):
647
+ """
648
+ Deletes one document from the collection
649
+
650
+ :param query: dictionary representing the mongo query
651
+ :return: DeleteResult
652
+ """
653
+ item = self.find_one(query)
654
+ result = self.table.remove(where(u"_id") == item[u"_id"])
655
+
656
+ return DeleteResult(raw_result=result)
657
+
658
+ def delete_many(self, query):
659
+ """
660
+ Removes all items matching the mongo query
661
+
662
+ :param query: dictionary representing the mongo query
663
+ :return: DeleteResult
664
+ """
665
+ items = self.find(query)
666
+ result = [self.table.remove(where(u"_id") == item[u"_id"]) for item in items]
667
+
668
+ if query == {}:
669
+ # need to reset TinyDB's index for docs order consistency
670
+ self.table._last_id = 0
671
+
672
+ return DeleteResult(raw_result=result)
673
+
674
+
675
+ class TinyMongoCursor(object):
676
+ """Mongo iterable cursor"""
677
+
678
+ def __init__(self, cursordat, sort=None, skip=None, limit=None):
679
+ """Initialize the mongo iterable cursor with data"""
680
+ self.cursordat = cursordat
681
+ self.cursorpos = -1
682
+
683
+ if len(self.cursordat) == 0:
684
+ self.currentrec = None
685
+ else:
686
+ self.currentrec = self.cursordat[self.cursorpos]
687
+
688
+ if sort:
689
+ self.sort(sort)
690
+
691
+ self.paginate(skip, limit)
692
+
693
+ def __getitem__(self, key):
694
+ """Gets record by index or value by key"""
695
+ if isinstance(key, int):
696
+ return self.cursordat[key]
697
+ return self.currentrec[key]
698
+
699
+ def paginate(self, skip, limit):
700
+ """Paginate list of records"""
701
+ if not self.count() or not limit:
702
+ return
703
+ skip = skip or 0
704
+ pages = int(ceil(self.count() / float(limit)))
705
+ limits = {}
706
+ last = 0
707
+ for i in range(pages):
708
+ current = limit * i
709
+ limits[last] = current
710
+ last = current
711
+ # example with count == 62
712
+ # {0: 20, 20: 40, 40: 60, 60: 62}
713
+ if limit and limit < self.count():
714
+ limit = limits.get(skip, self.count())
715
+ self.cursordat = self.cursordat[skip:limit]
716
+
717
+ def _order(self, value, is_reverse=None):
718
+ """Parsing data to a sortable form
719
+ By giving each data type an ID(int), and assemble with the value
720
+ into a sortable tuple.
721
+ """
722
+
723
+ def _dict_parser(dict_doc):
724
+ """ dict ordered by:
725
+ valueType_N -> key_N -> value_N
726
+ """
727
+ result = list()
728
+ for key in dict_doc:
729
+ data = self._order(dict_doc[key])
730
+ res = (data[0], key, data[1])
731
+ result.append(res)
732
+ return tuple(result)
733
+
734
+ def _list_parser(list_doc):
735
+ """list will iter members to compare
736
+ """
737
+ result = list()
738
+ for member in list_doc:
739
+ result.append(self._order(member))
740
+ return result
741
+
742
+ # (TODO) include more data type
743
+ if value is None or not isinstance(
744
+ # value, (dict, list, basestring, bool, float, int)
745
+ value, (dict, list, str, bool, float, int)
746
+ ):
747
+ # not support/sortable value type
748
+ value = (0, None)
749
+
750
+ elif isinstance(value, bool):
751
+ value = (5, value)
752
+
753
+ elif isinstance(value, (int, float)):
754
+ value = (1, value)
755
+
756
+ # elif isinstance(value, basestring):
757
+ elif isinstance(value, str):
758
+
759
+ value = (2, value)
760
+
761
+ elif isinstance(value, dict):
762
+ value = (3, _dict_parser(value))
763
+
764
+ elif isinstance(value, list):
765
+ if len(value) == 0:
766
+ # [] less then None
767
+ value = [(-1, [])]
768
+ else:
769
+ value = _list_parser(value)
770
+
771
+ if is_reverse is not None:
772
+ # list will firstly compare with other doc by it's smallest
773
+ # or largest member
774
+ value = max(value) if is_reverse else min(value)
775
+ else:
776
+ # if the smallest or largest member is a list
777
+ # then compaer with it's sub-member in list index order
778
+ value = (4, tuple(value))
779
+
780
+ return value
781
+
782
+ def sort(self, key_or_list, direction=None):
783
+ """
784
+ Sorts a cursor object based on the input
785
+
786
+ :param key_or_list: a list/tuple containing the sort specification,
787
+ # i.e. ('user_number': -1), or a basestring
788
+ i.e. ('user_number': -1), or a str
789
+ :param direction: sorting direction, 1 or -1, needed if key_or_list
790
+ # is a basestring
791
+ is a str
792
+ :return:
793
+ """
794
+
795
+ # checking input format
796
+
797
+ sort_specifier = list()
798
+ if isinstance(key_or_list, list):
799
+ if direction is not None:
800
+ raise ValueError(
801
+ "direction can not be set separately "
802
+ "if sorting by multiple fields."
803
+ )
804
+ for pair in key_or_list:
805
+ if not (isinstance(pair, list) or isinstance(pair, tuple)):
806
+ raise TypeError("key pair should be a list or tuple.")
807
+ if not len(pair) == 2:
808
+ raise ValueError("Need to be (key, direction) pair")
809
+ # if not isinstance(pair[0], basestring):
810
+ if not isinstance(pair[0], str):
811
+ raise TypeError("first item in each key pair must " "be a string")
812
+ if not isinstance(pair[1], int) or not abs(pair[1]) == 1:
813
+ raise TypeError("bad sort specification.")
814
+
815
+ sort_specifier = key_or_list
816
+
817
+ # elif isinstance(key_or_list, basestring):
818
+ elif isinstance(key_or_list, str):
819
+ if direction is not None:
820
+ if not isinstance(direction, int) or not abs(direction) == 1:
821
+ raise TypeError("bad sort specification.")
822
+ else:
823
+ # default ASCENDING
824
+ direction = 1
825
+
826
+ sort_specifier = [(key_or_list, direction)]
827
+
828
+ else:
829
+ raise ValueError(
830
+ "Wrong input, pass a field name and a direction,"
831
+ " or pass a list of (key, direction) pairs."
832
+ )
833
+
834
+ # sorting
835
+
836
+ _cursordat = self.cursordat
837
+
838
+ total = len(_cursordat)
839
+ pre_sect_stack = list()
840
+ for pair in sort_specifier:
841
+
842
+ is_reverse = bool(1 - pair[1])
843
+ value_stack = list()
844
+ for index, data in enumerate(_cursordat):
845
+
846
+ # get field value
847
+
848
+ not_found = None
849
+ for key in pair[0].split("."):
850
+ not_found = True
851
+
852
+ if isinstance(data, dict) and key in data:
853
+ data = copy.deepcopy(data[key])
854
+ not_found = False
855
+
856
+ elif isinstance(data, list):
857
+ if not is_reverse and len(data) == 1:
858
+ # MongoDB treat [{data}] as {data}
859
+ # when finding fields
860
+ if isinstance(data[0], dict) and key in data[0]:
861
+ data = copy.deepcopy(data[0][key])
862
+ not_found = False
863
+
864
+ elif is_reverse:
865
+ # MongoDB will keep finding field in reverse mode
866
+ for _d in data:
867
+ if isinstance(_d, dict) and key in _d:
868
+ data = copy.deepcopy(_d[key])
869
+ not_found = False
870
+ break
871
+
872
+ if not_found:
873
+ break
874
+
875
+ # parsing data for sorting
876
+
877
+ if not_found:
878
+ # treat no match as None
879
+ data = None
880
+
881
+ value = self._order(data, is_reverse)
882
+
883
+ # read previous section
884
+ pre_sect = pre_sect_stack[index] if pre_sect_stack else 0
885
+ # inverse if in reverse mode
886
+ # for keeping order as ASCENDING after sort
887
+ pre_sect = (total - pre_sect) if is_reverse else pre_sect
888
+ _ind = (total - index) if is_reverse else index
889
+
890
+ value_stack.append((pre_sect, value, _ind))
891
+
892
+ # sorting cursor data
893
+
894
+ value_stack.sort(reverse=is_reverse)
895
+
896
+ ordereddat = list()
897
+ sect_stack = list()
898
+ sect_id = -1
899
+ last_dat = None
900
+ for dat in value_stack:
901
+ # restore if in reverse mode
902
+ _ind = (total - dat[-1]) if is_reverse else dat[-1]
903
+ ordereddat.append(_cursordat[_ind])
904
+
905
+ # define section
906
+ # maintain the sorting result in next level sorting
907
+ if not dat[1] == last_dat:
908
+ sect_id += 1
909
+ sect_stack.append(sect_id)
910
+ last_dat = dat[1]
911
+
912
+ # save result for next level sorting
913
+ _cursordat = ordereddat
914
+ pre_sect_stack = sect_stack
915
+
916
+ # done
917
+
918
+ self.cursordat = _cursordat
919
+
920
+ return self
921
+
922
+ def limit(self, n):
923
+ self.cursordat = self.cursordat[:n]
924
+ return self
925
+
926
+ def has_next(self):
927
+ """
928
+ Returns True if the cursor has a next position, False if not
929
+ :return:
930
+ """
931
+ cursor_pos = self.cursorpos + 1
932
+
933
+ return cursor_pos + 1 < len(self.cursordat)
934
+
935
+ def hasNext(self):
936
+ """
937
+ Returns True if the cursor has a next position, False if not
938
+ :return:
939
+ """
940
+ cursor_pos = self.cursorpos + 1
941
+
942
+ try:
943
+ self.cursordat[cursor_pos]
944
+ return True
945
+ except IndexError:
946
+ return False
947
+
948
+ def next(self):
949
+ """
950
+ Returns the next record
951
+
952
+ :return:
953
+ """
954
+ self.cursorpos += 1
955
+ return self.cursordat[self.cursorpos]
956
+
957
+ def count(self, with_limit_and_skip=False):
958
+ """
959
+ Returns the number of records in the current cursor
960
+
961
+ :return: number of records
962
+ """
963
+ return len(self.cursordat)
964
+
965
+ def __iter__(self):
966
+ self.cursorpos = -1
967
+ return self
968
+
969
+ def __next__(self):
970
+ """
971
+ Returns the next record
972
+ :return:
973
+ """
974
+ if not self.hasNext():
975
+ raise StopIteration
976
+ self.cursorpos += 1
977
+ return self.cursordat[self.cursorpos]
978
+
979
+
980
+ class TinyGridFS(object):
981
+ """GridFS for tinyDB"""
982
+
983
+ def __init__(self, *args, **kwargs):
984
+ self.database = None
985
+
986
+ def grid_fs(self, tinydatabase):
987
+ """TODO: Must implement yet"""
988
+ self.database = tinydatabase
989
+ return self
990
+
991
+ def GridFS(self, tinydatabase):
992
+ """TODO: Must implement yet"""
993
+ self.database = tinydatabase
994
+ return self
995
+
996
+ def generate_id():
997
+ """Generate new UUID"""
998
+ # TODO: Use six.string_type to Py3 compat
999
+ return str(uuid1()).replace(u"-", u"")
1000
+
1001
+ # def generate_id():
1002
+ # """Generate new UUID"""
1003
+ # # TODO: Use six.string_type to Py3 compat
1004
+ # try:
1005
+ # return unicode(uuid1()).replace(u"-", u"")
1006
+ # except NameError:
1007
+ # return str(uuid1()).replace(u"-", u"")