Added modular DAL, updated gluon imports to new structure
This commit is contained in:
1 parent
d73c668f2d
commit
5e5e649c28
43 files changed
+11577
-11686
No files matched your search
@@ -0,0 +1,575 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import logging
|
||||
import re
|
||||
|
||||
from .._globals import IDENTITY
|
||||
from ..objects import Table, Query, Field, Expression
|
||||
from ..helpers.classes import SQLALL
|
||||
from ..helpers.methods import xorify
|
||||
from .base import NoSQLAdapter
|
||||
|
||||
class MongoDBAdapter(NoSQLAdapter):
|
||||
native_json = True
|
||||
drivers = ('pymongo',)
|
||||
|
||||
uploads_in_blob = False
|
||||
|
||||
types = {
|
||||
'boolean': bool,
|
||||
'string': str,
|
||||
'text': str,
|
||||
'json': str,
|
||||
'password': str,
|
||||
'blob': str,
|
||||
'upload': str,
|
||||
'integer': long,
|
||||
'bigint': long,
|
||||
'float': float,
|
||||
'double': float,
|
||||
'date': datetime.date,
|
||||
'time': datetime.time,
|
||||
'datetime': datetime.datetime,
|
||||
'id': long,
|
||||
'reference': long,
|
||||
'list:string': list,
|
||||
'list:integer': list,
|
||||
'list:reference': list,
|
||||
}
|
||||
|
||||
error_messages = {"javascript_needed": "This must yet be replaced" +
|
||||
" with javascript in order to work."}
|
||||
|
||||
def __init__(self,db,uri='mongodb://127.0.0.1:5984/db',
|
||||
pool_size=0, folder=None, db_codec ='UTF-8',
|
||||
credential_decoder=IDENTITY, driver_args={},
|
||||
adapter_args={}, do_connect=True, after_connection=None):
|
||||
|
||||
self.db = db
|
||||
self.uri = uri
|
||||
if do_connect: self.find_driver(adapter_args)
|
||||
import random
|
||||
from bson.objectid import ObjectId
|
||||
from bson.son import SON
|
||||
import pymongo.uri_parser
|
||||
|
||||
m = pymongo.uri_parser.parse_uri(uri)
|
||||
|
||||
self.SON = SON
|
||||
self.ObjectId = ObjectId
|
||||
self.random = random
|
||||
|
||||
self.dbengine = 'mongodb'
|
||||
self.folder = folder
|
||||
db['_lastsql'] = ''
|
||||
self.db_codec = 'UTF-8'
|
||||
self._after_connection = after_connection
|
||||
self.pool_size = pool_size
|
||||
#this is the minimum amount of replicates that it should wait
|
||||
# for on insert/update
|
||||
self.minimumreplication = adapter_args.get('minimumreplication',0)
|
||||
# by default all inserts and selects are performand asynchronous,
|
||||
# but now the default is
|
||||
# synchronous, except when overruled by either this default or
|
||||
# function parameter
|
||||
self.safe = adapter_args.get('safe',True)
|
||||
# load user setting for uploads in blob storage
|
||||
self.uploads_in_blob = adapter_args.get('uploads_in_blob', False)
|
||||
|
||||
if isinstance(m,tuple):
|
||||
m = {"database" : m[1]}
|
||||
if m.get('database') is None:
|
||||
raise SyntaxError("Database is required!")
|
||||
|
||||
def connector(uri=self.uri,m=m):
|
||||
# Connection() is deprecated
|
||||
if hasattr(self.driver, "MongoClient"):
|
||||
Connection = self.driver.MongoClient
|
||||
else:
|
||||
Connection = self.driver.Connection
|
||||
return Connection(uri)[m.get('database')]
|
||||
|
||||
self.reconnect(connector,cursor=False)
|
||||
|
||||
def object_id(self, arg=None):
|
||||
""" Convert input to a valid Mongodb ObjectId instance
|
||||
|
||||
self.object_id("<random>") -> ObjectId (not unique) instance """
|
||||
if not arg:
|
||||
arg = 0
|
||||
if isinstance(arg, basestring):
|
||||
# we assume an integer as default input
|
||||
rawhex = len(arg.replace("0x", "").replace("L", "")) == 24
|
||||
if arg.isdigit() and (not rawhex):
|
||||
arg = int(arg)
|
||||
elif arg == "<random>":
|
||||
arg = int("0x%sL" % \
|
||||
"".join([self.random.choice("0123456789abcdef") \
|
||||
for x in range(24)]), 0)
|
||||
elif arg.isalnum():
|
||||
if not arg.startswith("0x"):
|
||||
arg = "0x%s" % arg
|
||||
try:
|
||||
arg = int(arg, 0)
|
||||
except ValueError, e:
|
||||
raise ValueError(
|
||||
"invalid objectid argument string: %s" % e)
|
||||
else:
|
||||
raise ValueError("Invalid objectid argument string. " +
|
||||
"Requires an integer or base 16 value")
|
||||
elif isinstance(arg, self.ObjectId):
|
||||
return arg
|
||||
|
||||
if not isinstance(arg, (int, long)):
|
||||
raise TypeError("object_id argument must be of type " +
|
||||
"ObjectId or an objectid representable integer")
|
||||
hexvalue = hex(arg)[2:].rstrip('L').zfill(24)
|
||||
return self.ObjectId(hexvalue)
|
||||
|
||||
def parse_reference(self, value, field_type):
|
||||
# here we have to check for ObjectID before base parse
|
||||
if isinstance(value, self.ObjectId):
|
||||
value = long(str(value), 16)
|
||||
return super(MongoDBAdapter,
|
||||
self).parse_reference(value, field_type)
|
||||
|
||||
def parse_id(self, value, field_type):
|
||||
if isinstance(value, self.ObjectId):
|
||||
value = long(str(value), 16)
|
||||
return super(MongoDBAdapter,
|
||||
self).parse_id(value, field_type)
|
||||
|
||||
def represent(self, obj, fieldtype):
|
||||
# the base adatpter does not support MongoDB ObjectId
|
||||
if isinstance(obj, self.ObjectId):
|
||||
value = obj
|
||||
else:
|
||||
value = NoSQLAdapter.represent(self, obj, fieldtype)
|
||||
# reference types must be convert to ObjectID
|
||||
if fieldtype =='date':
|
||||
if value is None:
|
||||
return value
|
||||
# this piece of data can be stripped off based on the fieldtype
|
||||
t = datetime.time(0, 0, 0)
|
||||
# mongodb doesn't has a date object and so it must datetime,
|
||||
# string or integer
|
||||
return datetime.datetime.combine(value, t)
|
||||
elif fieldtype == 'time':
|
||||
if value is None:
|
||||
return value
|
||||
# this piece of data can be stripped of based on the fieldtype
|
||||
d = datetime.date(2000, 1, 1)
|
||||
# mongodb doesn't has a time object and so it must datetime,
|
||||
# string or integer
|
||||
return datetime.datetime.combine(d, value)
|
||||
elif fieldtype == "blob":
|
||||
if value is None:
|
||||
return value
|
||||
from bson import Binary
|
||||
if not isinstance(value, Binary):
|
||||
if not isinstance(value, basestring):
|
||||
return Binary(str(value))
|
||||
return Binary(value)
|
||||
return value
|
||||
elif (isinstance(fieldtype, basestring) and
|
||||
fieldtype.startswith('list:')):
|
||||
if fieldtype.startswith('list:reference'):
|
||||
newval = []
|
||||
for v in value:
|
||||
newval.append(self.object_id(v))
|
||||
return newval
|
||||
return value
|
||||
elif ((isinstance(fieldtype, basestring) and
|
||||
fieldtype.startswith("reference")) or
|
||||
(isinstance(fieldtype, Table)) or fieldtype=="id"):
|
||||
value = self.object_id(value)
|
||||
return value
|
||||
|
||||
def create_table(self, table, migrate=True, fake_migrate=False,
|
||||
polymodel=None, isCapped=False):
|
||||
if isCapped:
|
||||
raise RuntimeError("Not implemented")
|
||||
|
||||
def count(self, query, distinct=None, snapshot=True):
|
||||
if distinct:
|
||||
raise RuntimeError("COUNT DISTINCT not supported")
|
||||
if not isinstance(query,Query):
|
||||
raise SyntaxError("Not Supported")
|
||||
tablename = self.get_table(query)
|
||||
return long(self.select(query,[self.db[tablename]._id], {},
|
||||
count=True,snapshot=snapshot)['count'])
|
||||
# Maybe it would be faster if we just implemented the pymongo
|
||||
# .count() function which is probably quicker?
|
||||
# therefor call __select() connection[table].find(query).count()
|
||||
# Since this will probably reduce the return set?
|
||||
|
||||
def expand(self, expression, field_type=None):
|
||||
if isinstance(expression, Query):
|
||||
# any query using 'id':=
|
||||
# set name as _id (as per pymongo/mongodb primary key)
|
||||
# convert second arg to an objectid field
|
||||
# (if its not already)
|
||||
# if second arg is 0 convert to objectid
|
||||
if isinstance(expression.first,Field) and \
|
||||
((expression.first.type == 'id') or \
|
||||
("reference" in expression.first.type)):
|
||||
if expression.first.type == 'id':
|
||||
expression.first.name = '_id'
|
||||
# cast to Mongo ObjectId
|
||||
if isinstance(expression.second, (tuple, list, set)):
|
||||
expression.second = [self.object_id(item) for
|
||||
item in expression.second]
|
||||
else:
|
||||
expression.second = self.object_id(expression.second)
|
||||
result = expression.op(expression.first, expression.second)
|
||||
|
||||
if isinstance(expression, Field):
|
||||
if expression.type=='id':
|
||||
result = "_id"
|
||||
else:
|
||||
result = expression.name
|
||||
elif isinstance(expression, (Expression, Query)):
|
||||
if not expression.second is None:
|
||||
result = expression.op(expression.first, expression.second)
|
||||
elif not expression.first is None:
|
||||
result = expression.op(expression.first)
|
||||
elif not isinstance(expression.op, str):
|
||||
result = expression.op()
|
||||
else:
|
||||
result = expression.op
|
||||
elif field_type:
|
||||
result = self.represent(expression,field_type)
|
||||
elif isinstance(expression,(list,tuple)):
|
||||
result = [self.represent(item,field_type) for
|
||||
item in expression]
|
||||
else:
|
||||
result = expression
|
||||
return result
|
||||
|
||||
def drop(self, table, mode=''):
|
||||
ctable = self.connection[table._tablename]
|
||||
ctable.drop()
|
||||
|
||||
def truncate(self, table, mode, safe=None):
|
||||
if safe == None:
|
||||
safe=self.safe
|
||||
ctable = self.connection[table._tablename]
|
||||
ctable.remove(None, safe=True)
|
||||
|
||||
def select(self, query, fields, attributes, count=False,
|
||||
snapshot=False):
|
||||
mongofields_dict = self.SON()
|
||||
mongoqry_dict = {}
|
||||
new_fields, mongosort_list = [], []
|
||||
# try an orderby attribute
|
||||
orderby = attributes.get('orderby', False)
|
||||
limitby = attributes.get('limitby', False)
|
||||
# distinct = attributes.get('distinct', False)
|
||||
if 'for_update' in attributes:
|
||||
logging.warn('mongodb does not support for_update')
|
||||
for key in set(attributes.keys())-set(('limitby',
|
||||
'orderby','for_update')):
|
||||
if attributes[key] is not None:
|
||||
logging.warn('select attribute not implemented: %s' % key)
|
||||
if limitby:
|
||||
limitby_skip, limitby_limit = limitby[0], int(limitby[1])
|
||||
else:
|
||||
limitby_skip = limitby_limit = 0
|
||||
if orderby:
|
||||
if isinstance(orderby, (list, tuple)):
|
||||
orderby = xorify(orderby)
|
||||
# !!!! need to add 'random'
|
||||
for f in self.expand(orderby).split(','):
|
||||
if f.startswith('-'):
|
||||
mongosort_list.append((f[1:], -1))
|
||||
else:
|
||||
mongosort_list.append((f, 1))
|
||||
for item in fields:
|
||||
if isinstance(item, SQLALL):
|
||||
new_fields += item._table
|
||||
else:
|
||||
new_fields.append(item)
|
||||
fields = new_fields
|
||||
if isinstance(query,Query):
|
||||
tablename = self.get_table(query)
|
||||
elif len(fields) != 0:
|
||||
tablename = fields[0].tablename
|
||||
else:
|
||||
raise SyntaxError("The table name could not be found in " +
|
||||
"the query nor from the select statement.")
|
||||
mongoqry_dict = self.expand(query)
|
||||
fields = fields or self.db[tablename]
|
||||
for field in fields:
|
||||
mongofields_dict[field.name] = 1
|
||||
ctable = self.connection[tablename]
|
||||
if count:
|
||||
return {'count' : ctable.find(
|
||||
mongoqry_dict, mongofields_dict,
|
||||
skip=limitby_skip, limit=limitby_limit,
|
||||
sort=mongosort_list, snapshot=snapshot).count()}
|
||||
else:
|
||||
# pymongo cursor object
|
||||
mongo_list_dicts = ctable.find(mongoqry_dict,
|
||||
mongofields_dict, skip=limitby_skip,
|
||||
limit=limitby_limit, sort=mongosort_list,
|
||||
snapshot=snapshot)
|
||||
rows = []
|
||||
# populate row in proper order
|
||||
# Here we replace ._id with .id to follow the standard naming
|
||||
colnames = []
|
||||
newnames = []
|
||||
for field in fields:
|
||||
colname = str(field)
|
||||
colnames.append(colname)
|
||||
tablename, fieldname = colname.split(".")
|
||||
if fieldname == "_id":
|
||||
# Mongodb reserved uuid key
|
||||
field.name = "id"
|
||||
newnames.append(".".join((tablename, field.name)))
|
||||
|
||||
for record in mongo_list_dicts:
|
||||
row=[]
|
||||
for colname in colnames:
|
||||
tablename, fieldname = colname.split(".")
|
||||
# switch to Mongo _id uuids for retrieving
|
||||
# record id's
|
||||
if fieldname == "id": fieldname = "_id"
|
||||
if fieldname in record:
|
||||
value = record[fieldname]
|
||||
else:
|
||||
value = None
|
||||
row.append(value)
|
||||
rows.append(row)
|
||||
processor = attributes.get('processor', self.parse)
|
||||
result = processor(rows, fields, newnames, False)
|
||||
return result
|
||||
|
||||
def insert(self, table, fields, safe=None):
|
||||
"""Safe determines whether a asynchronous request is done or a
|
||||
synchronous action is done
|
||||
For safety, we use by default synchronous requests"""
|
||||
|
||||
values = dict()
|
||||
if safe is None:
|
||||
safe = self.safe
|
||||
ctable = self.connection[table._tablename]
|
||||
for k, v in fields:
|
||||
if not k.name in ["id", "safe"]:
|
||||
fieldname = k.name
|
||||
fieldtype = table[k.name].type
|
||||
values[fieldname] = self.represent(v, fieldtype)
|
||||
|
||||
ctable.insert(values, safe=safe)
|
||||
return long(str(values['_id']), 16)
|
||||
|
||||
def update(self, tablename, query, fields, safe=None):
|
||||
if safe == None:
|
||||
safe = self.safe
|
||||
# return amount of adjusted rows or zero, but no exceptions
|
||||
# @ related not finding the result
|
||||
if not isinstance(query, Query):
|
||||
raise RuntimeError("Not implemented")
|
||||
amount = self.count(query, False)
|
||||
if not isinstance(query, Query):
|
||||
raise SyntaxError("Not Supported")
|
||||
filter = None
|
||||
if query:
|
||||
filter = self.expand(query)
|
||||
# do not try to update id fields to avoid backend errors
|
||||
modify = {'$set': dict((k.name, self.represent(v, k.type)) for
|
||||
k, v in fields if (not k.name in ("_id", "id")))}
|
||||
try:
|
||||
result = self.connection[tablename].update(filter,
|
||||
modify, multi=True, safe=safe)
|
||||
if safe:
|
||||
try:
|
||||
# if result count is available fetch it
|
||||
return result["n"]
|
||||
except (KeyError, AttributeError, TypeError):
|
||||
return amount
|
||||
else:
|
||||
return amount
|
||||
except Exception, e:
|
||||
# TODO Reverse update query to verifiy that the query succeded
|
||||
raise RuntimeError("uncaught exception when updating rows: %s" % e)
|
||||
|
||||
def delete(self, tablename, query, safe=None):
|
||||
if safe is None:
|
||||
safe = self.safe
|
||||
amount = 0
|
||||
amount = self.count(query, False)
|
||||
if not isinstance(query, Query):
|
||||
raise RuntimeError("query type %s is not supported" % \
|
||||
type(query))
|
||||
filter = self.expand(query)
|
||||
self.connection[tablename].remove(filter, safe=safe)
|
||||
return amount
|
||||
|
||||
def bulk_insert(self, table, items):
|
||||
return [self.insert(table,item) for item in items]
|
||||
|
||||
## OPERATORS
|
||||
def INVERT(self, first):
|
||||
#print "in invert first=%s" % first
|
||||
return '-%s' % self.expand(first)
|
||||
|
||||
# TODO This will probably not work:(
|
||||
def NOT(self, first):
|
||||
return {'$not': self.expand(first)}
|
||||
|
||||
def AND(self,first,second):
|
||||
# pymongo expects: .find({'$and': [{'x':'1'}, {'y':'2'}]})
|
||||
return {'$and': [self.expand(first),self.expand(second)]}
|
||||
|
||||
def OR(self,first,second):
|
||||
# pymongo expects: .find({'$or': [{'name':'1'}, {'name':'2'}]})
|
||||
return {'$or': [self.expand(first),self.expand(second)]}
|
||||
|
||||
def BELONGS(self, first, second):
|
||||
if isinstance(second, str):
|
||||
return {self.expand(first) : {"$in" : [ second[:-1]]} }
|
||||
elif second==[] or second==() or second==set():
|
||||
return {1:0}
|
||||
items = [self.expand(item, first.type) for item in second]
|
||||
return {self.expand(first) : {"$in" : items} }
|
||||
|
||||
def EQ(self,first,second=None):
|
||||
result = {}
|
||||
result[self.expand(first)] = self.expand(second)
|
||||
return result
|
||||
|
||||
def NE(self, first, second=None):
|
||||
result = {}
|
||||
result[self.expand(first)] = {'$ne': self.expand(second)}
|
||||
return result
|
||||
|
||||
def LT(self,first,second=None):
|
||||
if second is None:
|
||||
raise RuntimeError("Cannot compare %s < None" % first)
|
||||
result = {}
|
||||
result[self.expand(first)] = {'$lt': self.expand(second)}
|
||||
return result
|
||||
|
||||
def LE(self,first,second=None):
|
||||
if second is None:
|
||||
raise RuntimeError("Cannot compare %s <= None" % first)
|
||||
result = {}
|
||||
result[self.expand(first)] = {'$lte': self.expand(second)}
|
||||
return result
|
||||
|
||||
def GT(self,first,second):
|
||||
result = {}
|
||||
result[self.expand(first)] = {'$gt': self.expand(second)}
|
||||
return result
|
||||
|
||||
def GE(self,first,second=None):
|
||||
if second is None:
|
||||
raise RuntimeError("Cannot compare %s >= None" % first)
|
||||
result = {}
|
||||
result[self.expand(first)] = {'$gte': self.expand(second)}
|
||||
return result
|
||||
|
||||
def ADD(self, first, second):
|
||||
raise NotImplementedError(self.error_messages["javascript_needed"])
|
||||
return '%s + %s' % (self.expand(first),
|
||||
self.expand(second, first.type))
|
||||
|
||||
def SUB(self, first, second):
|
||||
raise NotImplementedError(self.error_messages["javascript_needed"])
|
||||
return '(%s - %s)' % (self.expand(first),
|
||||
self.expand(second, first.type))
|
||||
|
||||
def MUL(self, first, second):
|
||||
raise NotImplementedError(self.error_messages["javascript_needed"])
|
||||
return '(%s * %s)' % (self.expand(first),
|
||||
self.expand(second, first.type))
|
||||
|
||||
def DIV(self, first, second):
|
||||
raise NotImplementedError(self.error_messages["javascript_needed"])
|
||||
return '(%s / %s)' % (self.expand(first),
|
||||
self.expand(second, first.type))
|
||||
|
||||
def MOD(self, first, second):
|
||||
raise NotImplementedError(self.error_messages["javascript_needed"])
|
||||
return '(%s %% %s)' % (self.expand(first),
|
||||
self.expand(second, first.type))
|
||||
|
||||
def AS(self, first, second):
|
||||
raise NotImplementedError(self.error_messages["javascript_needed"])
|
||||
return '%s AS %s' % (self.expand(first), second)
|
||||
|
||||
# We could implement an option that simulates a full featured SQL
|
||||
# database. But I think the option should be set explicit or
|
||||
# implemented as another library.
|
||||
def ON(self, first, second):
|
||||
raise NotImplementedError("This is not possible in NoSQL" +
|
||||
" but can be simulated with a wrapper.")
|
||||
return '%s ON %s' % (self.expand(first), self.expand(second))
|
||||
|
||||
# BLOW ARE TWO IMPLEMENTATIONS OF THE SAME FUNCITONS
|
||||
# WHICH ONE IS BEST?
|
||||
|
||||
def COMMA(self, first, second):
|
||||
return '%s, %s' % (self.expand(first), self.expand(second))
|
||||
|
||||
def LIKE(self, first, second):
|
||||
#escaping regex operators?
|
||||
return {self.expand(first): ('%s' % \
|
||||
self.expand(second, 'string').replace('%','/'))}
|
||||
|
||||
def ILIKE(self, first, second):
|
||||
val = second if isinstance(second,self.ObjectId) else {
|
||||
'$regex': second.replace('%', ''), '$options': 'i'}
|
||||
return {self.expand(first): val}
|
||||
|
||||
def STARTSWITH(self, first, second):
|
||||
#escaping regex operators?
|
||||
return {self.expand(first): ('/^%s/' % \
|
||||
self.expand(second, 'string'))}
|
||||
|
||||
def ENDSWITH(self, first, second):
|
||||
#escaping regex operators?
|
||||
return {self.expand(first): ('/%s^/' % \
|
||||
self.expand(second, 'string'))}
|
||||
|
||||
def CONTAINS(self, first, second, case_sensitive=False):
|
||||
# silently ignore, only case sensitive
|
||||
# There is a technical difference, but mongodb doesn't support
|
||||
# that, but the result will be the same
|
||||
val = second if isinstance(second,self.ObjectId) else \
|
||||
{'$regex':".*" + re.escape(self.expand(second, 'string')) + ".*"}
|
||||
return {self.expand(first) : val}
|
||||
|
||||
def LIKE(self, first, second):
|
||||
import re
|
||||
return {self.expand(first): {'$regex': \
|
||||
re.escape(self.expand(second,
|
||||
'string')).replace('%','.*')}}
|
||||
|
||||
#TODO verify full compatibilty with official SQL Like operator
|
||||
def STARTSWITH(self, first, second):
|
||||
#TODO Solve almost the same problem as with endswith
|
||||
import re
|
||||
return {self.expand(first): {'$regex' : '^' +
|
||||
re.escape(self.expand(second,
|
||||
'string'))}}
|
||||
|
||||
#TODO verify full compatibilty with official SQL Like operator
|
||||
def ENDSWITH(self, first, second):
|
||||
#escaping regex operators?
|
||||
#TODO if searched for a name like zsa_corbitt and the function
|
||||
# is endswith('a') then this is also returned.
|
||||
# Aldo it end with a t
|
||||
import re
|
||||
return {self.expand(first): {'$regex': \
|
||||
re.escape(self.expand(second, 'string')) + '$'}}
|
||||
|
||||
#TODO verify full compatibilty with official oracle contains operator
|
||||
def CONTAINS(self, first, second, case_sensitive=False):
|
||||
# silently ignore, only case sensitive
|
||||
#There is a technical difference, but mongodb doesn't support
|
||||
# that, but the result will be the same
|
||||
#TODO contains operators need to be transformed to Regex
|
||||
return {self.expand(first) : {'$regex': \
|
||||
".*" + re.escape(self.expand(second, 'string')) + ".*"}}
|
||||
|
||||
Reference in new issue
Block a user