Возникла проблема SQLAlchemy + SQLite «create_function» с представлениями даты и времени - PullRequest
1 голос
/ 10 ноября 2011

У нас есть базы данных sqlite, и даты действительно хранятся в формате Excel (для этого есть веская причина; это стандартное представление нашей системы, и базы данных sqlite могут быть доступны для нескольких языков / систем)

В последние месяцы мы с большим успехом внедряем Python в микс, и SQLAlchemy является его частью.Особенно ценится способность уровня dbapi sqlite3 быстро связывать пользовательские функции Python, где SQLite не хватает данной функции SQL.

Я написал декоратор типа ExcelDateTime, и это прекрасно работает при получении наборов результатов из баз данных sqlite;Python возвращает правильные даты и обратно.

Однако у меня есть реальная проблема с привязкой пользовательских функций python, которые ожидают, что входные параметры будут датами Python;Я бы подумал, что это было то, для чего был bindparam, но я явно что-то упускаю, так как я не могу заставить этот сценарий работать.К сожалению, изменение функций для преобразования из даты-даты в Excel в дату-время Python не является опцией, и также не изменяется представление даты-времени в базе данных, поскольку к ней могут обращаться несколько систем / языков.

Кодниже приведен автономный пример, который можно запустить «как есть», и он представляет проблему.Пользовательская функция «get_month» создается, но завершается сбоем, поскольку получает необработанные данные, а не данные, преобразованные по типу, из столбца «Born».В конце вы можете увидеть, что я пробовал до сих пор, и ошибки, которые он выплевывает ...

Разве то, что я пытаюсь сделать, невозможно?Или есть другой способ гарантировать, что связанная функция получает соответствующий тип python?Это единственная проблема, которую я до сих пор не смог преодолеть, было бы здорово найти решение!

import sqlalchemy.types as types
from sqlalchemy import create_engine, Table, Column, Integer, String, MetaData
from sqlalchemy.sql.expression import bindparam
from sqlalchemy.sql import select, text
from sqlalchemy.interfaces import PoolListener
import datetime

# setup type decorator for excel<->python date conversions
class ExcelDateTime( types.TypeDecorator ):
    impl = types.FLOAT

    def process_result_value( self, value, dialect ):
        lxdays = int( value )
        lxsecs = int( round((value-lxdays) * 86400.0) )
        if lxsecs == 86400:
            lxsecs = 0
            lxdays += 1
        return ( datetime.datetime.fromordinal(lxdays+693594)
               + datetime.timedelta(seconds=lxsecs) )

    def process_bind_param( self, value, dialect ):
        if( value < 200000 ): # already excel float?
            return value
        elif( isinstance(value,datetime.date) ):
            return value.toordinal() - 693594.0
        elif( isinstance(value,datetime.datetime) ):
            date_part = value.toordinal() - 693594.0
            time_part = ((value.hour*3600) + (value.minute*60) + value.second) / 86400.0
            return date_part + time_part  # time part = day fraction

# create sqlite memory db via sqlalchemy
def get_month( dt ):
    return dt.month

class ConnectionFactory( PoolListener ):
    def connect( self, dbapi_con, con_record ):
        dbapi_con.create_function( 'GET_MONTH',1,get_month )

eng = create_engine('sqlite:///:memory:',listeners=[ConnectionFactory()])
eng.dialect.dbapi.enable_callback_tracebacks( 1 ) # show better errors from user functions
meta = MetaData()
birthdays = Table('Birthdays', meta, Column('Name',String,primary_key=True), Column('Born',ExcelDateTime), Column('BirthMonth',Integer))
meta.create_all(eng)
dbconn = eng.connect()
dbconn.execute( "INSERT INTO Birthdays VALUES('Jimi Hendrix',15672,NULL)" )

# demonstrate the type decorator works and we get proper datetimes out
res = dbconn.execute( select([birthdays]) )
tuple(res)
# >>> ((u'Jimi Hendrix', datetime.datetime(1942, 11, 27, 0, 0)),)

# simple attempt (blows up with "AttributeError: 'float' object has no attribute 'month'")
dbconn.execute( text("UPDATE Birthdays SET BirthMonth = GET_MONTH(Born)") )

# more involved attempt( blows up with "InterfaceError: (InterfaceError) Error binding parameter 0 - probably unsupported type")
dbconn.execute( text( "UPDATE Birthdays SET BirthMonth = GET_MONTH(:Born)",
                       bindparams=[bindparam('Born',ExcelDateTime)],
                       typemap={'Born':ExcelDateTime} ),
                Born=birthdays.c.Born )

Большое спасибо.

1 Ответ

1 голос
/ 10 ноября 2011

Вместо того, чтобы позволить Excel / Microsoft диктовать, как вы сохраняете дату / время, вам будет проще и труднее полагаться на стандартный / «очевидный» способ ведения дел.

  1. Обработка объектов в соответствии со стандартами их домена - путь Python (объекты datetime) внутри Python / SQLAlchemy, путь SQL внутри SQLite (собственный тип даты / времени вместо float!).

  2. Используйте API для выполнения необходимого перевода между доменами. (Python общается с SQLite через SQLAlchemy, Python общается с Excel через xlrd / xlwt , Python общается с другими системами, Python - это ваш клей.)

Использование стандартных типов даты / времени в SQLite позволяет писать SQL без использования Python стандартным читаемым способом (WHERE date BETWEEN '2011-11-01' AND '2011-11-02' имеет гораздо больший смысл, чем WHERE date BETWEEN 48560.9999 AND 48561.00001). Это позволяет вам легко перенести ее на другую СУБД (без переписывания всех этих специальных функций), когда ваше приложение / база данных должна расти.

Использование нативных объектов datetime в Python позволяет использовать множество свободно доступных, хорошо протестированных и не EEE (охватывать, расширять, тушить) API-интерфейсы. SQLAlchemy является одним из них.

И я надеюсь, что вы знаете об этой небольшой, но опасной разнице между плавающими датами и временем Excel в Mac и Windows? Кто знает, что один из ваших клиентов в будущем отправит файл Excel с Mac и вылетит из вашего приложения (на самом деле, что еще хуже, они неожиданно заработали миллион долларов от ошибки)?

Поэтому я предлагаю вам использовать xlrd / xlwt при работе с Excel из Python (есть другой пакет для чтения Excel 2007) и позволить SQLALchemy и вашей базе данных использовать стандартные типы datetime. Однако, если вы настаиваете на продолжении сохранения даты и времени как плавающего файла Excel, это может сэкономить вам много времени для повторного использования кода из xlrd / xlwt . Он имеет функции для преобразования объектов Python в данные Excel и наоборот.

РЕДАКТИРОВАТЬ: для ясности ...

У вас нет проблем с чтением из базы данных в Python, потому что у вас есть этот класс, который конвертирует число с плавающей точкой в ​​Python datetime.

У вас есть проблемы при записи в базу данных через SQLAlchemy или при использовании других собственных функций / модулей / расширений Python, потому что вы пытаетесь вызвать нестандартный тип, когда они ожидают стандартную дату / время Python . Тип ExcelDateTime с точки зрения Python - это число с плавающей точкой, а не дата-время.

Хотя Python использует динамическую / утиную типизацию, он все еще строго типизирован . Это не позволит вам делать «глупости / глупости», такие как добавление целых чисел в строку или принудительное использование float для datetime .

Как минимум два способа решения этой проблемы:

  1. Объявите пользовательский тип - Кажется, что вы хотите выбрать путь. К сожалению, это трудный путь . Довольно сложно создать тип с плавающей точкой, который также может выглядеть как datetime . Возможно, да, но требует большого изучения типовых приборов. Извините, вы должны взять документацию на это самостоятельно.

  2. Создание служебных функций - Должно быть проще, ИМХО. Вам нужны 2 функции: а) float_to_datetime () для преобразования данных из базы данных для возврата даты и времени Python и б) datetime_to_float () для преобразования даты и времени Python в Excel с плавающей точкой.

О решении № 2, как я уже говорил, вы можете упростить свою жизнь, повторно используя xldate_from_datetime_tuple () из xlrd / xlwt . Эта функция «Преобразование кортежа даты и времени (год, месяц, день, час, минута, секунда) в значение даты Excel». Установите xlrd, затем перейдите в / path_to_python / lib / site-packages / xlrd. Функция находится в xldate.py - источник хорошо документирован для понимания.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...