# -*- coding: utf-8 -*- # pylint: disable=no-member,not-an-iterable import base64 import binascii import cgi import copy import csv import datetime import decimal import os import shutil import sys import types import re from collections import OrderedDict from ._compat import ( PY2, StringIO, BytesIO, pjoin, exists, hashlib_md5, basestring, iteritems, xrange, implements_iterator, implements_bool, copyreg, reduce, to_bytes, to_native, to_unicode, long, text_type, ) from ._globals import DEFAULT, IDENTITY, AND, OR from ._gae import Key from .exceptions import NotFoundException, NotAuthorizedException from .helpers.regex import ( REGEX_TABLE_DOT_FIELD, REGEX_ALPHANUMERIC, REGEX_PYTHON_KEYWORDS, REGEX_UPLOAD_EXTENSION, REGEX_UPLOAD_PATTERN, REGEX_UPLOAD_CLEANUP, REGEX_VALID_TB_FLD, REGEX_TYPE, REGEX_TABLE_DOT_FIELD_OPTIONAL_QUOTES, ) from .helpers.classes import ( Reference, MethodAdder, SQLCallableList, SQLALL, Serializable, BasicStorage, SQLCustomType, OpRow, cachedprop, ) from .helpers.methods import ( list_represent, bar_decode_integer, bar_decode_string, bar_encode, archive_record, cleanup, use_common_filters, attempt_upload_on_insert, attempt_upload_on_update, delete_uploaded_files, uuidstr ) from .helpers.serializers import serializers from .utils import deprecated if not PY2: unicode = str DEFAULTLENGTH = { "string": 512, "password": 512, "upload": 512, "text": 2 ** 15, "blob": 2 ** 31, } DEFAULT_REGEX = { "id": "[1-9]\d*", "decimal": "\d{1,10}\.\d{2}", "integer": "[+-]?\d*", "float": "[+-]?\d*(\.\d*)?", "double": "[+-]?\d*(\.\d*)?", "date": "\d{4}\-\d{2}\-\d{2}", "time": "\d{2}\:\d{2}(\:\d{2}(\.\d*)?)?", "datetime": "\d{4}\-\d{2}\-\d{2} \d{2}\:\d{2}(\:\d{2}(\.\d*)?)?", } def csv_reader(utf8_data, dialect=csv.excel, encoding="utf-8", **kwargs): """like csv.reader but allows to specify an encoding, defaults to utf-8""" csv_reader = csv.reader(utf8_data, dialect=dialect, **kwargs) for row in csv_reader: yield [to_unicode(cell, encoding) for cell in row] class Row(BasicStorage): """ A dictionary that lets you do d['a'] as well as d.a this is only used to store a `Row` """ def __getitem__(self, k): key = str(k) _extra = BasicStorage.get(self, "_extra", None) if _extra is not None: v = _extra.get(key, DEFAULT) if v is not DEFAULT: return v try: return BasicStorage.__getattribute__(self, key) except AttributeError: pass m = REGEX_TABLE_DOT_FIELD.match(key) if m: key2 = m.group(2) try: return BasicStorage.__getitem__(self, m.group(1))[key2] except (KeyError, TypeError): pass try: return BasicStorage.__getitem__(self, key2) except KeyError: pass lg = BasicStorage.get(self, "__get_lazy_reference__", None) if callable(lg): v = self[key] = lg(key) return v raise KeyError(key) def __repr__(self): return "" % self.as_dict(custom_types=[LazySet]) def __int__(self): return self.get("id") def __long__(self): return long(int(self)) def __hash__(self): return id(self) __str__ = __repr__ __call__ = __getitem__ def __getattr__(self, k): try: return self.__getitem__(k) except KeyError: raise AttributeError def __copy__(self): return Row(self) def __eq__(self, other): try: return self.as_dict() == other.as_dict() except AttributeError: return False def get(self, key, default=None): try: return self.__getitem__(key) except (KeyError, AttributeError, TypeError): return default def as_dict(self, datetime_to_str=False, custom_types=None): SERIALIZABLE_TYPES = [str, int, float, bool, list, dict] DT_INST = (datetime.date, datetime.datetime, datetime.time) if PY2: SERIALIZABLE_TYPES += [unicode, long] if isinstance(custom_types, (list, tuple, set)): SERIALIZABLE_TYPES += custom_types elif custom_types: SERIALIZABLE_TYPES.append(custom_types) d = dict(self) for k in list(d.keys()): v = d[k] if d[k] is None: continue elif isinstance(v, Row): d[k] = v.as_dict() elif isinstance(v, Reference): d[k] = long(v) elif isinstance(v, decimal.Decimal): d[k] = float(v) elif isinstance(v, DT_INST): if datetime_to_str: d[k] = v.isoformat().replace("T", " ")[:19] elif not isinstance(v, tuple(SERIALIZABLE_TYPES)): del d[k] return d def as_xml(self, row_name="row", colnames=None, indent=" "): def f(row, field, indent=" "): if isinstance(row, Row): spc = indent + " \n" items = [f(row[x], x, indent + " ") for x in row] return "%s<%s>\n%s\n%s" % ( indent, field, spc.join(item for item in items if item), indent, field, ) elif not callable(row): if re.match(REGEX_ALPHANUMERIC, field): return "%s<%s>%s" % (indent, field, row, field) else: return '%s%s' % (indent, field, row) else: return None return f(self, row_name, indent=indent) def as_json( self, mode="object", default=None, colnames=None, serialize=True, **kwargs ): """ serializes the row to a JSON object kwargs are passed to .as_dict method only "object" mode supported `serialize = False` used by Rows.as_json TODO: return array mode with query column order mode and colnames are not implemented """ item = self.as_dict(**kwargs) if serialize: return serializers.json(item) else: return item def pickle_row(s): return Row, (dict(s),) copyreg.pickle(Row, pickle_row) class Table(Serializable, BasicStorage): """ Represents a database table Example:: You can create a table as:: db = DAL(...) db.define_table('users', Field('name')) And then:: db.users.insert(name='me') # print db.users._insert(...) to see SQL db.users.drop() """ def __init__(self, db, tablename, *fields, **args): """ Initializes the table and performs checking on the provided fields. Each table will have automatically an 'id'. If a field is of type Table, the fields (excluding 'id') from that table will be used instead. Raises: SyntaxError: when a supplied field is of incorrect type. """ # import DAL here to avoid circular imports from .base import DAL super(Table, self).__init__() self._actual = False # set to True by define_table() self._db = db self._migrate = None self._tablename = self._dalname = tablename if ( not isinstance(tablename, str) or hasattr(DAL, tablename) or not REGEX_VALID_TB_FLD.match(tablename) or REGEX_PYTHON_KEYWORDS.match(tablename) ): raise SyntaxError( "Field: invalid table name: %s, " 'use rname for "funny" names' % tablename ) self._rname = args.get("rname") or db and db._adapter.dialect.quote(tablename) self._raw_rname = args.get("rname") or db and tablename self._sequence_name = ( args.get("sequence_name") or db and db._adapter.dialect.sequence_name(self._raw_rname) ) self._trigger_name = ( args.get("trigger_name") or db and db._adapter.dialect.trigger_name(tablename) ) self._common_filter = args.get("common_filter") self._format = args.get("format") self._singular = args.get("singular", tablename.replace("_", " ").capitalize()) self._plural = args.get("plural") # horrible but for backward compatibility of appadmin if "primarykey" in args and args["primarykey"] is not None: self._primarykey = args.get("primarykey") self._before_insert = [attempt_upload_on_insert(self)] self._before_update = [delete_uploaded_files, attempt_upload_on_update(self)] self._before_delete = [delete_uploaded_files] self._after_insert = [] self._after_update = [] self._after_delete = [] self._virtual_fields = [] self._virtual_methods = [] self.add_method = MethodAdder(self) fieldnames = set() newfields = [] _primarykey = getattr(self, "_primarykey", None) if _primarykey is not None: if not isinstance(_primarykey, list): raise SyntaxError( "primarykey must be a list of fields from table '%s'" % tablename ) if len(_primarykey) == 1: self._id = [ f for f in fields if isinstance(f, Field) and f.name == _primarykey[0] ][0] elif not [ f for f in fields if (isinstance(f, Field) and f.type == "id") or (isinstance(f, dict) and f.get("type", None) == "id") ]: field = Field("id", "id") newfields.append(field) fieldnames.add("id") self._id = field virtual_fields = [] def include_new(field): newfields.append(field) fieldnames.add(field.name) if field.type == "id": self._id = field for field in fields: if isinstance(field, (FieldVirtual, FieldMethod)): virtual_fields.append(field) elif isinstance(field, Field) and field.name not in fieldnames: if field.db is not None: field = copy.copy(field) include_new(field) elif isinstance(field, (list, tuple)): for other in field: include_new(other) elif isinstance(field, Table): table = field for field in table: if field.name not in fieldnames and field.type != "id": t2 = not table._actual and self._tablename include_new(field.clone(point_self_references_to=t2)) elif isinstance(field, dict) and field["fieldname"] not in fieldnames: include_new(Field(**field)) elif not isinstance(field, (Field, Table)): raise SyntaxError( "define_table argument is not a Field, Table of list: %s" % field ) fields = newfields self._fields = SQLCallableList() self.virtualfields = [] if db and db._adapter.uploads_in_blob is True: uploadfields = [f.name for f in fields if f.type == "blob"] for field in fields: fn = field.uploadfield if ( isinstance(field, Field) and field.type == "upload" and fn is True and not field.uploadfs ): fn = field.uploadfield = "%s_blob" % field.name if ( isinstance(fn, str) and fn not in uploadfields and not field.uploadfs ): fields.append( Field(fn, "blob", default="", writable=False, readable=False) ) fieldnames_set = set() reserved = dir(Table) + ["fields"] if db and db._check_reserved: check_reserved_keyword = db.check_reserved_keyword else: def check_reserved_keyword(field_name): if field_name in reserved: raise SyntaxError("field name %s not allowed" % field_name) for field in fields: field_name = field.name check_reserved_keyword(field_name) if db and db._ignore_field_case: fname_item = field_name.lower() else: fname_item = field_name if fname_item in fieldnames_set: raise SyntaxError( "duplicate field %s in table %s" % (field_name, tablename) ) else: fieldnames_set.add(fname_item) self.fields.append(field_name) self[field_name] = field if field.type == "id": self["id"] = field field.bind(self) self.ALL = SQLALL(self) if _primarykey is not None: for k in _primarykey: if k not in self.fields: raise SyntaxError( "primarykey must be a list of fields from table '%s " % tablename ) else: self[k].notnull = True for field in virtual_fields: self[field.name] = field @property def fields(self): return self._fields def _structure(self): keys = [ "name", "type", "writable", "listable", "searchable", "regex", "options", "default", "label", "unique", "notnull", "required", ] def noncallable(obj): return obj if not callable(obj) else None return [ {key: noncallable(getattr(field, key)) for key in keys} for field in self if field.readable and not field.type == "password" ] @cachedprop def _upload_fieldnames(self): return set(field.name for field in self if field.type == "upload") def update(self, *args, **kwargs): raise RuntimeError("Syntax Not Supported") def _enable_record_versioning( self, archive_db=None, archive_name="%(tablename)s_archive", is_active="is_active", current_record="current_record", current_record_label=None, migrate=None, redefine=None, ): db = self._db archive_db = archive_db or db archive_name = archive_name % dict(tablename=self._dalname) if archive_name in archive_db.tables(): return # do not try define the archive if already exists fieldnames = self.fields() same_db = archive_db is db field_type = self if same_db else "bigint" clones = [] for field in self: nfk = same_db or not field.type.startswith("reference") clones.append( field.clone(unique=False, type=field.type if nfk else "bigint") ) d = dict(format=self._format) if migrate: d["migrate"] = migrate elif isinstance(self._migrate, basestring): d["migrate"] = self._migrate + "_archive" elif self._migrate: d["migrate"] = self._migrate if redefine: d["redefine"] = redefine archive_db.define_table( archive_name, Field(current_record, field_type, label=current_record_label), *clones, **d ) self._before_update.append( lambda qset, fs, db=archive_db, an=archive_name, cn=current_record: archive_record( qset, fs, db[an], cn ) ) if is_active and is_active in fieldnames: self._before_delete.append(lambda qset: qset.update(is_active=False)) newquery = lambda query, t=self, name=self._tablename: reduce( AND, [ tab.is_active == True for tab in db._adapter.tables(query).values() if tab._raw_rname == self._raw_rname ], ) query = self._common_filter if query: self._common_filter = lambda q: reduce(AND, [query(q), newquery(q)]) else: self._common_filter = newquery def _validate(self, **vars): errors = Row() for key, value in iteritems(vars): value, error = getattr(self, key).validate(value, vars.get("id")) if error: errors[key] = error return errors def _create_references(self): db = self._db pr = db._pending_references self._referenced_by_list = [] self._referenced_by = [] self._references = [] for field in self: # fieldname = field.name #FIXME not used ? field_type = field.type if isinstance(field_type, str) and ( field_type.startswith("reference ") or field_type.startswith("list:reference ") ): is_list = field_type[:15] == "list:reference " if is_list: ref = field_type[15:].strip() else: ref = field_type[10:].strip() if not ref: SyntaxError("Table: reference to nothing: %s" % ref) if "." in ref: rtablename, throw_it, rfieldname = ref.partition(".") else: rtablename, rfieldname = ref, None if rtablename not in db: pr[rtablename] = pr.get(rtablename, []) + [field] continue rtable = db[rtablename] if rfieldname: if not hasattr(rtable, "_primarykey"): raise SyntaxError( "keyed tables can only reference other keyed tables (for now)" ) if rfieldname not in rtable.fields: raise SyntaxError( "invalid field '%s' for referenced table '%s'" " in table '%s'" % (rfieldname, rtablename, self._tablename) ) rfield = rtable[rfieldname] else: rfield = rtable._id if is_list: rtable._referenced_by_list.append(field) else: rtable._referenced_by.append(field) field.referent = rfield self._references.append(field) else: field.referent = None if self._tablename in pr: referees = pr.pop(self._tablename) for referee in referees: if referee.type.startswith("list:reference "): self._referenced_by_list.append(referee) else: self._referenced_by.append(referee) def _filter_fields(self, record, id=False): return dict( [ (k, v) for (k, v) in iteritems(record) if k in self.fields and (getattr(self, k).type != "id" or id) ] ) def _build_query(self, key): """ for keyed table only """ query = None for k, v in iteritems(key): if k in self._primarykey: if query: query = query & (getattr(self, k) == v) else: query = getattr(self, k) == v else: raise SyntaxError( "Field %s is not part of the primary key of %s" % (k, self._tablename) ) return query def __getitem__(self, key): if str(key).isdigit() or (Key is not None and isinstance(key, Key)): # non negative key or gae return ( self._db(self._id == str(key)) .select(limitby=(0, 1), orderby_on_limitby=False) .first() ) elif isinstance(key, dict): # keyed table query = self._build_query(key) return ( self._db(query).select(limitby=(0, 1), orderby_on_limitby=False).first() ) elif key is not None: try: return getattr(self, key) except: raise KeyError(key) def __call__(self, key=DEFAULT, **kwargs): for_update = kwargs.get("_for_update", False) if "_for_update" in kwargs: del kwargs["_for_update"] orderby = kwargs.get("_orderby", None) if "_orderby" in kwargs: del kwargs["_orderby"] if key is not DEFAULT: if isinstance(key, Query): record = ( self._db(key) .select( limitby=(0, 1), for_update=for_update, orderby=orderby, orderby_on_limitby=False, ) .first() ) elif not str(key).isdigit(): record = None else: record = ( self._db(self._id == key) .select( limitby=(0, 1), for_update=for_update, orderby=orderby, orderby_on_limitby=False, ) .first() ) if record: for k, v in iteritems(kwargs): if record[k] != v: return None return record elif kwargs: query = reduce( lambda a, b: a & b, [getattr(self, k) == v for k, v in iteritems(kwargs)], ) return ( self._db(query) .select( limitby=(0, 1), for_update=for_update, orderby=orderby, orderby_on_limitby=False, ) .first() ) else: return None def __setitem__(self, key, value): if key is None: # table[None] = value (shortcut for insert) self.insert(**self._filter_fields(value)) elif str(key).isdigit(): # table[non negative key] = value (shortcut for update) if not self._db(self._id == key).update(**self._filter_fields(value)): raise SyntaxError("No such record: %s" % key) elif isinstance(key, dict): # keyed table if not isinstance(value, dict): raise SyntaxError("value must be a dictionary: %s" % value) if set(key.keys()) == set(self._primarykey): value = self._filter_fields(value) kv = {} kv.update(value) kv.update(key) if not self.insert(**kv): query = self._build_query(key) self._db(query).update(**self._filter_fields(value)) else: raise SyntaxError( "key must have all fields from primary key: %s" % self._primarykey ) else: if isinstance(value, FieldVirtual): value.bind(self, str(key)) self._virtual_fields.append(value) elif isinstance(value, FieldMethod): value.bind(self, str(key)) self._virtual_methods.append(value) self.__dict__[str(key)] = value def __setattr__(self, key, value): if key[:1] != "_" and key in self: raise SyntaxError("Object exists and cannot be redefined: %s" % key) self[key] = value def __delitem__(self, key): if isinstance(key, dict): query = self._build_query(key) if not self._db(query).delete(): raise SyntaxError("No such record: %s" % key) elif not str(key).isdigit() or not self._db(self._id == key).delete(): raise SyntaxError("No such record: %s" % key) def __iter__(self): for fieldname in self.fields: yield getattr(self, fieldname) def __repr__(self): return "" % (self._tablename, ", ".join(self.fields())) def __str__(self): if self._tablename == self._dalname: return self._tablename return self._db._adapter.dialect._as(self._dalname, self._tablename) @property @deprecated("sqlsafe", "sql_shortref", "Table") def sqlsafe(self): return self.sql_shortref @property @deprecated("sqlsafe_alias", "sql_fullref", "Table") def sqlsafe_alias(self): return self.sql_fullref @property def sql_shortref(self): if self._tablename == self._dalname: return self._rname return self._db._adapter.sqlsafe_table(self._tablename) @property def sql_fullref(self): if self._tablename == self._dalname: if self._db._adapter.dbengine == "oracle": return self._db._adapter.dialect.quote(self._rname) return self._rname return self._db._adapter.sqlsafe_table(self._tablename, self._rname) def query_name(self, *args, **kwargs): return (self.sql_fullref,) def _drop(self, mode=""): return self._db._adapter.dialect.drop_table(self, mode) def drop(self, mode=""): return self._db._adapter.drop_table(self, mode) def _filter_fields_for_operation(self, fields): new_fields = {} # format: new_fields[name] = (field, value) input_fieldnames = set(fields) table_fieldnames = set(self.fields) empty_fieldnames = OrderedDict((name, name) for name in self.fields) for name in list(input_fieldnames & table_fieldnames): field = getattr(self, name) value = field.filter_in(fields[name]) if field.filter_in else fields[name] new_fields[name] = (field, value) del empty_fieldnames[name] return list(empty_fieldnames), new_fields def _compute_fields_for_operation(self, fields, to_compute): row = OpRow(self) for name, tup in iteritems(fields): field, value = tup if isinstance( value, ( types.LambdaType, types.FunctionType, types.MethodType, types.BuiltinFunctionType, types.BuiltinMethodType, ), ): value = value() row.set_value(name, value, field) for name, field in to_compute: try: row.set_value(name, field.compute(row), field) except (KeyError, AttributeError): # error silently unless field is required! if field.required and name not in fields: raise RuntimeError("unable to compute required field: %s" % name) return row def _fields_and_values_for_insert(self, fields): empty_fieldnames, new_fields = self._filter_fields_for_operation(fields) to_compute = [] for name in empty_fieldnames: field = getattr(self, name) if field.compute: to_compute.append((name, field)) elif field.default is not None: new_fields[name] = (field, field.default) elif field.required: raise RuntimeError("Table: missing required field: %s" % name) return self._compute_fields_for_operation(new_fields, to_compute) def _fields_and_values_for_update(self, fields): empty_fieldnames, new_fields = self._filter_fields_for_operation(fields) to_compute = [] for name in empty_fieldnames: field = getattr(self, name) if field.compute: to_compute.append((name, field)) if field.update is not None: new_fields[name] = (field, field.update) return self._compute_fields_for_operation(new_fields, to_compute) def _insert(self, **fields): row = self._fields_and_values_for_insert(fields) return self._db._adapter._insert(self, row.op_values()) def insert(self, **fields): row = self._fields_and_values_for_insert(fields) if any(f(row) for f in self._before_insert): return 0 ret = self._db._adapter.insert(self, row.op_values()) if ret and self._after_insert: for f in self._after_insert: f(row, ret) return ret def _validate_fields(self, fields, defattr="default", id=None): response = Row() response.id, response.errors, new_fields = None, Row(), Row() for field in self: # we validate even if not passed in case it is required error = default = None if not field.required and not field.compute: default = getattr(field, defattr) if callable(default): default = default() if not field.compute: value = fields.get(field.name, default) value, error = field.validate(value, id) if error: response.errors[field.name] = "%s" % error elif field.name in fields: # only write if the field was passed and no error new_fields[field.name] = value return response, new_fields def validate_and_insert(self, **fields): response, new_fields = self._validate_fields(fields, "default") if not response.errors: response.id = self.insert(**new_fields) return response def validate_and_update(self, _key, **fields): record = self(**_key) if isinstance(_key, dict) else self(_key) response, new_fields = self._validate_fields(fields, "update", record.id) #: do the update if not response.errors and record: if "_id" in self: myset = self._db(self._id == record[self._id.name]) else: query = None for key, value in iteritems(_key): if query is None: query = getattr(self, key) == value else: query = query & (getattr(self, key) == value) myset = self._db(query) response.updated = myset.update(**new_fields) if record: response.id = record.id return response def update_or_insert(self, _key=DEFAULT, **values): if _key is DEFAULT: record = self(**values) elif isinstance(_key, dict): record = self(**_key) else: record = self(_key) if record: record.update_record(**values) newid = None else: newid = self.insert(**values) return newid def validate_and_update_or_insert(self, _key=DEFAULT, **fields): if _key is DEFAULT or _key == "": primary_keys = {} for key, value in iteritems(fields): if key in self._primarykey: primary_keys[key] = value if primary_keys != {}: record = self(**primary_keys) _key = primary_keys else: required_keys = {} for key, value in iteritems(fields): if getattr(self, key).required: required_keys[key] = value record = self(**required_keys) _key = required_keys elif isinstance(_key, dict): record = self(**_key) else: record = self(_key) if record: response = self.validate_and_update(_key, **fields) if hasattr(self, "_primarykey"): primary_keys = {} for key in self._primarykey: primary_keys[key] = getattr(record, key) response.id = primary_keys else: response = self.validate_and_insert(**fields) return response def bulk_insert(self, items): """ here items is a list of dictionaries """ data = [self._fields_and_values_for_insert(item) for item in items] if any(f(el) for el in data for f in self._before_insert): return 0 ret = self._db._adapter.bulk_insert(self, [el.op_values() for el in data]) ret and [ [f(el, ret[k]) for k, el in enumerate(data)] for f in self._after_insert ] return ret def _truncate(self, mode=""): return self._db._adapter.dialect.truncate(self, mode) def truncate(self, mode=""): return self._db._adapter.truncate(self, mode) def import_from_csv_file( self, csvfile, id_map=None, null="", unique="uuid", id_offset=None, # id_offset used only when id_map is None transform=None, validate=False, encoding="utf-8", **kwargs ): """ Import records from csv file. Column headers must have same names as table fields. Field 'id' is ignored. If column names read 'table.file' the 'table.' prefix is ignored. - 'unique' argument is a field which must be unique (typically a uuid field) - 'restore' argument is default False; if set True will remove old values in table first. - 'id_map' if set to None will not map ids The import will keep the id numbers in the restored table. This assumes that there is a field of type id that is integer and in incrementing order. Will keep the id numbers in restored table. """ if validate: inserting = self.validate_and_insert else: inserting = self.insert delimiter = kwargs.get("delimiter", ",") quotechar = kwargs.get("quotechar", '"') quoting = kwargs.get("quoting", csv.QUOTE_MINIMAL) restore = kwargs.get("restore", False) if restore: self._db[self].truncate() reader = csv_reader( csvfile, delimiter=delimiter, encoding=encoding, quotechar=quotechar, quoting=quoting, ) colnames = None if isinstance(id_map, dict): if self._tablename not in id_map: id_map[self._tablename] = {} id_map_self = id_map[self._tablename] def fix(field, value, id_map, id_offset): list_reference_s = "list:reference" if value == null: value = None elif field.type == "blob": value = base64.b64decode(value) elif field.type == "double" or field.type == "float": if not value.strip(): value = None else: value = float(value) elif field.type in ("integer", "bigint"): if not value.strip(): value = None else: value = long(value) elif field.type.startswith("list:string"): value = bar_decode_string(value) elif field.type.startswith(list_reference_s): ref_table = field.type[len(list_reference_s) :].strip() if id_map is not None: value = [ id_map[ref_table][long(v)] for v in bar_decode_string(value) ] else: value = [v for v in bar_decode_string(value)] elif field.type.startswith("list:"): value = bar_decode_integer(value) elif id_map and field.type.startswith("reference"): try: value = id_map[field.type[9:].strip()][long(value)] except KeyError: pass elif id_offset and field.type.startswith("reference"): try: value = id_offset[field.type[9:].strip()] + long(value) except KeyError: pass return value def is_id(colname): if colname in self: return getattr(self, colname).type == "id" else: return False first = True unique_idx = None for lineno, line in enumerate(reader): if not line: return if not colnames: # assume this is the first line of the input, contains colnames colnames = [x.split(".", 1)[-1] for x in line] cols, cid = {}, None for i, colname in enumerate(colnames): if is_id(colname): cid = colname elif colname in self.fields: cols[colname] = getattr(self, colname) if colname == unique: unique_idx = i elif len(line) == len(colnames): # every other line contains instead data items = dict(zip(colnames, line)) if transform: items = transform(items) ditems = dict() csv_id = None for field in self: fieldname = field.name if fieldname in items: try: value = fix(field, items[fieldname], id_map, id_offset) if field.type != "id": ditems[fieldname] = value else: csv_id = long(value) except ValueError: raise RuntimeError("Unable to parse line:%s" % (lineno + 1)) if not (id_map or csv_id is None or id_offset is None or unique_idx): curr_id = inserting(**ditems) if first: first = False # First curr_id is bigger than csv_id, # then we are not restoring but # extending db table with csv db table id_offset[self._tablename] = ( (curr_id - csv_id) if curr_id > csv_id else 0 ) # create new id until we get the same as old_id+offset while curr_id < csv_id + id_offset[self._tablename]: self._db(getattr(self, cid) == curr_id).delete() curr_id = inserting(**ditems) # Validation. Check for duplicate of 'unique' &, # if present, update instead of insert. elif not unique_idx: new_id = inserting(**ditems) else: unique_value = line[unique_idx] query = getattr(self, unique) == unique_value record = self._db(query).select().first() if record: record.update_record(**ditems) new_id = record[self._id.name] else: new_id = inserting(**ditems) if id_map and csv_id is not None: id_map_self[csv_id] = new_id if lineno % 1000 == 999: self._db.commit() def as_dict(self, flat=False, sanitize=True): table_as_dict = dict( tablename=str(self), fields=[], sequence_name=self._sequence_name, trigger_name=self._trigger_name, common_filter=self._common_filter, format=self._format, singular=self._singular, plural=self._plural, ) for field in self: if (field.readable or field.writable) or (not sanitize): table_as_dict["fields"].append( field.as_dict(flat=flat, sanitize=sanitize) ) return table_as_dict def with_alias(self, alias): try: if self._db[alias]._rname == self._rname: return self._db[alias] except AttributeError: # we never used this alias pass other = copy.copy(self) other["ALL"] = SQLALL(other) other["_tablename"] = alias for fieldname in other.fields: tmp = getattr(self, fieldname).clone() tmp.bind(other) other[fieldname] = tmp if "id" in self and "id" not in other.fields: other["id"] = other[self.id.name] other._id = other[self._id.name] setattr(self._db._aliased_tables, alias, other) return other def on(self, query): return Expression(self._db, self._db._adapter.dialect.on, self, query) def create_index(self, name, *fields, **kwargs): return self._db._adapter.create_index(self, name, *fields, **kwargs) def drop_index(self, name): return self._db._adapter.drop_index(self, name) class Select(BasicStorage): def __init__(self, db, query, fields, attributes): self._db = db self._tablename = None # alias will be stored here self._rname = self._raw_rname = self._dalname = None self._common_filter = None self._query = query # if false, the subquery will never reference tables from parent scope self._correlated = attributes.pop("correlated", True) self._attributes = attributes self._qfields = list(fields) self._fields = SQLCallableList() self._virtual_fields = [] self._virtual_methods = [] self.virtualfields = [] self._sql_cache = None self._colnames_cache = None fieldcheck = set() for item in fields: if isinstance(item, Field): checkname = item.name field = item.clone() elif isinstance(item, Expression): if item.op != item._dialect._as: continue checkname = item.second field = Field(item.second, type=item.type) else: raise SyntaxError("Invalid field in Select") if db and db._ignore_field_case: checkname = checkname.lower() if checkname in fieldcheck: raise SyntaxError("duplicate field %s in select query" % field.name) fieldcheck.add(checkname) field.bind(self) self.fields.append(field.name) self[field.name] = field self.ALL = SQLALL(self) @property def fields(self): return self._fields def update(self, *args, **kwargs): raise RuntimeError("update() method not supported") def __getitem__(self, key): try: return getattr(self, key) except AttributeError: raise KeyError(key) def __setitem__(self, key, value): self.__dict__[str(key)] = value def __call__(self): adapter = self._db._adapter colnames, sql = self._compile() cache = self._attributes.get("cache", None) if cache and self._attributes.get("cacheable", False): return adapter._cached_select( cache, sql, self._fields, self._attributes, colnames ) return adapter._select_aux(sql, self._qfields, self._attributes, colnames) def __setattr__(self, key, value): if key[:1] != "_" and key in self: raise SyntaxError("Object exists and cannot be redefined: %s" % key) self[key] = value def __iter__(self): for fieldname in self.fields: yield self[fieldname] def __repr__(self): return "