db(...).select().group_by_value(db.table.field), thanks Yair
This commit is contained in:
@@ -1 +1 @@
|
|||||||
Version 1.99.4 (2012-02-21 16:25:39) stable
|
Version 1.99.4 (2012-02-22 11:32:05) stable
|
||||||
|
|||||||
@@ -120,9 +120,9 @@
|
|||||||
</li><li>Thadeus Burgess (validators)
|
</li><li>Thadeus Burgess (validators)
|
||||||
</li><li>Tim Michelsen (Sphinx documentation)
|
</li><li>Tim Michelsen (Sphinx documentation)
|
||||||
</li><li>Timothy Farrell (python 2.6 compliance, windows support)
|
</li><li>Timothy Farrell (python 2.6 compliance, windows support)
|
||||||
</li><li>Yair Eshel (internationalizaiton)
|
</li><li>Yair Eshel (internationalizaiton, DAL improvement)
|
||||||
</li><li>Yannis Aribaud (CAS compliance)
|
</li><li>Yannis Aribaud (CAS compliance)
|
||||||
</li><li>Yarko Tymciurak (design, Sphinx documentation)
|
</li><li>Yarko Tymciurak (design)
|
||||||
</li><li>Younghyun Jo (internationalization)
|
</li><li>Younghyun Jo (internationalization)
|
||||||
</li><li>Vidul Nikolaev Petrov (captcha)
|
</li><li>Vidul Nikolaev Petrov (captcha)
|
||||||
</li><li>Vinicius Assef
|
</li><li>Vinicius Assef
|
||||||
|
|||||||
+57
-39
@@ -930,7 +930,7 @@ class BaseAdapter(ConnectionPool):
|
|||||||
self.execute(query)
|
self.execute(query)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
if isinstance(e,self.integrity_error_class()):
|
if isinstance(e,self.integrity_error_class()):
|
||||||
return None
|
return None
|
||||||
raise e
|
raise e
|
||||||
if hasattr(table,'_primarykey'):
|
if hasattr(table,'_primarykey'):
|
||||||
return dict([(k[0].name, k[1]) for k in fields \
|
return dict([(k[0].name, k[1]) for k in fields \
|
||||||
@@ -1190,7 +1190,7 @@ class BaseAdapter(ConnectionPool):
|
|||||||
for tablename in self.tables(field):
|
for tablename in self.tables(field):
|
||||||
if not tablename in tablenames:
|
if not tablename in tablenames:
|
||||||
tablenames.append(tablename)
|
tablenames.append(tablename)
|
||||||
|
|
||||||
if use_common_filters(query):
|
if use_common_filters(query):
|
||||||
query = self.common_filter(query,tablenames)
|
query = self.common_filter(query,tablenames)
|
||||||
|
|
||||||
@@ -1470,7 +1470,7 @@ class BaseAdapter(ConnectionPool):
|
|||||||
try:
|
try:
|
||||||
value = value.decode(self.db._db_codec)
|
value = value.decode(self.db._db_codec)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
if isinstance(value, unicode):
|
if isinstance(value, unicode):
|
||||||
value = value.encode('utf-8')
|
value = value.encode('utf-8')
|
||||||
elif isinstance(field_type, SQLCustomType):
|
elif isinstance(field_type, SQLCustomType):
|
||||||
@@ -1478,7 +1478,7 @@ class BaseAdapter(ConnectionPool):
|
|||||||
if not isinstance(field_type, str) or value is None:
|
if not isinstance(field_type, str) or value is None:
|
||||||
return value
|
return value
|
||||||
elif field_type in ('string', 'text', 'password', 'upload'):
|
elif field_type in ('string', 'text', 'password', 'upload'):
|
||||||
return value
|
return value
|
||||||
else:
|
else:
|
||||||
key = regex_type.match(field_type).group(0)
|
key = regex_type.match(field_type).group(0)
|
||||||
return self.parsemap[key](value,field_type)
|
return self.parsemap[key](value,field_type)
|
||||||
@@ -1522,7 +1522,7 @@ class BaseAdapter(ConnectionPool):
|
|||||||
|
|
||||||
def parse_blob(self, value, field_type):
|
def parse_blob(self, value, field_type):
|
||||||
return base64.b64decode(str(value))
|
return base64.b64decode(str(value))
|
||||||
|
|
||||||
def parse_decimal(self, value, field_type):
|
def parse_decimal(self, value, field_type):
|
||||||
decimals = int(field_type[8:-1].split(',')[-1])
|
decimals = int(field_type[8:-1].split(',')[-1])
|
||||||
if self.dbengine == 'sqlite':
|
if self.dbengine == 'sqlite':
|
||||||
@@ -1536,7 +1536,7 @@ class BaseAdapter(ConnectionPool):
|
|||||||
value = bar_decode_integer(value)
|
value = bar_decode_integer(value)
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def parse_list_references(self, value, field_type):
|
def parse_list_references(self, value, field_type):
|
||||||
if not self.dbengine=='google:datastore':
|
if not self.dbengine=='google:datastore':
|
||||||
value = bar_decode_integer(value)
|
value = bar_decode_integer(value)
|
||||||
return [self.parse_reference(r, field_type[5:]) for r in value]
|
return [self.parse_reference(r, field_type[5:]) for r in value]
|
||||||
@@ -1568,7 +1568,7 @@ class BaseAdapter(ConnectionPool):
|
|||||||
'blob':self.parse_blob,
|
'blob':self.parse_blob,
|
||||||
'decimal':self.parse_decimal,
|
'decimal':self.parse_decimal,
|
||||||
'list:integer':self.parse_list_integers,
|
'list:integer':self.parse_list_integers,
|
||||||
'list:reference':self.parse_list_references,
|
'list:reference':self.parse_list_references,
|
||||||
'list:string':self.parse_list_strings,
|
'list:string':self.parse_list_strings,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1974,10 +1974,10 @@ class PostgreSQLAdapter(BaseAdapter):
|
|||||||
elif library == "postgres:psycopg2":
|
elif library == "postgres:psycopg2":
|
||||||
self.driver = self.drivers.get('psycopg2')
|
self.driver = self.drivers.get('psycopg2')
|
||||||
elif library == "postgres:pg8000":
|
elif library == "postgres:pg8000":
|
||||||
self.driver = self.drivers.get('pg8000')
|
self.driver = self.drivers.get('pg8000')
|
||||||
if not self.driver:
|
if not self.driver:
|
||||||
raise RuntimeError, "%s is not available" % library
|
raise RuntimeError, "%s is not available" % library
|
||||||
|
|
||||||
self.__version__ = "%s %s" % (self.driver.__name__, self.driver.__version__)
|
self.__version__ = "%s %s" % (self.driver.__name__, self.driver.__version__)
|
||||||
def connect(msg=msg,driver_args=driver_args):
|
def connect(msg=msg,driver_args=driver_args):
|
||||||
return self.driver.connect(msg,**driver_args)
|
return self.driver.connect(msg,**driver_args)
|
||||||
@@ -2073,8 +2073,8 @@ class OracleAdapter(BaseAdapter):
|
|||||||
'datetime': 'DATE',
|
'datetime': 'DATE',
|
||||||
'id': 'NUMBER PRIMARY KEY',
|
'id': 'NUMBER PRIMARY KEY',
|
||||||
'reference': 'NUMBER, CONSTRAINT %(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
|
'reference': 'NUMBER, CONSTRAINT %(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
|
||||||
'reference FK': ', CONSTRAINT FK_%(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
|
'reference FK': ', CONSTRAINT FK_%(constraint_name)s FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_key)s ON DELETE %(on_delete_action)s',
|
||||||
'reference TFK': ' CONSTRAINT FK_%(foreign_table)s_PK FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s',
|
'reference TFK': ' CONSTRAINT FK_%(foreign_table)s_PK FOREIGN KEY (%(field_name)s) REFERENCES %(foreign_table)s (%(foreign_key)s) ON DELETE %(on_delete_action)s',
|
||||||
'list:integer': 'CLOB',
|
'list:integer': 'CLOB',
|
||||||
'list:string': 'CLOB',
|
'list:string': 'CLOB',
|
||||||
'list:reference': 'CLOB',
|
'list:reference': 'CLOB',
|
||||||
@@ -3911,7 +3911,7 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
elif fieldtype == 'list:string' or fieldtype == 'list:integer' or fieldtype == 'list:reference':
|
elif fieldtype == 'list:string' or fieldtype == 'list:integer' or fieldtype == 'list:reference':
|
||||||
return value #raise SyntaxError, "Not Supported"
|
return value #raise SyntaxError, "Not Supported"
|
||||||
return value
|
return value
|
||||||
|
|
||||||
#Safe determines whether a asynchronious request is done or a synchronious action is done
|
#Safe determines whether a asynchronious request is done or a synchronious action is done
|
||||||
#For safety, we use by default synchronious requests
|
#For safety, we use by default synchronious requests
|
||||||
def insert(self,table,fields,safe=None):
|
def insert(self,table,fields,safe=None):
|
||||||
@@ -3920,14 +3920,14 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
ctable = self.connection[table._tablename]
|
ctable = self.connection[table._tablename]
|
||||||
values = dict((k.name,self.represent(v,table[k.name].type)) for k,v in fields)
|
values = dict((k.name,self.represent(v,table[k.name].type)) for k,v in fields)
|
||||||
ctable.insert(values,safe=safe)
|
ctable.insert(values,safe=safe)
|
||||||
return int(str(values['_id']), 16)
|
return int(str(values['_id']), 16)
|
||||||
|
|
||||||
def create_table(self, table, migrate=True, fake_migrate=False, polymodel=None, isCapped=False):
|
def create_table(self, table, migrate=True, fake_migrate=False, polymodel=None, isCapped=False):
|
||||||
if isCapped:
|
if isCapped:
|
||||||
raise RuntimeError, "Not implemented"
|
raise RuntimeError, "Not implemented"
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def count(self,query,distinct=None,snapshot=True):
|
def count(self,query,distinct=None,snapshot=True):
|
||||||
if distinct:
|
if distinct:
|
||||||
raise RuntimeError, "COUNT DISTINCT not supported"
|
raise RuntimeError, "COUNT DISTINCT not supported"
|
||||||
@@ -3966,7 +3966,7 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
raise SyntaxError, 'second argument must be of type bson.objectid.ObjectId or an objectid representable integer'
|
raise SyntaxError, 'second argument must be of type bson.objectid.ObjectId or an objectid representable integer'
|
||||||
elif expression.second == 0:
|
elif expression.second == 0:
|
||||||
expression.second = pymongo.objectid.ObjectId('000000000000000000000000')
|
expression.second = pymongo.objectid.ObjectId('000000000000000000000000')
|
||||||
return expression.op(expression.first, expression.second)
|
return expression.op(expression.first, expression.second)
|
||||||
if isinstance(expression, Field):
|
if isinstance(expression, Field):
|
||||||
if expression.type=='id':
|
if expression.type=='id':
|
||||||
return "_id"
|
return "_id"
|
||||||
@@ -3988,16 +3988,16 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
return ','.join(self.represent(item,field_type) for item in expression)
|
return ','.join(self.represent(item,field_type) for item in expression)
|
||||||
else:
|
else:
|
||||||
return expression
|
return expression
|
||||||
|
|
||||||
def _select(self,query,fields,attributes):
|
def _select(self,query,fields,attributes):
|
||||||
from pymongo import son
|
from pymongo import son
|
||||||
|
|
||||||
for key in set(attributes.keys())-set(('limitby','orderby')):
|
for key in set(attributes.keys())-set(('limitby','orderby')):
|
||||||
raise SyntaxError, 'invalid select attribute: %s' % key
|
raise SyntaxError, 'invalid select attribute: %s' % key
|
||||||
|
|
||||||
new_fields=[]
|
new_fields=[]
|
||||||
mongosort_list = []
|
mongosort_list = []
|
||||||
|
|
||||||
# try an orderby attribute
|
# try an orderby attribute
|
||||||
orderby = attributes.get('orderby', False)
|
orderby = attributes.get('orderby', False)
|
||||||
limitby = attributes.get('limitby', False)
|
limitby = attributes.get('limitby', False)
|
||||||
@@ -4007,7 +4007,7 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
if isinstance(orderby, (list, tuple)):
|
if isinstance(orderby, (list, tuple)):
|
||||||
print "in xorify"
|
print "in xorify"
|
||||||
orderby = xorify(orderby)
|
orderby = xorify(orderby)
|
||||||
|
|
||||||
|
|
||||||
# !!!! need to add 'random'
|
# !!!! need to add 'random'
|
||||||
for f in self.expand(orderby).split(','):
|
for f in self.expand(orderby).split(','):
|
||||||
@@ -4015,10 +4015,10 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
mongosort_list.append((f[1:],-1))
|
mongosort_list.append((f[1:],-1))
|
||||||
else:
|
else:
|
||||||
mongosort_list.append((f,1))
|
mongosort_list.append((f,1))
|
||||||
print "mongosort_list = %s" % mongosort_list
|
print "mongosort_list = %s" % mongosort_list
|
||||||
|
|
||||||
if limitby:
|
if limitby:
|
||||||
# a tuple
|
# a tuple
|
||||||
limitby_skip,limitby_limit = limitby
|
limitby_skip,limitby_limit = limitby
|
||||||
else:
|
else:
|
||||||
limitby_skip = 0
|
limitby_skip = 0
|
||||||
@@ -4029,7 +4029,7 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
|
|
||||||
#if distinct:
|
#if distinct:
|
||||||
#print "in distinct %s" % distinct
|
#print "in distinct %s" % distinct
|
||||||
|
|
||||||
mongofields_dict = son.SON()
|
mongofields_dict = son.SON()
|
||||||
mongoqry_dict = {}
|
mongoqry_dict = {}
|
||||||
for item in fields:
|
for item in fields:
|
||||||
@@ -4049,7 +4049,7 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
for f in fieldnames:
|
for f in fieldnames:
|
||||||
mongofields_dict[f.name] = 1 # ie field=1
|
mongofields_dict[f.name] = 1 # ie field=1
|
||||||
return tablename, mongoqry_dict, mongofields_dict, mongosort_list, limitby_limit, limitby_skip
|
return tablename, mongoqry_dict, mongofields_dict, mongosort_list, limitby_limit, limitby_skip
|
||||||
|
|
||||||
# need to define all the 'sql' methods gt,lt etc....
|
# need to define all the 'sql' methods gt,lt etc....
|
||||||
|
|
||||||
def select(self,query,fields,attributes,count=False,snapshot=False):
|
def select(self,query,fields,attributes,count=False,snapshot=False):
|
||||||
@@ -4069,10 +4069,10 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
return {'count' : ctable.find(mongoqry_dict,mongofields_dict,skip=limitby_skip, limit=limitby_limit, sort=mongosort_list,snapshot=snapshot).count()}
|
return {'count' : ctable.find(mongoqry_dict,mongofields_dict,skip=limitby_skip, limit=limitby_limit, sort=mongosort_list,snapshot=snapshot).count()}
|
||||||
else:
|
else:
|
||||||
mongo_list_dicts = ctable.find(mongoqry_dict,mongofields_dict,skip=limitby_skip, limit=limitby_limit, sort=mongosort_list,snapshot=snapshot) # pymongo cursor object
|
mongo_list_dicts = ctable.find(mongoqry_dict,mongofields_dict,skip=limitby_skip, limit=limitby_limit, sort=mongosort_list,snapshot=snapshot) # pymongo cursor object
|
||||||
print "mongo_list_dicts=%s" % mongo_list_dicts
|
print "mongo_list_dicts=%s" % mongo_list_dicts
|
||||||
#if mongo_list_dicts.count() > 0: #
|
#if mongo_list_dicts.count() > 0: #
|
||||||
#colnames = mongo_list_dicts[0].keys() # assuming all docs have same "shape", grab colnames from first dictionary (aka row)
|
#colnames = mongo_list_dicts[0].keys() # assuming all docs have same "shape", grab colnames from first dictionary (aka row)
|
||||||
#else:
|
#else:
|
||||||
#colnames = mongofields_dict.keys()
|
#colnames = mongofields_dict.keys()
|
||||||
#print "colnames = %s" % colnames
|
#print "colnames = %s" % colnames
|
||||||
#rows = [row.values() for row in mongo_list_dicts]
|
#rows = [row.values() for row in mongo_list_dicts]
|
||||||
@@ -4095,7 +4095,7 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
|
|
||||||
def INVERT(self,first):
|
def INVERT(self,first):
|
||||||
#print "in invert first=%s" % first
|
#print "in invert first=%s" % first
|
||||||
return '-%s' % self.expand(first)
|
return '-%s' % self.expand(first)
|
||||||
|
|
||||||
def drop(self, table, mode=''):
|
def drop(self, table, mode=''):
|
||||||
ctable = self.connection[table._tablename]
|
ctable = self.connection[table._tablename]
|
||||||
@@ -4208,7 +4208,7 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
#return '(%s == %s)' % (self.expand(first),self.expand(second,first.type))
|
#return '(%s == %s)' % (self.expand(first),self.expand(second,first.type))
|
||||||
result[self.expand(first)] = self.expand(second)
|
result[self.expand(first)] = self.expand(second)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def NE(self, first, second=None):
|
def NE(self, first, second=None):
|
||||||
print "in NE"
|
print "in NE"
|
||||||
result = {}
|
result = {}
|
||||||
@@ -4345,7 +4345,7 @@ class MongoDBAdapter(NoSQLAdapter):
|
|||||||
#return '(%s == %s)' % (self.expand(first),self.expand(second,first.type))
|
#return '(%s == %s)' % (self.expand(first),self.expand(second,first.type))
|
||||||
result[self.expand(first)] = self.expand(second)
|
result[self.expand(first)] = self.expand(second)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def NE(self, first, second=None):
|
def NE(self, first, second=None):
|
||||||
print "in NE"
|
print "in NE"
|
||||||
result = {}
|
result = {}
|
||||||
@@ -4453,14 +4453,14 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
IMAP server mailbox list information.
|
IMAP server mailbox list information.
|
||||||
|
|
||||||
Here is a list of supported fields:
|
Here is a list of supported fields:
|
||||||
|
|
||||||
Field Type Description
|
Field Type Description
|
||||||
################################################################
|
################################################################
|
||||||
uid string
|
uid string
|
||||||
answered boolean Flag
|
answered boolean Flag
|
||||||
created date
|
created date
|
||||||
content list:string A list of text or html parts
|
content list:string A list of text or html parts
|
||||||
to string
|
to string
|
||||||
cc string
|
cc string
|
||||||
bcc string
|
bcc string
|
||||||
size integer the amount of octets of the message*
|
size integer the amount of octets of the message*
|
||||||
@@ -4470,7 +4470,7 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
sender string
|
sender string
|
||||||
recent boolean Flag
|
recent boolean Flag
|
||||||
seen boolean Flag
|
seen boolean Flag
|
||||||
subject string
|
subject string
|
||||||
mime string The mime header declaration
|
mime string The mime header declaration
|
||||||
email string The complete RFC822 message**
|
email string The complete RFC822 message**
|
||||||
attachments list:string Each non text decoded part as string
|
attachments list:string Each non text decoded part as string
|
||||||
@@ -4498,7 +4498,7 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
# Count today's unseen messages
|
# Count today's unseen messages
|
||||||
# smaller than 6000 octets from the
|
# smaller than 6000 octets from the
|
||||||
# inbox mailbox
|
# inbox mailbox
|
||||||
|
|
||||||
q = imapdb.INBOX.seen == False
|
q = imapdb.INBOX.seen == False
|
||||||
q &= imapdb.INBOX.created == datetime.date.today()
|
q &= imapdb.INBOX.created == datetime.date.today()
|
||||||
q &= imapdb.INBOX.size < 6000
|
q &= imapdb.INBOX.size < 6000
|
||||||
@@ -4524,7 +4524,7 @@ class IMAPAdapter(NoSQLAdapter):
|
|||||||
|
|
||||||
# It is possible also to mark messages for deletion instead of ereasing them
|
# It is possible also to mark messages for deletion instead of ereasing them
|
||||||
# directly with set.update(deleted=True)
|
# directly with set.update(deleted=True)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
types = {
|
types = {
|
||||||
@@ -6384,7 +6384,7 @@ class Table(dict):
|
|||||||
if not field.name in fieldnames and not field.type=='id':
|
if not field.name in fieldnames and not field.type=='id':
|
||||||
field = copy.copy(field)
|
field = copy.copy(field)
|
||||||
# correct self references
|
# correct self references
|
||||||
if not table._actual and field.type == 'reference '+table._tablename:
|
if not table._actual and field.type == 'reference '+table._tablename:
|
||||||
field.type = 'reference '+self._tablename
|
field.type = 'reference '+self._tablename
|
||||||
newfields.append(field)
|
newfields.append(field)
|
||||||
fieldnames.add(field.name)
|
fieldnames.add(field.name)
|
||||||
@@ -7373,7 +7373,7 @@ class Set(object):
|
|||||||
def count(self,distinct=None):
|
def count(self,distinct=None):
|
||||||
return self.db._adapter.count(self.query,distinct)
|
return self.db._adapter.count(self.query,distinct)
|
||||||
|
|
||||||
def select(self, *fields, **attributes):
|
def select(self, *fields, **attributes):
|
||||||
adapter = self.db._adapter
|
adapter = self.db._adapter
|
||||||
fields = adapter.expand_all(fields, adapter.tables(self.query))
|
fields = adapter.expand_all(fields, adapter.tables(self.query))
|
||||||
return adapter.select(self.query,fields,attributes)
|
return adapter.select(self.query,fields,attributes)
|
||||||
@@ -7624,6 +7624,23 @@ class Rows(object):
|
|||||||
"""
|
"""
|
||||||
return Rows(self.db,sorted(self,key=f,reverse=reverse),self.colnames)
|
return Rows(self.db,sorted(self,key=f,reverse=reverse),self.colnames)
|
||||||
|
|
||||||
|
def group_by_value(self, field):
|
||||||
|
"""
|
||||||
|
regroups the rows, by one of the fields
|
||||||
|
"""
|
||||||
|
if not self.records:
|
||||||
|
return {}
|
||||||
|
key = str(field)
|
||||||
|
grouped_row_group = dict()
|
||||||
|
|
||||||
|
for row in self:
|
||||||
|
value = row[key]
|
||||||
|
if not value in grouped_row_group:
|
||||||
|
grouped_row_group[value] = [row]
|
||||||
|
else:
|
||||||
|
grouped_row_group[value].append(row)
|
||||||
|
return grouped_row_group
|
||||||
|
|
||||||
def as_list(self,
|
def as_list(self,
|
||||||
compact=True,
|
compact=True,
|
||||||
storage_to_dict=True,
|
storage_to_dict=True,
|
||||||
@@ -8000,3 +8017,4 @@ if __name__ == '__main__':
|
|||||||
import doctest
|
import doctest
|
||||||
doctest.testmod()
|
doctest.testmod()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user