[Zope3-checkins] CVS: zopeproducts/psycopgda - __init__.py:1.1 adapter.py:1.1 browser.py:1.1 configure.zcml:1.1

Viktorija Zaksiene ryzaja@codeworks.lt
Thu, 9 Jan 2003 06:39:00 -0500


Update of /cvs-repository/zopeproducts/psycopgda
In directory cvs.zope.org:/tmp/cvs-serv18584/psycopgda

Added Files:
	__init__.py adapter.py browser.py configure.zcml 
Log Message:
Replaced old PsycopgDA directory stucture with new one.
Also make adapter to use new datatype schema.



=== Added File zopeproducts/psycopgda/__init__.py ===
##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
# 
##############################################################################
"""PostgreSQL Database Adapter for Zope 3

$Id: __init__.py,v 1.1 2003/01/09 11:38:56 ryzaja Exp $
"""


=== Added File zopeproducts/psycopgda/adapter.py ===
##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors.
# All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
# 
##############################################################################
"""PostgreSQL Database Adapter for Zope 3

$Id: adapter.py,v 1.1 2003/01/09 11:38:56 ryzaja Exp $
"""

from persistence import Persistent
from zope.app.rdb import ZopeDatabaseAdapter, parseDSN
from zope.app.rdb import ZopeConnection

from datetime import date, time, datetime, timedelta
import psycopg

# These OIDs are taken from pg_types.h from PostgreSQL headers.
# Unfortunatelly psycopg does not export them as constants, and
# we cannot use psycopg.FOO.values because they overlap.
DATE_OID        = 1082
TIME_OID        = 1083
TIMETZ_OID      = 1266
TIMESTAMP_OID   = 1114
TIMESTAMPTZ_OID = 1184
INTERVAL_OID    = 1186
# The following ones are obsolete and we don't handle them
#ABSTIME_OID     = 702
#RELTIME_OID     = 703
#TINTERVAL_OID   = 704

# Date/time parsing functions
def parse_date(s):
    """Parses ISO-8601 compliant dates and returns a tuple (year, month,
    day).

    The following formats are accepted:
        YYYY-MM-DD  (extended format)
        YYYYMMDD    (basic format)
    """
    if len(s) == 8:
        return (int(s[:4]), int(s[4:6]), int(s[6:]))    # YYYYMMDD
    elif len(s) == 10 and s[4] == s[7] == '-':
        return (int(s[:4]), int(s[5:7]), int(s[8:]))    # YYYY-MM-DD
    else:
        raise ValueError, 'invalid date string: %s' % s

def parse_time(s):
    """Parses ISO-8601 compliant times and returns a tuple (hour, minute,
    second).
    
    The following formats are accepted:
        HH:MM:SS.ssss or HHMMSS.ssss
        HH:MM:SS,ssss or HHMMSS,ssss
        HH:MM:SS      or HHMMSS
        HH:MM         or HHMM
        HH
    """
    if len(s) == 2:
        return (int(s[:2]), 0, 0)                       # HH
    elif len(s) == 4:
        return (int(s[:2]), int(s[2:]), 0)              # HHMM
    elif len(s) == 5 and s[2] == ':':
        return (int(s[:2]), int(s[3:]), 0)              # HH:MM
    elif len(s) == 6:
        return (int(s[:2]), int(s[2:4]), int(s[4:]))    # HHMMSS
    elif len(s) == 8 and s[2] == s[5] == ':':
        return (int(s[:2]), int(s[3:5]), int(s[6:]))    # HH:MM:SS
    elif len(s) > 9 and s[2] == s[5] == ':' and s[8] == '.':
        return (int(s[:2]), int(s[3:5]), float(s[6:]))  # HH:MM:SS.ssss
    elif len(s) > 9 and s[2] == s[5] == ':' and s[8] == ',':
        return (int(s[:2]), int(s[3:5]), float(s[6:8] + "." + s[9:]))
                                                        # HH:MM:SS,ssss
    elif len(s) > 7 and s[6] == '.':
        return (int(s[:2]), int(s[2:4]), float(s[4:]))  # HHMMSS.ssss
    elif len(s) > 7 and s[6] == ',':
        return (int(s[:2]), int(s[2:4]), float(s[4:6] + "." + s[7:]))
                                                        # HHMMSS,ssss
    else:
        raise ValueError, 'invalid time string: %s' % s

def parse_tz(s):
    """Parses ISO-8601 timezones and returns the offset east of UTC in
    minutes.
    
    The following formats are accepted:
        +/-HH:MM
        +/-HHMM
        +/-HH
        Z           (equivalent to +0000)
    """
    if s == "Z":
        return 0
    if len(s) in (3, 5, 6) and s[0] in ('+', '-'):
        hoff = int(s[1:3])
        if len(s) > 3:
            moff = int(s[-2:])
            if len(s) == 6 and s[3] != ':':
                raise ValueError, 'invalid time zone: %s' % s
        else:
            moff = 0
        if s[0] == '-':
            return - hoff * 60 - moff
        else:
            return hoff * 60 + moff
    else:
        raise ValueError, 'invalid time zone: %s' % s

def parse_timetz(s):
    """Parses ISO-8601 compliant times that may include timezone information
    and returns a tuple (hour, minute, second, tzoffset).

    tzoffset is the offset east of UTC in minutes.  It will be None if s does
    not include time zone information.

    Formats accepted are those listed in the descriptions of parse_time() and
    parse_tz().  Time zone should immediatelly follow time without intervening
    spaces.
    """
    idx = s.find('+')
    if idx < 0: idx = s.find('-')
    if idx < 0: idx = s.find('Z')
    if idx < 0:
        return parse_time(s) + (None,)
    else:
        return parse_time(s[:idx]) + (parse_tz(s[idx:]),)

def parse_datetime(s):
    """Parses ISO-8601 compliant timestamp and returns a tuple (year, month,
    day, hour, minute, second).

    Formats accepted are those listed in the descriptions of parse_date() and
    parse_time() with ' ' or 'T' used to separate date and time parts.
    """
    idx = s.find('T')
    if idx < 0: idx = s.find(' ')
    if idx < 0:
        raise ValueError, 'time part of datetime missing: %s' % s
    return parse_date(s[:idx]) + parse_time(s[idx + 1:])

def parse_datetimetz(s):
    """Parses ISO-8601 compliant timestamp that may include timezone
    information and returns a tuple (year, month, day, hour, minute, second,
    tzoffset).

    tzoffset is the offset east of UTC in minutes.  It will be None if s does
    not include time zone information.

    Formats accepted are those listed in the descriptions of parse_date() and
    parse_timetz() with ' ' or 'T' used to separate date and time parts.
    """
    idx = s.find('T')
    if idx < 0: idx = s.find(' ')
    if idx < 0:
        raise ValueError, 'time part of datetime missing: %s' % s
    return parse_date(s[:idx]) + parse_timetz(s[idx + 1:])


def parse_interval(s):
    """Parses PostgreSQL interval notation and returns a tuple (years, months,
    days, hours, minutes, seconds).

    Values accepted:
        interval  ::= date
                   |  time
                   |  date time
        date      ::= date_comp
                   |  date date_comp
        date_comp ::= 1 'day'
                   |  number 'days'
                   |  1 'month'
                   |  number 'months'
                   |  1 'year'
                   |  number 'years'
        time      ::= number ':' number
                   |  number ':' number ':' number
                   |  number ':' number ':' number '.' fraction
    """
    years = months = days = 0
    hours = minutes = seconds = 0
    elements = s.split()
    for i in range(0, len(elements) - 1, 2):
        count, unit = elements[i:i+2]
        if unit == 'day' and count == '1':
            days += 1
        elif unit == 'days':
            days += int(count)
        elif unit == 'month' and count == '1':
            months += 1
        elif unit == 'months':
            months += int(count)
        elif unit == 'year' and count == '1':
            years += 1
        elif unit == 'years':
            years += int(count)
        else:
            raise ValueError, 'unknown time interval %s %s' % (count, unit)
    if len(elements) % 2 == 1:
        hours, minutes, seconds = parse_time(elements[-1])
    return (years, months, days, hours, minutes, seconds)


# Type conversions
def _conv_date(s):
    if s:
        return date(*parse_date(s))

def _conv_time(s):
    if s:
        hr, mn, sc = parse_time(s)
        sc, micro = divmod(sc, 1.0)
        micro = round(micro * 1000000)
        return time(hr, mn, sc, micro)

def _conv_timetz(s):
    if s:
        from zope.app.datetimeutils import tzinfo
        hr, mn, sc, tz = parse_timetz(s)
        sc, micro = divmod(sc, 1.0)
        micro = round(micro * 1000000)
        if tz: tz = tzinfo(tz)
        return time(hr, mn, sc, micro, tz)

def _conv_timestamp(s):
    if s:
        y, m, d, hr, mn, sc = parse_datetime(s)
        sc, micro = divmod(sc, 1.0)
        micro = round(micro * 1000000)
        return datetime(y, m, d, hr, mn, sc, micro)

def _conv_timestamptz(s):
    if s:
        from zope.app.datetimeutils import tzinfo
        y, m, d, hr, mn, sc, tz = parse_datetimetz(s)
        sc, micro = divmod(sc, 1.0)
        micro = round(micro * 1000000)
        if tz: tz = tzinfo(tz)
        return datetime(y, m, d, hr, mn, sc, micro, tz)

def _conv_interval(s):
    if s:
        y, m, d, hr, mn, sc = parse_interval(s)
        if (y, m) != (0, 0):
            # XXX: Currently there's no way to represent years and months as
            # timedeltas
            return s
        else:
            return timedelta(days=d, hours=hr, minutes=mn, seconds=sc)


# User-defined types
DATE = psycopg.new_type((DATE_OID,), "ZDATE", _conv_date)
TIME = psycopg.new_type((TIME_OID,), "ZTIME", _conv_time)
TIMETZ = psycopg.new_type((TIMETZ_OID,), "ZTIMETZ", _conv_timetz)
TIMESTAMP = psycopg.new_type((TIMESTAMP_OID,), "ZTIMESTAMP", _conv_timestamp)
TIMESTAMPTZ = psycopg.new_type((TIMESTAMPTZ_OID,), "ZTIMESTAMPTZ",
                                _conv_timestamptz)
INTERVAL = psycopg.new_type((INTERVAL_OID,), "ZINTERVAL", _conv_interval)


dsn2option_mapping = {'host': 'host',
                      'port': 'port',
                      'dbname': 'dbname',
                      'username': 'user',
                      'password': 'password'}


class PsycopgAdapter(ZopeDatabaseAdapter):
    """A PsycoPG adapter for Zope3.

    The following type conversions are performed:

        DATE -> datetime.date
        TIME -> datetime.time
        TIMETZ -> datetime.time
        TIMESTAMP -> datetime.datetime
        TIMESTAMPTZ -> datetime.datetime

    XXX: INTERVAL cannot be represented exactly as datetime.timedelta since
    it might be something like '1 month', which is a variable number of days.
    """
    
    __implements__ = ZopeDatabaseAdapter.__implements__

    def _connection_factory(self):
        """Create a Psycopg DBI connection based on the DSN"""
        conn_info = parseDSN(self.dsn)
        conn_list = []
        for option in dsn2option_mapping:
            if conn_info[option]:
                conn_list.append('%s=%s' %(dsn2option_mapping[option],
                                           conn_info[option]))
        conn_str = ' '.join(conn_list)
        self._registerTypes()
        return psycopg.connect(conn_str)

    def _registerTypes(self):
        """Register type conversions for psycopg"""
        psycopg.register_type(DATE)
        psycopg.register_type(TIME)
        psycopg.register_type(TIMETZ)
        psycopg.register_type(TIMESTAMP)
        psycopg.register_type(TIMESTAMPTZ)
        psycopg.register_type(INTERVAL)



=== Added File zopeproducts/psycopgda/browser.py ===
##############################################################################
#
# Copyright (c) 2002 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""
$Id: browser.py,v 1.1 2003/01/09 11:38:56 ryzaja Exp $
"""
from zope.app.browser.rdb import AdapterAdd


class PsycopgDAAddView(AdapterAdd):
    """Provide a user interface for adding a Psycopg DA"""

    # This needs to be overridden by the actual implementation
    _adapter_factory_id = "psycopgda"



=== Added File zopeproducts/psycopgda/configure.zcml ===
<zopeConfigure
   xmlns='http://namespaces.zope.org/zope'
   xmlns:browser='http://namespaces.zope.org/browser'
>

<content class=".adapter.PsycopgAdapter">
  <factory id="psycopgda"
      permission="zope.Public" />
  <require permission="zope.Public" 
    interface="zope.app.interfaces.rdb.IZopeDatabaseAdapter." />
  <implements interface="zope.app.interfaces.annotation.IAttributeAnnotatable." />
</content>

<browser:view
    name="zopeproducts.psycopgda"
    for="zope.app.interfaces.container.IAdding."
    class=
      "zopeproducts.psycopgda.browser.PsycopgDAAddView"
    permission="zope.Public">

  <browser:page name="+" attribute="add" />
  <browser:page name="action.html" attribute="action" />
</browser:view>

<browser:menuItem menu="add_component"
    for="zope.app.interfaces.container.IAdding."
    title="Psycopg DA" action="zopeproducts.psycopgda"
    description="A PostgreSQL Database Adapter using the Psycopg driver"/>


</zopeConfigure>