[Zope3-checkins] CVS: zopeproducts/pypgsqlda - INSTALL.txt:1.1.2.1 README.txt:1.1.2.1 TODO.txt:1.1.2.1 __init__.py:1.1.1.1.2.1 adapter.py:1.6.2.2 configure.zcml:1.1.1.1.2.1 TODO:NONE geometric.py:NONE
Christian 'Tiran' Heimes
heimes@faho.rwth-aachen.de
Mon, 7 Apr 2003 12:17:32 -0400
Update of /cvs-repository/zopeproducts/pypgsqlda
In directory cvs.zope.org:/tmp/cvs-serv6268
Modified Files:
Tag: tiran-pypgsql_types-branch
__init__.py adapter.py configure.zcml
Added Files:
Tag: tiran-pypgsql_types-branch
INSTALL.txt README.txt TODO.txt
Removed Files:
Tag: tiran-pypgsql_types-branch
TODO geometric.py
Log Message:
* changed directory structure
* changed converter to curry class
* added first release of geometric types
* added NULL type
* added some text files
=== Added File zopeproducts/pypgsqlda/INSTALL.txt ===
=========================
Installation of pyPgSQLDA
=========================
Requirements
------------
* pypgsqlda 2.3 http://pypgsql.sourceforge.net
* Postgres 7.x
* Zope3
Conventions
-----------
'ZOPE_HOME'
Your Zope3 root directory
'$ cmd'
A command typed and executed in a shell (assuming bash on a linux box)
Installation of the package
---------------------------
1. checkout the newest version of pyPgSQLDA from the zope.org cvs repository
* change into your local 'ZOPE_HOME' directory.
* create a directory 'zopeproducts'.
* change into 'zopeprocuts'.
* create an empty '__init__.py' file. This will transform 'zopeproducts' to a python package
* '$ cvs -d :pserver:anonymous@cvs.zope.org:/cvs-repository login' (empty password)
* '$ cvs -z7 -d :pserver:anonymous@cvs.zope.org:/cvs-repository checkout zopeproducts/pypgsqlda'
**Note:** You should use '$ cvs -q up -P -d' in the
'ZOPE_HOME/zopeproducts/pypgsqlda' directory to stick to the newest version
if you already have pyPgSQLDA installed.
Note: You can also check out the whole zopeproducts tree to 'ZOPE_HOME/zopeproducts'
and stay tuned by simple cvs up in the 'ZOPE_HOME'
2. Add the following line to your 'ZOPE_HOME/products.zcml' configuration file::
<include package='zopeproducts.pypgsqlda' />
3. Start Zope3
Adding a SQL connection to Zope 3
---------------------------------
To use an external rdb you must add a sql connection service and a database
adapter and a connection for each database you want to access.
1. Browse to your zope3 webserver
2. Click on 'Manage Local Services' in the upper right corner
3. 'Visit default package'
4. 'Add'
5. Choose 'Service' and click on the 'Add' button
6. Add a 'SQL Connection Service'. You can type a name in the textbox next
to the addd button (e.g. SQLConnService).
7. Make shure that 'Configure' and 'Activate' is checked and submit the form
8. Now browse back to the default package and click on 'Add' again
9. Add a 'pypgsql DA' database adapter. Again you can pick up a name, for
example the database name.
10. Next you have to type a connection uri (also called a dsn). Currently you
have to give a full uri with all option. In most cases it will look like::
dbi://myuser:mypassword@localhost:5432/mydb
**Note:** You must start the Postgres postmaster with the '-i' option to
enable tcp conncetions. See postmaster(1) manual for more informations.
11. You can validate the connction by browsing to the database adapter and
clicking on 'Connect'. If the button changes to 'Disconnect' everything
is just fine, if not consult the debug log of zope.
12. Now it's time to connect the database adapter and the sql connection
service. Browse to the 'Configuration Manager' in the default package
and click on 'Add'.
13. Add a 'Connection'.
14. Type in a name (e.g. mydbConn), change the status to 'Active' and pick up
the database adapter in the 'Component Path' dropdown field.
That's it. :)
-= EOF =-
=== Added File zopeproducts/pypgsqlda/README.txt ===
deprecated types
-------------------
'abstime' : '',
'money' : 'numeric',
'reltime' : '',
'tinverval' : 'interval',
List of data types
Name | Description
-----------------------------+-------------------------------------------------------------------
"SET" | set of tuples
"char" | single character
abstime | absolute, limited-range date and time (Unix system time)
aclitem | access control list
bigint | ~18 digit integer, 8-byte storage
bit | fixed-length bit string
bit varying | variable-length bit string
boolean | boolean, 'true'/'false'
box | geometric box '(lower left,upper right)'
bytea | variable-length string, binary values escaped
character | char(length), blank-padded string, fixed storage length
character varying | varchar(length), non-blank-padded string, variable storage length
cid | command identifier type, sequence in transaction id
cidr | network IP address/netmask, network address
circle | geometric circle '(center,radius)'
date | ANSI SQL date
double precision | double-precision floating point number, 8-byte storage
inet | IP address/netmask, host address, netmask optional
int2vector | array of 16 int2 integers, used in system tables
integer | -2 billion to 2 billion integer, 4-byte storage
interval | @ <number> <units>, time interval
line | geometric line '(pt1,pt2)'
lseg | geometric line segment '(pt1,pt2)'
macaddr | XX:XX:XX:XX:XX:XX, MAC address
money | $d,ddd.cc, money
name | 31-character type for storing system identifiers
numeric | numeric(precision, decimal), arbitrary precision number
oid | object identifier(oid), maximum 4 billion
oidvector | array of 16 oids, used in system tables
path | geometric path '(pt1,...)'
point | geometric point '(x, y)'
polygon | geometric polygon '(pt1,...)'
real | single-precision floating point number, 4-byte storage
refcursor | reference cursor (portal name)
regproc | registered procedure
reltime | relative, limited-range time interval (Unix delta time)
smallint | -32 thousand to 32 thousand, 2-byte storage
smgr | storage manager
text | variable-length string, no limit specified
tid | (Block, offset), physical location of tuple
time with time zone | hh:mm:ss, ANSI SQL time
time without time zone | hh:mm:ss, ANSI SQL time
timestamp with time zone | date and time with time zone
timestamp without time zone | date and time
tinterval | (abstime,abstime), time interval
unknown |
xid | transaction id
=== Added File zopeproducts/pypgsqlda/TODO.txt ===
* support for postgres arrays
* testing, testing, testing
* documentation
Bugs in pypgsql
******************
no typechar for timetz
http://sourceforge.net/tracker/index.php?func=detail&aid=708013&group_id=16528&atid=316528
interval does not support intervals longer then a few days:
http://sourceforge.net/tracker/index.php?func=detail&aid=708002&group_id=16528&atid=116528
=== zopeproducts/pypgsqlda/__init__.py 1.1.1.1 => 1.1.1.1.2.1 ===
--- zopeproducts/pypgsqlda/__init__.py:1.1.1.1 Mon Mar 17 12:17:18 2003
+++ zopeproducts/pypgsqlda/__init__.py Mon Apr 7 12:17:31 2003
@@ -1,2 +1,17 @@
-# make this a python package
+##############################################################################
+#
+# 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 using pyPgSQL >= 2.3
+$Id$
+"""
=== zopeproducts/pypgsqlda/adapter.py 1.6.2.1 => 1.6.2.2 ===
--- zopeproducts/pypgsqlda/adapter.py:1.6.2.1 Fri Mar 21 17:21:31 2003
+++ zopeproducts/pypgsqlda/adapter.py Mon Apr 7 12:17:31 2003
@@ -16,16 +16,15 @@
$Id$
"""
-from pyPgSQL import PgSQL
-
from zope.app.rdb import ZopeDatabaseAdapter, parseDSN, identity
+from pyPgSQL import PgSQL
+from pyPgSQL.PgSQL import PgArray
-from geometric import Point2d
+import pgtypes
import string
-
-import datetime
from zope.app import datetimeutils
+import datetime
class pypgsqlAdapter(ZopeDatabaseAdapter):
@@ -53,15 +52,42 @@
def getConverter(self, typechar):
if converter_mapping.has_key(str(typechar)):
- return converter_mapping[str(typechar)]
+ return Converter(converter_mapping[str(typechar)])
else:
- return identity
+ # XXX: I don't know how to handle this, so convert it to a
+ # unicode string or a (nested) list of unicode string if it's
+ # a PgArray
+ return Converter(convertUnicode)
# helpers
-def _isPgArray(data):
- """Check if data is a postgres array"""
- return isinstance(data, PgArray)
+NULL = pgtypes.Null()
+
+class Converter(object):
+ """
+ """
+ def __init__(self, cfun):
+ self.cfun = cfun
+
+ def isPgArray(self, data):
+ """Check if data is a postgres array"""
+ return isinstance(data, PgArray)
+
+ def convertArray(self, data):
+ """Converts postgres arrays
+
+ FYI: postgres arrays are homogen and could be nested
+ """
+ return map(self.__class__(self.cfun), data)
+
+ def __call__(self, data):
+ if not data: return NULL
+ if self.isPgArray(data): return self.convertArray(data)
+ retval = self.cfun(data)
+ #import pdb;
+ #pdb.set_trace()
+ return retval
+
def _sec2my(sec):
""" Return second and mikro for a float second"""
@@ -72,141 +98,86 @@
# Converters
-def convertINet(data):
- """Converts inet and cidr to an nested tuple
-
- Return a nested tuple of the form (hostaddress, netmask) where
- hostaddress itself is a tuple of ints and netmask is an int.
-
- FYI: cidr and inet can handle only ipv4 (12 bytes)
-
- XXX: make this a special type
- """
- addr = unicode(data).split('/')
- if len(addr) == 2:
- addr[1] = string.atoi(addr[1])
- else:
- addr.append(32)
- return (tuple([string.atoi(a) for a in addr[0].split('.')]), addr[1])
+
+# *********************************
+# *** byte and string types ***
+# *********************************
def convertBool(data):
- """Returns a boolean as normal python boolean."""
- return not not data
-
-def convertSmallInt(data):
- """Converts a string containing a small int (int2) to int"""
- return string.atoi(unicode(data))
-
-
-class GeometricError(Exception): pass
-
-import re
-# matching an open path e.g. [(0,0),(1,1)]
-isOpenPath = re.compile(r"^\[\(.*\)\]$")
-
-# matching a closed path e.g. ((0,0),(1,1))
-isClosedPath = re.compile(r"^\(\(.*\)\)$")
-
-# group a circle e.g. <(1,1),0.5> into (1.0,1.0) as the middle point
-# and 0.5 as the radius
-groupCircle = re.compile(r"^<(\([\d.]+,[\d.]+\)),([\d.]+)>$")
-
-# group a single point e.g. (0,1) into 0 as the x value and 1 as the y value
-groupPoint = re.compile(r"^\(([\d.]+),([\d.]+)\)$")
-
-# group a path or a box e.g.((0,0),(1,1)) or [(0,0),(1,1)] into a
-# list of points
-groupPointList = re.compile(r"(\([\d.]+,[\d.]+\))")
-
-def convertPoint(data):
- """Returns tuple (x,y) coordinate of floats
+ """Returns a boolean as an ordenary python boolean.
- XXX: make this a special type
+ XXX postgres NULL is neither True nor False ... how to handle this?
"""
- point = groupPoint.match(unicode(data)).groups()
- if len(point) != 2:
- raise GeometricError, u"'%s' has wrong size" % data
- return tuple([string.atof(p) for p in point])
+ return not not data
-def convertBox(data):
- """Returns nested list upper right and lower left point
+def convertBlob(data):
+ """Convert a bytea or binary large object (BLOB) to a bytesafe string
- XXX: make this a special type
+ XXX test, if blobs can be nested in PgArrays
+ XXX test, if str is ok
"""
- box = groupPointList.findall(unicode(data))
- if len(box) != 2:
- raise GeometricError, u"'%s' has wrong size" % data
- return tuple([convertPoint(p) for p in box])
+ return str(data)
-def convertCircle(data):
- """Returns tuple ((x,y), radius) of floats
-
- XXX: make this a special type
+def convertBit(data):
"""
- circle = groupCircle.match(unicode(data)).groups()
- if len(circle) != 2:
- raise GeometricError, u"'%s' has wrong size" % data
- return (convertPoint(circle[0]), string.atof(circle[1]))
-
-def convertLine(data):
- """Returns tuple of two points, which defines the infinite line
-
- XXX: make this a special type
"""
- line = groupPointList.findall(unicode(data))
- if len(line) != 2:
- raise GeometricError, u"'%s' has wrong size" % data
- return tuple([convertPoint(p) for p in line])
+ return str(data)
-def convertLSeg(data):
- """Returns list of two points
-
- XXX: make this a special type
- """
- lseg = groupPointList.findall(unicode(data))
- if len(lseg) != 2:
- raise GeometricError, u"'%s' has wrong size" % data
- return [convertPoint(p) for p in lseg]
+def convertUnicode(data):
+ """Return a unicode string"""
+ return unicode(data)
-def convertPath(data):
- """Returns list or tuple of n points, tuple is used for a closed path
-
- XXX: make this a special type
- """
- if isOpenPath.match(unicode(data)):
- return [convertPoint(p)
- for p in groupPointList.findall(unicode(data))]
- elif isClosedPath.match(unicode(data)):
- return tuple([convertPoint(p)
- for p in groupPointList.findall(unicode(data))])
- else:
- raise GeometricError, u"'%s' has wrong format" % data
-def convertPolygon(data):
- """Returns tuple of n points
-
- XXX: make this a special type
- """
- return tuple([convertPoint(p) for p in groupPointList.findall(unicode(data))])
+# *********************************
+# *** numbers ***
+# *********************************
+
+def convertSmallInt(data):
+ """Converts a string containing a small int (int2) to int"""
+ return string.atoi(unicode(data))
+
+def convertInt(data):
+ """Return an int"""
+ return int(data)
+
+def convertLong(data):
+ """Return a long"""
+ return long(data)
+
+def convertFloat(data):
+ """Return a float"""
+ return long(data)
+
+
+# *********************************
+# *** time types ***
+# *********************************
def convertTime(data):
"""Converts a mxDateTime.time object to a datetime.time object"""
# XXX: day?
- if not data: return None
d, h, min, sec = data.tuple()
sec, my = _sec2my(sec)
return datetime.time(h, min, sec, my)
+def convertTimeTZ(data):
+ """Converts a mxDateTime.time object to a datetime.time object with timezone"""
+ # XXX: day?
+ d, h, min, sec = data.tuple()
+ sec, my = _sec2my(sec)
+ tzoffset = data.gmtoffset().minutes
+ if tzoffset: tz = datetimeutils.tzinfo(tzoffset)
+ else: tz = datetimeutils.tzinfo(0)
+ return datetime.time(h, min, sec, my, tz)
+
def convertTimestamp(data):
"""Converts a mxDateTime.datetime object to a datetime.datetime object"""
- if not data: return None
y, m, d, h, min, sec, dayOfWeek, dayOfYear, dst = data.tuple()
sec, my = _sec2my(sec)
return datetime.datetime(y, m, d, h, min, sec, my)
def convertTimestampTZ(data):
"""Converts a mxDateTime.time object to a datetime.time object with timezone"""
- if not data: return None
y, m, d, h, min, sec, dayOfWeek, dayOfYear, dst = data.tuple()
sec, my = _sec2my(sec)
# time zone offset in minutes
@@ -219,7 +190,7 @@
"""Converts a mxDateTime.DateTimeDelta object to a datetime.timedelta object
XXX BUG BUG BUG
- Warning: mxDateTime.DateTimeDelta returns wrong data for large intervals
+ Warning: pyPgSQL returns wrong data for large intervals
for example 20 years are 20 days!
XXX BUG BUG BUG
"""
@@ -227,6 +198,52 @@
sec, my = _sec2my(sec)
return datetime.timedelta(days=days, hours=hours, minutes=min,
seconds=sec, microseconds=my)
+
+def convertPoint(data):
+ return pgtypes.Point2d(data)
+
+def convertBox(data):
+ return pgtypes.Box2d(data)
+
+def convertCircle(data):
+ return pgtypes.Circle2d(data)
+
+def convertLSeg(data):
+ return pgtypes.ClosedPath(data)
+
+def convertLine(data):
+ return pgtypes.OpenPath(data)
+
+def convertPolygon(data):
+ return pgtypes.ClosedPath(data)
+
+def convertPath(data):
+ if pgtypes.isOpenPath(data):
+ return pgtypes.OpenPath(data)
+ if pgtypes.isClosedPath(data):
+ return pgtypes.ClosedPath(data)
+ raise TypeError
+
+def convertINet(data):
+ return convertUnicode(data)
+
+def convertAclitem(data):
+ """
+ unnested PgArray with string 'key=val'
+ """
+ retval = {}
+ for item in data:
+ key, val = unicode(item).split('=')
+ retval[key] = val
+ return retval
+
+def convertVector(data):
+ """
+ int int .... NOT PgArray
+ """
+ if not data: return None
+ return tuple([ int(item) for item in data.split(' ')])
+
def debug(data):
"""XXX used for debugging"""
@@ -238,53 +255,56 @@
# http://www.cs.nott.ac.uk/TSG/manuals/databases/postgres/user/x774.htm#AEN1354
# http://www.postgresql.org/docs/view.php?version=7.2&idoc=0&file=datatype.html
# types from pypgsql/libpqmodule.c
+# or see postgres src include/catalog/pg_type.h, interfaces/ecpg/lib/pg_type.h
+
converter_mapping = {
- 'abstime' : unicode, # PG_ABSTIME XXXtime (obsolet)
- 'aclitem' : unicode, # PG_ACLITEM S unicode XXX PgArray
- 'blob' : string, # PG_BLOB string
+ 'abstime' : convertUnicode, # PG_ABSTIME XXXtime (obsolet)
+ 'aclitem' : convertAclitem, # PG_ACLITEM S
+ 'blob' : convertBlob, # PG_BLOB string
'bool' : convertBool, # PG_BOOL boolean
'box' : convertBox, # PG_BOX geometric.box
- 'char' : identity, # PG_BPCHAR identity XXX ??
- 'bytea' : string, # PG_BYTEA string
- 'money' : float, # PG_CASH float XXX: special?
+ 'char' : convertUnicode, # PG_BPCHAR XXX
+ 'bytea' : convertBlob, # PG_BYTEA string
+ 'money' : convertFloat, # PG_CASH float XXX: special?
'cid' : debug, # PG_CID S unicode XXX number?
# need more information! could be connection id
'cidr' : convertINet, # PG_CIDR network XXX
'circle' : convertCircle, # PG_CIRCLE geometric.circle
'date' : convertTimestamp, # PG_DATE datetime.date
- 'float4' : identity, # PG_FLOAT4 float
- 'float' : identity, # PG_FLOAT8 float
+ 'float4' : convertFloat, # PG_FLOAT4 float
+ 'float' : convertFloat, # PG_FLOAT8 float
'inet' : convertINet, # PG_INET network XXX
'int2' : convertSmallInt, # PG_INT2 integer
- 'int2vector' : unicode, # PG_INT2VECTOR S unicode XXX PgArray of int2
- 'integer' : identity, # PG_INT4 integer
- 'bigint' : long, # PG_INT8 longint
+ 'int2vector' : convertVector, # PG_INT2VECTOR S
+ 'integer' : convertInt, # PG_INT4 integer
+ 'bigint' : convertLong, # PG_INT8 longint
'interval' : convertInterval, # PG_INTERVAL datetime.delta
'line' : convertLine, # PG_LINE geometric.line (XXX not implemented in pgsql 7.2)
'lseg' : convertLSeg, # PG_LSEG geometric.linesegment
- 'macaddr' : unicode, # PG_MACADDR unicode
- 'name' : identity, # PG_NAME S identity == unicode
- 'numeric' : float, # PG_NUMERIC float XXX: special?
+ 'macaddr' : convertUnicode, # PG_MACADDR unicode
+ 'name' : convertUnicode, # PG_NAME S identity == unicode
+ 'numeric' : convertFloat, # PG_NUMERIC float XXX: special?
'oid' : debug, # PG_OID S unicode XXX: what? is NOT oid in pgpPgAdmin, see rowid
- 'oidvector' : unicode, # PG_OIDVECTOR S unicode XXX: PgArray
+ 'oidvector' : convertVector, # PG_OIDVECTOR S
'path' : convertPath, # PG_PATH geometric.path
'point' : convertPoint, # PG_POINT geometric.point
'polygon' : convertPolygon, # PG_POLYGON geometric.polygon
- 'refcursor' : unicode, # PG_REFCURSOR S unicode XXX: what? identitiy?
- 'regproc' : unicode, # PG_REGPROC S unicode XXX: what? identitiy?
- 'reltime' : unicode, # PG_RELTIME XXX datetime.delta (obsolet)
- 'rowid' : long, # PG_ROWID S longint; rowid is oid in pgpPgAdmin
- 'text' : identity, # PG_TEXT unicode
+ 'refcursor' : convertUnicode, # PG_REFCURSOR S unicode XXX: what? identitiy?
+ 'regproc' : convertUnicode, # PG_REGPROC S unicode XXX: what? identitiy?
+ 'reltime' : convertUnicode, # PG_RELTIME XXX datetime.delta (obsolet)
+ 'rowid' : convertLong, # PG_ROWID S longint; rowid is oid in pgpPgAdmin
+ 'text' : convertUnicode, # PG_TEXT unicode
'tid' : debug, # PG_TID S unicode XXX number? what?
# need more information, could be transaction id
'time' : convertTime, # PG_TIME datetime.time
+ 'timetz' : convertTimeTZ, # XXX not supported by pyPgSQL, only found in postgres sources
'timestamp' : convertTimestamp, # PG_TIMESTAMP datetime.datetime
'timestamptz' : convertTimestampTZ, # PG_TIMESTAMPTZ datetime.datetime
- 'tinterval' : unicode, # PG_TINTERVAL XXX datetime.delta (obsolet)
- 'unknown' : unicode, # PG_UNKNOWN S? unicode (unknown), XXX what?
- 'varbit' : unicode, # PG_VARBIT XXX see zpbit!
- 'varchar' : identity, # PG_VARCHAR unicode
- 'xid' : long, # PG_XID S xid is a longint [was: unicode]
- 'zpbit' : unicode # PG_ZPBIT unicode XXX: pgsql returns string
+ 'tinterval' : convertUnicode, # PG_TINTERVAL XXX datetime.delta (obsolet)
+ 'unknown' : convertUnicode, # PG_UNKNOWN S? unicode (unknown), XXX what?
+ 'varbit' : convertBit, # PG_VARBIT XXX see zpbit!
+ 'varchar' : convertUnicode, # PG_VARCHAR unicode
+ 'xid' : convertLong, # PG_XID S xid is a longint [was: unicode]
+ 'zpbit' : convertBit, # PG_ZPBIT unicode XXX: pgsql returns string
# make int?, this would kill all leading zeros!
}
=== zopeproducts/pypgsqlda/configure.zcml 1.1.1.1 => 1.1.1.1.2.1 ===
--- zopeproducts/pypgsqlda/configure.zcml:1.1.1.1 Mon Mar 17 12:17:18 2003
+++ zopeproducts/pypgsqlda/configure.zcml Mon Apr 7 12:17:31 2003
@@ -12,10 +12,10 @@
</content>
<browser:view
- name="zopeproducts.pypgsqlda"
+ name="zopeproducts.pypgsqlda_branch"
for="zope.app.interfaces.container.IAdding."
class=
- "zopeproducts.pypgsqlda.browser.pypgsqlDAAddView"
+ "zopeproducts.pypgsqlda_branch.browser.pypgsqlDAAddView"
permission="zope.Public">
<browser:page name="+" attribute="add" />
@@ -24,6 +24,6 @@
<browser:menuItem menu="add_component"
for="zope.app.interfaces.container.IAdding."
- title="pypgsql DA" action="zopeproducts.pypgsqlda"
+ title="pypgsql DA" action="zopeproducts.pypgsqlda_branch"
description="A PostgreSQL Database Adapter using the pypgsql driver"/>
</zopeConfigure>
=== Removed File zopeproducts/pypgsqlda/TODO ===
=== Removed File zopeproducts/pypgsqlda/geometric.py ===