import functools
from datetime import datetime, date, time
from decimal import Decimal

from sqlalchemy import Column
from sqlalchemy.orm import Query, CompositeProperty
from sqlalchemy.orm.attributes import InstrumentedAttribute
from sqlalchemy.orm.relationships import RelationshipProperty
from sqlalchemy.ext.hybrid import HYBRID_PROPERTY
from sqlalchemy.ext.associationproxy import ASSOCIATION_PROXY
from sqlalchemy.inspection import inspect
from sqlalchemy_dict import DefaultFormatter


class BaseModel(object):
    __formatter__ = DefaultFormatter

    @classmethod
    def get_column(cls, column):
        if isinstance(column, str):
            mapper = inspect(cls)
            return mapper.columns[column]
        return column

    @classmethod
    def import_value(cls, column, v):
        c = cls.get_column(column)
        if isinstance(c, Column) or isinstance(c, InstrumentedAttribute):
            if c.type.python_type is bool and not isinstance(v, bool):
                return str(v).lower() == 'true'
        return v

    @classmethod
    def prepare_for_export(cls, column, v):
        param_name = column.info.get('dict_key') or cls.__formatter__.export_key(column.key)
        if hasattr(column, 'property') and isinstance(column.property,
                                                      RelationshipProperty) and column.property.uselist:
            result = [c.to_dict() for c in v]

        elif hasattr(column, 'property') and isinstance(column.property, CompositeProperty):
            result = v.__composite_values__()

        elif v is None:
            result = v

        elif isinstance(v, datetime):
            result = cls.__formatter__.export_datetime(v)

        elif isinstance(v, date):
            result = cls.__formatter__.export_date(v)

        elif isinstance(v, time):
            result = cls.__formatter__.export_time(v)

        elif hasattr(v, 'to_dict'):
            result = v.to_dict()

        elif isinstance(v, Decimal):
            result = str(v)

        else:
            result = v

        return param_name, result

    def update_from_dict(self, context):
        for column, value in self.extract_data_from_dict(context):
            setattr(
                self,
                column.key[1:] if column.key.startswith('_') else column.key,
                self.import_value(column, value)
            )

    @classmethod
    def iter_columns(cls, relationships=True, synonyms=True, composites=True, hybrids=True):
        mapper = inspect(cls)
        for k, c in mapper.all_orm_descriptors.items():

            if k == '__mapper__':
                continue

            if c.extension_type == ASSOCIATION_PROXY:
                continue

            if (not hybrids and c.extension_type == HYBRID_PROPERTY) \
                    or (not relationships and k in mapper.relationships) \
                    or (not synonyms and k in mapper.synonyms) \
                    or (not composites and k in mapper.composites):
                continue
            yield getattr(cls, k)

    @classmethod
    def iter_dict_columns(cls, include_readonly_columns=True, include_protected_columns=False, **kw):
        for c in cls.iter_columns(**kw):
            if (not include_protected_columns and c.info.get('protected')) or \
                    (not include_readonly_columns and c.info.get('readonly')):
                continue

            yield c

    @classmethod
    def extract_data_from_dict(cls, context):
        for c in cls.iter_dict_columns(include_protected_columns=True, include_readonly_columns=False):
            param_name = c.info.get('dict_key', cls.__formatter__.export_key(c.key))

            if param_name in context:
                value = context[param_name]
                # Ensuring the python type, and ignoring silently if python type is not specified
                try:
                    c.type.python_type
                except NotImplementedError:
                    yield c, value
                    continue

                if c.type.python_type == datetime:
                    yield c, cls.__formatter__.import_datetime(value)

                elif c.type.python_type == date:
                    yield c, cls.__formatter__.import_date(value)

                elif c.type.python_type == time:
                    yield c, cls.__formatter__.import_time(value)

                else:
                    yield c, value

    def to_dict(self):
        result = {}
        for c in self.iter_dict_columns():
            result.setdefault(*self.prepare_for_export(c, getattr(self, c.key)))
        return result

    @classmethod
    def dump_query(cls, query=None):
        result = []
        for o in query:
            result.append(o.to_dict())
        return result

    @classmethod
    def expose(cls, func):

        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)

            if hasattr(result, 'to_dict'):
                return result.to_dict()

            if isinstance(result, Query):
                return cls.dump_query(result)
            return result

        return wrapper

@@ -549,16 +549,154 @@
 matter%0A%0A
+    @classmethod%0A    def get_dict_key(cls, column):%0A        return column.info.get('dict_key', cls.__formatter__.export_key(column.key))%0A%0A
     @cla
@@ -1260,32 +1260,23 @@
  = c
-olumn.info
+ls
 .get
-('
+_
 dict_key
 ') o
@@ -1275,53 +1275,15 @@
 _key
-') or cls.__formatter__.export_key
 (column
-.key
 )%0A  
@@ -3703,35 +3703,31 @@
 name = c
-.info
+ls
 .get
-('
+_
 dict_key
 ', cls._
@@ -3722,46 +3722,10 @@
 _key
-', cls.__formatter__.export_key(c.key)
+(c
 )%0A%0A 

Add dictionary key generator