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/__init__.py +4 -0
- tinymongo/errors.py +61 -0
- tinymongo/parquet_storage.py +256 -0
- tinymongo/results.py +115 -0
- tinymongo/serializers.py +22 -0
- tinymongo/storage_backends.py +174 -0
- tinymongo/tinymongo.py +1007 -0
- tinymongo-1.0.0.dist-info/METADATA +234 -0
- tinymongo-1.0.0.dist-info/RECORD +12 -0
- tinymongo-1.0.0.dist-info/WHEEL +5 -0
- tinymongo-1.0.0.dist-info/licenses/LICENSE.txt +10 -0
- tinymongo-1.0.0.dist-info/top_level.txt +1 -0
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"")
|