/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/Model/postgisdb.py
ViewVC logotype

Annotation of /branches/WIP-pyshapelib-bramz/Thuban/Model/postgisdb.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1636 - (hide annotations)
Fri Aug 22 17:20:29 2003 UTC (21 years, 6 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/postgisdb.py
File MIME type: text/x-python
File size: 12038 byte(s)
* Thuban/Model/postgisdb.py (ConnectionError): New exception class
for exceptions occurring when establishing a Database connection
(PostGISConnection.connect): Catch psycopg.OperationalError during
connects and raise ConnectionError.

* test/test_postgis_db.py (TestPostgisDBExceptions): New class for
tests for database exceptions

1 bh 1605 # Copyright (C) 2003 by Intevation GmbH
2     # Authors:
3     # Martin Mueller <[email protected]>
4     # Bernhard Herzog <[email protected]>
5     #
6     # This program is free software under the GPL (>=v2)
7     # Read the file COPYING coming with the software for details.
8    
9     """Basic interface to a PostGIS database"""
10    
11     from __future__ import generators
12    
13     try:
14     import psycopg
15     except ImportError:
16     psycopg = None
17    
18     import table
19     import wellknowntext
20    
21     from data import SHAPETYPE_POLYGON, SHAPETYPE_ARC, SHAPETYPE_POINT, RAW_WKT
22    
23 bh 1625 def has_postgis_support():
24     """Return whether this Thuban instance supports PostGIS connections
25 bh 1605
26 bh 1625 Having PostGIS support means that the psycopg module can be
27     imported.
28     """
29     return psycopg is not None
30    
31 bh 1631 def psycopg_version():
32     return psycopg.__version__
33    
34 bh 1605 if psycopg is not None:
35     type_map = [(psycopg.STRING, table.FIELDTYPE_STRING),
36     (psycopg.INTEGER, table.FIELDTYPE_INT),
37     (psycopg.FLOAT, table.FIELDTYPE_DOUBLE)]
38    
39 bh 1636
40     class ConnectionError(Exception):
41    
42     """Class for exceptions occurring when establishing a Databse connection"""
43    
44    
45 bh 1605 class PostGISConnection:
46    
47     """Represent a PostGIS database
48    
49     A PostGISConnection instance has the following public attributes:
50    
51     dbname -- The name of the database
52     host, port -- Host and port to connect to
53     user -- The user name to connect as.
54    
55     All of these attributes are strings and may be empty strings to
56     indicate default values.
57     """
58    
59     def __init__(self, dbname, host="", user="", password="", dbtype="",
60     port=""):
61     self.dbname = dbname
62     self.host = host
63     self.port = port
64     self.user = user
65     self.password = password
66     self.dbtype = dbtype
67 bh 1620 self.connect()
68    
69     def connect(self):
70     """Internal: Establish the database connection"""
71 bh 1605 params = []
72     for name in ("host", "port", "dbname", "user", "password"):
73     val = getattr(self, name)
74     if val:
75     params.append("%s=%s" % (name, val))
76 bh 1636 try:
77     self.connection = psycopg.connect(" ".join(params))
78     except psycopg.OperationalError, val:
79     raise ConnectionError(str(val))
80 bh 1605
81     # determine the OID for the geometry type. This is PostGIS
82     # specific.
83     cursor = self.connection.cursor()
84     cursor.execute("SELECT OID, typname FROM pg_type WHERE"
85     +" typname = 'geometry'")
86     row = cursor.fetchone()
87     self.connection.commit()
88     if row is not None:
89     self.geometry_type = row[0]
90     else:
91     raise ValueError("Can't determine postgres type of geometries")
92    
93 bh 1620 def BriefDescription(self):
94     """Return a brief, one-line description of the connection
95    
96     The return value is suitable for a list box of all database
97     connections.
98     """
99     return ("postgis://%(user)s@%(host)s:%(port)s/%(dbname)s"
100     % self.__dict__)
101    
102 bh 1605 def Close(self):
103     """Close the database connection"""
104     self.connection.close()
105    
106     def GeometryTables(self):
107     """Return a list with the names of all tables with a geometry column"""
108     cursor = self.connection.cursor()
109     cursor.execute("SELECT f_table_name FROM geometry_columns;")
110     result = [row[0] for row in cursor.fetchall()]
111     self.connection.commit()
112     return result
113    
114     def cursor(self):
115     """Return a DB API 2.0 cursor for the database"""
116     return self.connection.cursor()
117    
118    
119    
120     class PostGISColumn:
121    
122     """Column description for a PostGISTable"""
123    
124     def __init__(self, name, type, index):
125     self.name = name
126     self.type = type
127     self.index = index
128    
129    
130     class PostGISTable:
131    
132     """A Table in a PostGIS database"""
133    
134     def __init__(self, db, tablename):
135     """Initialize the PostGISTable.
136    
137     The db parameter should be an instance of PostGISConnection and
138     tablename the name of a table in the database represented by db.
139     """
140     self.db = db
141     self.tablename = tablename
142     self.column_map = {}
143     self._fetch_table_information()
144    
145     def _fetch_table_information(self):
146     """Internal: Update information about the table"""
147     self.columns = []
148     cursor = self.db.cursor()
149     cursor.execute("SELECT * FROM %s WHERE 0=1" % self.tablename)
150     description = cursor.description
151    
152     for i in range(len(description)):
153     for pgtyp, tabletyp in type_map:
154     if pgtyp == description[i][1]:
155     col = PostGISColumn(description[i][0], tabletyp, i)
156     break
157     else:
158     if description[i][1] == self.db.geometry_type:
159     self.geometry_column = description[i][0]
160     # No matching table type. Ignore the column.
161     # FIXME: We should at least print a warning about
162     # ignored columns
163     continue
164     self.columns.append(col)
165    
166     for col in self.columns:
167     self.column_map[col.name] = col
168     self.column_map[col.index] = col
169    
170     # Build query string for ReadRowAsDict
171     self.query_stmt = ("SELECT %s from %s"
172     % (", ".join([col.name for col in self.columns]),
173     self.tablename))
174    
175     def Dependencies(self):
176     """Return an empty tuple because a PostGISTable depends on nothing else
177     """
178     return ()
179    
180     def NumColumns(self):
181     return len(self.columns)
182    
183     def Columns(self):
184     return self.columns
185    
186     def Column(self, col):
187     return self.column_map[col]
188    
189     def HasColumn(self, col):
190     return self.column_map.has_key(col)
191    
192     def NumRows(self):
193     cursor = self.db.cursor()
194     cursor.execute("SELECT count(*) FROM %s" % self.tablename)
195     return cursor.fetchone()[0]
196    
197     def ReadRowAsDict(self, row):
198     cursor = self.db.cursor()
199     cursor.execute(self.query_stmt + " LIMIT 1 OFFSET %d" % row)
200     result = {}
201     for col, value in zip(self.columns, cursor.fetchone()):
202     result[col.name] = value
203     return result
204    
205     def ReadValue(self, row, col):
206     cursor = self.db.cursor()
207     cursor.execute("SELECT %s FROM %s LIMIT 1 OFFSET %d" %
208     (self.column_map[col].name, self.tablename, row))
209     return cursor.fetchone()[0]
210    
211     def ValueRange(self, col):
212     cursor = self.db.cursor()
213     name = self.column_map[col].name
214     cursor.execute("SELECT min(%s), max(%s) FROM %s" %
215     (name, name, self.tablename))
216     return tuple(cursor.fetchone())
217    
218     def UniqueValues(self, col):
219     cursor = self.db.cursor()
220     name = self.column_map[col].name
221     cursor.execute("SELECT %s FROM %s GROUP BY %s" %
222     (name, self.tablename, name))
223     return [row[0] for row in cursor.fetchall()]
224    
225     def SimpleQuery(self, left, comparison, right):
226     if comparison not in ("==", "!=", "<", "<=", ">=", ">"):
227     raise ValueError("Comparison operator %r not allowed" % comparison)
228    
229     if comparison == "==":
230     comparison = "="
231    
232     if isinstance(right, PostGISColumn):
233     right_template = right.name
234     params = ()
235     else:
236     right_template = "%s"
237     params = (right,)
238    
239     query = "SELECT gid FROM %s WHERE %s %s %s ORDER BY gid;" \
240     % (self.tablename, left.name, comparison, right_template)
241    
242     cursor = self.db.cursor()
243     cursor.execute(query, params)
244     result = []
245     while 1:
246     row = cursor.fetchone()
247     if row is None:
248     break
249     result.append(row[0])
250     return result
251    
252    
253     class PostGISShape:
254    
255     def __init__(self, shapeid, data):
256     self.shapeid = shapeid
257     self.data = data
258    
259     def compute_bbox(self):
260     """
261     Return the bounding box of the shape as a tuple (minx,miny,maxx,maxy)
262     """
263     xs = []
264     ys = []
265     for part in self.Points():
266     for x, y in part:
267     xs.append(x)
268     ys.append(y)
269     return (min(xs), min(ys), max(xs), max(ys))
270    
271     def ShapeID(self):
272     return self.shapeid
273    
274     def Points(self):
275     return wellknowntext.parse_wkt_thuban(self.data)
276    
277     def RawData(self):
278     return self.data
279    
280    
281     shapetype_map = {"POLYGON": SHAPETYPE_POLYGON,
282     "MULTILINESTRING": SHAPETYPE_ARC,
283     "POINT": SHAPETYPE_POINT}
284    
285    
286     class PostGISShapeStore(PostGISTable):
287    
288     """Shapestore interface to a table in a PostGIS database"""
289    
290     def Table(self):
291     """Return self since a PostGISShapeStore is its own table."""
292     return self
293    
294     def OrigShapeStore(self):
295     """Return None since the PostGISShapeStore is not derived from another
296     """
297     return None
298    
299     def ShapeType(self):
300     """Return the type of the shapes in the shapestore."""
301     cursor = self.db.cursor()
302     cursor.execute("SELECT type FROM geometry_columns WHERE"
303     " f_table_name=%s", (self.tablename,))
304     result = cursor.fetchone()[0]
305     cursor.close()
306     return shapetype_map[result]
307    
308     def RawShapeFormat(self):
309     """Return the raw data format of the shape data.
310    
311     For the PostGISShapeStore this is RAW_WKT.
312     """
313     return RAW_WKT
314    
315     def NumShapes(self):
316     # The number of shapes is the same as the number of rows,
317     # assuming that the geometry can't be NULL.
318     return self.NumRows()
319    
320     def BoundingBox(self):
321     """Return the bounding box of all shapes in the postgis table"""
322     minx = miny = maxx = maxy = None
323     x=[]
324     y=[]
325     cursor = self.db.cursor()
326     try:
327     stmt = ("SELECT AsText(Envelope(%s)) FROM %s;"
328     % (self.geometry_column, self.tablename))
329     cursor.execute(stmt)
330     result = cursor.fetchone()
331     while result:
332     result = result[0]
333     # Here we must do some parsing through the result string
334     # to get the points out of the polygon which representes
335     # the bounding box The first and the last point of a
336     # polygon are identical
337     result = result.split("(")[2]
338     result = result.split(")")[0]
339     points = result.split(",")
340     del points[4] # Remove the last point
341     for point in points:
342     px, py = point.split()
343     x.append(float(px))
344     y.append(float(py))
345     result = cursor.fetchone()
346     finally:
347     cursor.close()
348     if not x:
349     # Empty table
350     return None
351     return (min(x), min(y), max(x), max(y))
352    
353     def Shape(self, shapeid):
354     cursor = self.db.cursor()
355     cursor.execute("SELECT AsText(%s) FROM %s WHERE gid=%d"
356     % (self.geometry_column, self.tablename, shapeid))
357     wkt = cursor.fetchone()[0]
358     cursor.close()
359     return PostGISShape(shapeid, wkt)
360    
361     def ShapesInRegion(self, bbox):
362     """Generate all shapes overlapping the region given by bbox."""
363     # IMPORTANT:This will work for PostGIS < 0.8
364     left, bottom, right, top = bbox
365     geom = (("POLYGON((" + ", ".join(["%f %f"] * 5) + "))")
366     % (left, bottom, left, top, right, top, right, bottom,
367     left, bottom))
368     cursor = self.db.cursor()
369     cursor.execute("SELECT gid, AsText(%s) FROM %s"
370     " WHERE %s && GeometryFromText('%s', -1) ORDER BY gid"
371     % (self.geometry_column, self.tablename,
372     self.geometry_column, geom))
373     while 1:
374     result = cursor.fetchone()
375     if result is None:
376     raise StopIteration
377     yield PostGISShape(result[0], result[1])

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26