/[thuban]/branches/WIP-pyshapelib-bramz/test/postgissupport.py
ViewVC logotype

Annotation of /branches/WIP-pyshapelib-bramz/test/postgissupport.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2471 - (hide annotations)
Thu Dec 16 14:19:21 2004 UTC (20 years, 2 months ago) by bh
Original Path: trunk/thuban/test/postgissupport.py
File MIME type: text/x-python
File size: 28770 byte(s)
Make the test suite work with PostGIS 0.8.2 and PostgreSQL 7.4

* test/postgissupport.py (find_postgis_sql): Different postgis
versions put the postgis.sql file into slightly different places
so we have to look in both.  The updated doc string describes this
is more detail.

* test/test_postgis_db.py
(TestPostGISSpecialCases.test_column_name_quoting): The return
value of UniqueValues is unsorted, so it has to be sorted for
comparison.

1 bh 2057 # Copyright (C) 2003, 2004 by Intevation GmbH
2 bh 1605 # Authors:
3     # Bernhard Herzog <[email protected]>
4     #
5     # This program is free software under the GPL (>=v2)
6     # Read the file COPYING coming with the software for details.
7    
8     """Support module for tests that use a live PostGIS database"""
9    
10     __version__ = "$Revision$"
11     # $Source$
12     # $Id$
13    
14     import sys
15     import os
16     import time
17     import popen2
18     import shutil
19     import traceback
20 bh 2460 import re
21 bh 1605
22     import support
23    
24     try:
25     import psycopg
26     except ImportError:
27     psycopg = None
28    
29     #
30     # Helper code
31     #
32    
33     def run_config_script(cmdline):
34     """Run command cmdline and return its stdout or none in case of errors"""
35     pipe = os.popen(cmdline)
36     result = pipe.read()
37     if pipe.close() is not None:
38     raise RuntimeError('Command %r failed' % cmdline)
39     return result
40    
41     def run_command(command, outfilename = None):
42     """Run command as a subprocess and send its stdout and stderr to outfile
43    
44     The subprocess is run synchroneously so the function returns once
45     the subprocess has termninated. If the process' exit code is not
46     zero raise a RuntimeError.
47    
48     If outfilename is None stdout and stderr are still captured but they
49     are ignored and not written to any file.
50     """
51     proc = popen2.Popen4(command)
52     proc.tochild.close()
53     output = proc.fromchild.read()
54     status = proc.wait()
55     if outfilename is not None:
56     outfile = open(outfilename, "w")
57     outfile.write(output)
58     outfile.close()
59     if not os.WIFEXITED(status) or os.WEXITSTATUS(status) != 0:
60     if outfilename:
61     message = "see %s" % outfilename
62     else:
63     message = output
64     raise RuntimeError("command %r exited with code %d.\n%s"
65     % (command, status, message))
66    
67    
68     def run_boolean_command(command):
69     """
70     Run command as a subprocess silently and return whether it ran successfully
71    
72     Silently means that all output is captured and ignored. The exit
73     status is true if the command ran successfull, i.e. it terminated by
74     exiting and returned as zero exit code and false other wise
75     """
76     try:
77     run_command(command, None)
78     return 1
79     except RuntimeError:
80     pass
81     return 0
82    
83    
84     #
85     # PostgreSQL and database
86     #
87    
88     class PostgreSQLServer:
89    
90     """A PostgreSQL server
91    
92     Instances of this class represent a PostgreSQL server with postgis
93     extensions run explicitly for the test cases. Such a server has its
94     own database directory and its own directory for the unix sockets so
95     that it doesn't interfere with any other PostgreSQL server already
96     running on the system.
97     """
98    
99     def __init__(self, dbdir, port, postgis_sql, socket_dir):
100     """Initialize the PostgreSQLServer object
101    
102     Parameters:
103    
104     dbdir -- The directory for the databases
105     port -- The port to use
106     postgis_sql -- The name of the file with the SQL statements to
107     initialize a database for postgis.
108     socket_dir -- The directory for the socket files.
109    
110     When connecting to the database server use the port and host
111     instance variables.
112     """
113     self.dbdir = dbdir
114     self.port = port
115     self.postgis_sql = postgis_sql
116     self.socket_dir = socket_dir
117    
118     # For the client side the socket directory can be used as the
119 bh 1634 # host if the name starts with a slash.
120 bh 1605 self.host = os.path.abspath(socket_dir)
121    
122 bh 1634 # name and password for the admin and an unprivileged user
123     self.admin_name = "postgres"
124     self.admin_password = "postgres"
125     self.user_name = "observer"
126     self.user_password = "telescope"
127    
128 bh 1605 # Map db names to db objects
129     self.known_dbs = {}
130    
131     def createdb(self):
132     """Create the database in dbdir and start the server.
133    
134     First check whether the dbdir already exists and if necessary
135     stop an already running postmaster and remove the dbdir
136     directory completely. Then create a new database cluster in the
137     dbdir and start a postmaster.
138     """
139     if os.path.isdir(self.dbdir):
140     if self.is_running():
141     self.shutdown()
142     shutil.rmtree(self.dbdir)
143     os.mkdir(self.dbdir)
144    
145 bh 1634 run_command(["initdb", "-D", self.dbdir, "-U", self.admin_name],
146 bh 1605 os.path.join(self.dbdir, "initdb.log"))
147    
148     extra_opts = "-p %d" % self.port
149     if self.socket_dir is not None:
150     extra_opts += " -k %s" % self.socket_dir
151     run_command(["pg_ctl", "-D", self.dbdir,
152     "-l", os.path.join(self.dbdir, "logfile"),
153     "-o", extra_opts, "start"],
154     os.path.join(self.dbdir, "pg_ctl-start.log"))
155     # the -w option of pg_ctl doesn't work properly when the port is
156     # not the default port, so we have to implement waiting for the
157     # server ourselves
158     self.wait_for_postmaster()
159    
160 bh 1634 self.alter_user(self.admin_name, self.admin_password)
161     self.create_user(self.user_name, self.user_password)
162    
163 bh 1605 def wait_for_postmaster(self):
164     """Return when the database server is running
165    
166     Internal method to wait until the postmaster process has been
167     started and is ready for client connections.
168     """
169     max_count = 60
170     count = 0
171     while count < max_count:
172     try:
173     run_command(["psql", "-l", "-p", str(self.port),
174 bh 1634 "-h", self.host, "-U", self.admin_name],
175 bh 1605 os.path.join(self.dbdir, "psql-%d.log" % count))
176 bh 1634 except RuntimeError:
177     pass
178 bh 1605 except:
179 bh 1634 traceback.print_exc()
180 bh 1605 else:
181     break
182     time.sleep(0.5)
183     count += 1
184     else:
185     raise RuntimeError("postmaster didn't start")
186    
187     def is_running(self):
188 bh 2459 """Return whether a postmaster process is running on self.dbdir
189 bh 1605
190 bh 2459 This method runs pg_ctl status on the dbdir and returns True if
191     that command succeeds and False otherwise.
192    
193     Note that it is possible that this method returns true even if
194     the PostgreSQLServer instance has just been created and
195     createdb() has not been called yet. This can happen, for
196     instance, if the server has been started manually for debugging
197     purposes after a test suite run.
198 bh 1605 """
199     return run_boolean_command(["pg_ctl", "-D", self.dbdir, "status"])
200    
201     def shutdown(self):
202     """Stop the postmaster running for self.dbdir"""
203     run_command(["pg_ctl", "-m", "fast", "-D", self.dbdir, "stop"],
204     os.path.join(self.dbdir, "pg_ctl-stop.log"))
205    
206 bh 2106 def new_postgis_db(self, dbname, tables = None, reference_systems = None,
207     views = None):
208 bh 1605 """Create and return a new PostGISDatabase object using self as server
209     """
210 bh 2057 db = PostGISDatabase(self, self.postgis_sql, dbname, tables = tables,
211 bh 2106 reference_systems = reference_systems,
212     views = views)
213 bh 1605 db.initdb()
214     self.known_dbs[dbname] = db
215     return db
216    
217 bh 2106 def get_static_data_db(self, dbname, tables, reference_systems, views):
218 bh 1605 """Return a PostGISDatabase for a database with the given static data
219    
220     If no databasse of the name dbname exists, create a new one via
221     new_postgis_db and upload the data.
222    
223     If a database of the name dbname already exists and uses the
224     indicated data, return that. If the already existing db uses
225     different data raise a value error.
226    
227 bh 2057 If the database doesn't exist, create a new one via
228     self.new_postgis_db.
229    
230     The parameters tables and reference_systems have the same
231     meaning as for new_postgis_db.
232 bh 1605 """
233     db = self.known_dbs.get(dbname)
234     if db is not None:
235 bh 2106 if db.has_data(tables, reference_systems, views):
236 bh 1605 return db
237     raise ValueError("PostGISDatabase named %r doesn't have tables %r"
238     % (dbname, tables))
239 bh 2106 return self.new_postgis_db(dbname, tables, reference_systems, views)
240 bh 1605
241     def get_default_static_data_db(self):
242     dbname = "PostGISStaticTests"
243 bh 2057 srids = [(1, "proj=longlat datum=WGS84")]
244 bh 1662 tables = [
245     # Direct copies of the shapefiles. The shapeids are exactly
246 bh 2057 # the same, except where changed with "gid_offset", of
247 bh 2096 # course. Note that the test implementation requires that
248     # all the landmard tables use an gid_offset of 1000.
249 bh 1662 ("landmarks", os.path.join("..", "Data", "iceland",
250     "cultural_landmark-point.shp"),
251     [("gid_offset", 1000)]),
252     ("political", os.path.join("..", "Data", "iceland",
253 bh 1605 "political.shp")),
254 bh 1662 ("roads", os.path.join("..", "Data", "iceland",
255     "roads-line.shp")),
256 bh 1656
257 bh 1662 # The polygon data as a MULTIPOLYGON geometry type
258     ("political_multi", os.path.join("..", "Data", "iceland",
259 bh 1656 "political.shp"),
260 bh 1662 [("force_wkt_type", "MULTIPOLYGON")]),
261 bh 2057
262 bh 2096 # Copy of landmarks but using an srid != -1
263 bh 2057 ("landmarks_srid", os.path.join("..", "Data", "iceland",
264     "cultural_landmark-point.shp"),
265     [("gid_offset", 1000),
266     ("srid", 1)]),
267 bh 2096
268     # Copy of landmarks with a gid column called "point_id" instead
269     # of "gid" and using an srid != -1.
270     ("landmarks_point_id", os.path.join("..", "Data", "iceland",
271     "cultural_landmark-point.shp"),
272     [("gid_offset", 1000),
273     ("srid", 1),
274     ("gid_column", "point_id")]),
275 bh 1662 ]
276 bh 2106 views = [("v_landmarks", "SELECT * FROM landmarks_point_id")]
277     return self.get_static_data_db(dbname, tables, srids, views)
278 bh 1605
279 bh 1634 def connection_params(self, user):
280     """Return the connection parameters for the given user
281 bh 1605
282 bh 1634 The return value is a dictionary suitable as keyword argument
283     list to PostGISConnection. The user parameter may be either
284     'admin' to connect as admin or 'user' to connect as an
285     unprivileged user.
286     """
287     return {"host": self.host, "port": self.port,
288     "user": getattr(self, user + "_name"),
289     "password": getattr(self, user + "_password")}
290 bh 1605
291 bh 1634 def connection_string(self, user):
292     """Return (part of) the connection string to pass to psycopg.connect
293 bh 1605
294 bh 1634 The string contains host, port, user and password. The user
295     parameter must be either 'admin' or 'user', as for
296     connection_params.
297     """
298     params = []
299     for key, value in self.connection_params(user).items():
300     # FIXME: this doesn't do quiting correctly but that
301     # shouldn't be much of a problem (people shouldn't be using
302     # single quotes in filenames anyway :) )
303     params.append("%s='%s'" % (key, value))
304     return " ".join(params)
305    
306     def execute_sql(self, dbname, user, sql):
307 bh 2460 """Execute the sql statament and return a result for SELECT statements
308 bh 1634
309     The user parameter us used as in connection_params. The dbname
310 bh 2460 parameter must be the name of a database in the cluster. The
311     sql parameter is the SQL statement to execute as a string. If
312     the string starts with 'select' (matched case insensitively) the
313     first row of the result will be returned. Otherwise the return
314     value is None.
315 bh 1634 """
316     conn = psycopg.connect("dbname=%s " % dbname
317     + self.connection_string(user))
318     cursor = conn.cursor()
319     cursor.execute(sql)
320 bh 2460 if sql.lower().startswith("select"):
321     row = cursor.fetchone()
322     else:
323     row = None
324 bh 1634 conn.commit()
325     conn.close()
326 bh 2460 return row
327 bh 1634
328 bh 2460 def server_version(self):
329     """Return the server version as a tuple (major, minor, patch)
330    
331     Each item in the tuple is an int.
332     """
333     result = self.execute_sql("template1", "admin", "SELECT version();")[0]
334     match = re.match(r"PostgreSQL (\d+\.\d+\.\d+)", result)
335     if match:
336     return tuple(map(int, match.group(1).split(".")))
337     else:
338     raise RutimeError("Cannot determine PostgreSQL server version"
339     " from %r" % result)
340    
341 bh 1634 def require_authentication(self, required):
342     """Switch authentication requirements on or off
343    
344     When started for the first time no passwords are required. Some
345     tests want to explicitly test whether Thuban's password
346     infrastructure works and switch password authentication on
347     explicitly. When switching it on, there should be a
348     corresponding call to switch it off again in the test case'
349     tearDown method or in a finally: block.
350     """
351 bh 2460 # Starting with PostgreSQL 7.3 the pg_hba.conf file has an
352     # additional column with a username. Query the server version
353     # and generate a file in the correct format.
354     if self.server_version() >= (7, 3):
355     user = "all"
356     else:
357     user = ""
358 bh 1634 if required:
359 bh 2460 contents = "local all %s password\n" % user
360 bh 1634 else:
361 bh 2460 contents = "local all %s trust\n" % user
362 bh 1634 f = open(os.path.join(self.dbdir, "pg_hba.conf"), "w")
363     f.write(contents)
364     f.close()
365     run_command(["pg_ctl", "-D", self.dbdir, "reload"],
366     os.path.join(self.dbdir, "pg_ctl-reload.log"))
367    
368    
369     def create_user(self, username, password):
370     """Create user username with password in the database"""
371     self.execute_sql("template1", "admin",
372     "CREATE USER %s PASSWORD '%s';" % (username,password))
373    
374     def alter_user(self, username, password):
375     """Change the user username's password in the database"""
376     self.execute_sql("template1", "admin",
377     "ALTER USER %s PASSWORD '%s';" % (username,password))
378    
379    
380 bh 1605 class PostGISDatabase:
381    
382     """A PostGIS database in a PostgreSQLServer"""
383    
384 bh 2057 def __init__(self, server, postgis_sql, dbname, tables = None,
385 bh 2106 reference_systems = (), views = None):
386 bh 1662 """Initialize the PostGISDatabase
387    
388     Parameters:
389    
390     server -- The PostgreSQLServer instance containing the
391     database
392    
393     postgis_sql -- Filename of the postgis.sql file with the
394     postgis initialization code
395    
396     dbname -- The name of the database
397    
398     tables -- Optional description of tables to create in the
399     new database. If given it should be a list of
400     (tablename, shapefilename) pairs meaning that a table
401     tablename will be created with the contents of the given
402     shapefile or (tablename, shapefilename, extraargs)
403     triples. The extraargs should be a list of key, value
404     pairs to use as keyword arguments to upload_shapefile.
405 bh 2057
406     reference_systems -- Optional description of spatial
407     reference systems. If given, it should be a sequence of
408     (srid, params) pairs where srid is the srid defined by
409     the proj4 paramter string params. The srid can be given
410     as an extra parameter in the tables list.
411 bh 2106
412     views -- Optional description of views. If given it should
413     be a list of (viewname, select_stmt) pairs where
414     viewname is the name of the view to be created and
415     select_stmt is the select statement to use as the basis.
416     The views will be created after the tables and may refer
417     to them in the select_stmt.
418 bh 1662 """
419 bh 1605 self.server = server
420     self.postgis_sql = postgis_sql
421     self.dbname = dbname
422     self.tables = tables
423 bh 2106 self.views = views
424 bh 2057 if reference_systems:
425     self.reference_systems = reference_systems
426     else:
427     # Make sure that it's a sequence we can iterate over even if
428     # the parameter's None
429     self.reference_systems = ()
430 bh 1605
431     def initdb(self):
432     """Remove the old db directory and create and initialize a new database
433     """
434     run_command(["createdb", "-p", str(self.server.port),
435 bh 1634 "-h", self.server.host, "-U", self.server.admin_name,
436     self.dbname],
437 bh 1605 os.path.join(self.server.dbdir, "createdb.log"))
438     run_command(["createlang", "-p", str(self.server.port),
439 bh 1634 "-h", self.server.host, "-U", self.server.admin_name,
440     "plpgsql", self.dbname],
441 bh 1605 os.path.join(self.server.dbdir, "createlang.log"))
442     # for some reason psql doesn't exit with an error code if the
443     # file given as -f doesn't exist, so we check manually by trying
444     # to open it before we run psql
445     f = open(self.postgis_sql)
446     f.close()
447     del f
448     run_command(["psql", "-f", self.postgis_sql, "-d", self.dbname,
449 bh 1634 "-p", str(self.server.port), "-h", self.server.host,
450     "-U", self.server.admin_name],
451 bh 1605 os.path.join(self.server.dbdir, "psql.log"))
452    
453 bh 1634 self.server.execute_sql(self.dbname, "admin",
454     "GRANT SELECT ON geometry_columns TO PUBLIC;")
455 bh 2057 self.server.execute_sql(self.dbname, "admin",
456     "GRANT SELECT ON spatial_ref_sys TO PUBLIC;")
457 bh 1634
458 bh 2057 for srid, params in self.reference_systems:
459     self.server.execute_sql(self.dbname, "admin",
460     "INSERT INTO spatial_ref_sys VALUES"
461     " (%d, '', %d, '', '%s');"
462     % (srid, srid, params))
463 bh 1605 if self.tables is not None:
464 bh 1662 def unpack(item):
465 bh 2057 extra = {"force_wkt_type": None, "gid_offset": 0,
466     "srid": -1}
467 bh 1656 if len(info) == 2:
468     tablename, shapefile = info
469     else:
470 bh 1662 tablename, shapefile, kw = info
471     for key, val in kw:
472     extra[key] = val
473     return tablename, shapefile, extra
474 bh 1605
475 bh 1662 for info in self.tables:
476     tablename, shapefile, kw = unpack(info)
477     upload_shapefile(shapefile, self, tablename, **kw)
478    
479 bh 2106 if self.views is not None:
480     for viewname, select_stmt in self.views:
481     self.server.execute_sql(self.dbname, "admin",
482     "CREATE VIEW %s AS %s" % (viewname,
483     select_stmt))
484     self.server.execute_sql(self.dbname, "admin",
485     "GRANT SELECT ON %s TO PUBLIC;"
486     % viewname)
487    
488     def has_data(self, tables, reference_systems, views):
489 bh 2057 return (self.tables == tables
490 bh 2106 and self.reference_systems == reference_systems
491     and self.views == views)
492 bh 1605
493    
494     def find_postgis_sql():
495     """Return the name of the postgis_sql file
496    
497     A postgis installation usually has the postgis_sql file in
498 bh 2471 PostgreSQL's $datadir (i.e. the directory where PostgreSQL keeps
499 bh 1605 static files, not the directory containing the databases).
500     Unfortunately there's no way to determine the name of this directory
501     with pg_config so we assume here that it's
502     $bindir/../share/postgresql/.
503 bh 2471
504     Furthermore, different versions of postgis place the file in
505     slightly different locations. For instance:
506    
507     postgis 0.7.5 $datadir/contrib/postgis.sql
508     postgis 0.8.1 $datadir/postgis.sql
509    
510     To support both versions, we look in both places and return the
511     first one found (looking under contrib first). If the file is not
512     found the return value is None.
513 bh 1605 """
514     bindir = run_config_script("pg_config --bindir").strip()
515 bh 2471 datadir = os.path.join(bindir, "..", "share", "postgresql")
516     for filename in [os.path.join(datadir, "contrib", "postgis.sql"),
517     os.path.join(datadir, "postgis.sql")]:
518     if os.path.exists(filename):
519     return filename
520 bh 1605
521 bh 2471
522 bh 1605 _postgres_server = None
523     def get_test_server():
524     """Return the test database server object.
525    
526     If it doesn't exist yet, create it first.
527    
528     The server will use the directory postgis under the temp dir (as
529     defined by support.create_temp_dir()) for the database cluster.
530     Sockets will be created in tempdir.
531     """
532     global _postgres_server
533     if _postgres_server is None:
534     tempdir = support.create_temp_dir()
535     dbdir = os.path.join(tempdir, "postgis")
536     socket_dir = tempdir
537    
538     _postgres_server = PostgreSQLServer(dbdir, 6543, find_postgis_sql(),
539     socket_dir = socket_dir)
540     _postgres_server.createdb()
541    
542     return _postgres_server
543    
544     def shutdown_test_server():
545     """Shutdown the test server if it is running"""
546     global _postgres_server
547     if _postgres_server is not None:
548     _postgres_server.shutdown()
549     _postgres_server = None
550    
551    
552     def reason_for_not_running_tests():
553     """
554     Determine whether postgis tests can be run and return a reason they can't
555    
556     There's no fool-proof way to reliably determine this short of
557     actually running the tests but we try the following here:
558    
559     - test whether pg_ctl --help can be run successfully
560     - test whether the postgis_sql can be opened
561     The name of the postgis_sql file is determined by find_postgis_sql()
562     - psycopg can be imported successfully.
563     """
564 bh 1679 # run_command currently uses Popen4 which is not available under
565     # Windows, for example.
566     if not hasattr(popen2, "Popen4"):
567     return "Can't run PostGIS test because popen2.Popen4 does not exist"
568    
569 bh 1605 try:
570     run_command(["pg_ctl", "--help"], None)
571     except RuntimeError:
572     return "Can't run PostGIS tests because pg_ctl fails"
573    
574     try:
575     postgis_sql = find_postgis_sql()
576     except:
577     return "Can't run PostGIS tests because postgis.sql can't be found"
578    
579     try:
580     f = open(postgis_sql)
581     f.close()
582     except:
583     return "Can't run PostGIS tests because postgis.sql can't be opened"
584    
585     # The test for psycopg was already done when this module was
586     # imported so we only have to check whether it was successful
587     if psycopg is None:
588     return "Can't run PostGIS tests because psycopg can't be imported"
589    
590     return ""
591    
592    
593     _cannot_run_postgis_tests = None
594     def skip_if_no_postgis():
595     global _cannot_run_postgis_tests
596     if _cannot_run_postgis_tests is None:
597     _cannot_run_postgis_tests = reason_for_not_running_tests()
598     if _cannot_run_postgis_tests:
599     raise support.SkipTest(_cannot_run_postgis_tests)
600    
601 bh 1947 def skip_if_addgeometrycolumn_does_not_use_quote_ident():
602     """Skip a test if the AddGeometryColumn function doesn't use quote_ident
603    
604     If the AddGeometryColumn function doesn't use quote_ident it doesn't
605     support unusual table or column names properly, that is, it will
606     fail with errors for names that contain spaces or double quotes.
607    
608     The test performed by this function is a bit simplistic because it
609     only tests whether the string 'quote_ident' occurs anywhere in the
610 bh 2096 postgis.sql file. This will hopefully work because when this was
611 bh 1947 fixed in postgis CVS AddGeometryColumn was the first function to use
612     quote_ident.
613     """
614     f = file(find_postgis_sql())
615     content = f.read()
616     f.close()
617     if content.find("quote_ident") < 0:
618     raise support.SkipTest("AddGeometryColumn doesn't use quote_ident")
619    
620 bh 1656 def coords_to_point(coords):
621 bh 1605 """Return string with a WKT representation of the point in coords"""
622     x, y = coords[0]
623     return "POINT(%r %r)" % (x, y)
624    
625 bh 1656 def coords_to_polygon(coords):
626 bh 1605 """Return string with a WKT representation of the polygon in coords"""
627     poly = []
628     for ring in coords:
629     poly.append(", ".join(["%r %r" % p for p in ring]))
630     return "POLYGON((%s))" % "), (".join(poly)
631    
632 bh 1656 def coords_to_multilinestring(coords):
633 bh 1605 """Return string with a WKT representation of the arc in coords"""
634     poly = []
635     for ring in coords:
636     poly.append(", ".join(["%r %r" % p for p in ring]))
637     return "MULTILINESTRING((%s))" % "), (".join(poly)
638    
639 bh 1656 def coords_to_multipolygon(coords):
640     """Return string with a WKT representation of the polygon in coords"""
641     poly = []
642     for ring in coords:
643     poly.append(", ".join(["%r %r" % p for p in ring]))
644     return "MULTIPOLYGON(((%s)))" % ")), ((".join(poly)
645    
646     wkt_converter = {
647     "POINT": coords_to_point,
648     "MULTILINESTRING": coords_to_multilinestring,
649     "POLYGON": coords_to_polygon,
650     "MULTIPOLYGON": coords_to_multipolygon,
651     }
652    
653 bh 1662 def upload_shapefile(filename, db, tablename, force_wkt_type = None,
654 bh 2096 gid_offset = 0, gid_column = "gid", srid = -1):
655 bh 1662 """Upload a shapefile into a new database table
656    
657     Parameters:
658    
659     filename -- The name of the shapefile
660    
661     db -- The PostGISDatabase instance representing the database
662    
663     tablename -- The name of the table to create and into which the data
664     is to be inserted
665    
666     force_wkt_type -- If given the real WKT geometry type to use instead
667     of the default that would be chosen based on the type of
668     the shapefile
669    
670     gid_offset -- A number to add to the shapeid to get the value for
671     the gid column (default 0)
672 bh 2057
673 bh 2096 gid_column -- The name of the column with the shape ids. Default
674     'gid'. If None, no gid column will be created. The
675     name is directly used in SQL statements, so if it
676     contains unusualy characters the caller should provide
677     a suitable quoted string.
678    
679 bh 2057 srid -- The srid of the spatial references system used by the table
680     and the data
681 bh 1662 """
682 bh 1605 import dbflib, shapelib
683    
684 bh 1656 # We build this map here because we need shapelib which can only be
685     # imported after support.initthuban has been called which we can't
686     # easily do in this module because it's imported by support.
687     shp_to_wkt = {
688     shapelib.SHPT_POINT: "POINT",
689     shapelib.SHPT_ARC: "MULTILINESTRING",
690     shapelib.SHPT_POLYGON: "POLYGON",
691     }
692    
693 bh 1605 server = db.server
694     dbname = db.dbname
695 bh 1634 conn = psycopg.connect("dbname=%s " % dbname
696     + db.server.connection_string("admin"))
697 bh 1605 cursor = conn.cursor()
698    
699     shp = shapelib.ShapeFile(filename)
700     dbf = dbflib.DBFFile(filename)
701     typemap = {dbflib.FTString: "VARCHAR",
702     dbflib.FTInteger: "INTEGER",
703     dbflib.FTDouble: "DOUBLE PRECISION"}
704    
705 bh 2096 insert_formats = []
706     if gid_column:
707     insert_formats.append("%(gid)s")
708    
709     fields = []
710     fields_decl = []
711     if gid_column:
712     fields.append(gid_column)
713     fields_decl.append("%s INT" % gid_column)
714 bh 1605 for i in range(dbf.field_count()):
715     ftype, name, width, prec = dbf.field_info(i)
716 bh 2096 fields.append(name)
717     fields_decl.append("%s %s" % (name, typemap[ftype]))
718 bh 1605 insert_formats.append("%%(%s)s" % name)
719     stmt = "CREATE TABLE %s (\n %s\n);" % (tablename,
720 bh 2096 ",\n ".join(fields_decl))
721 bh 1605 cursor.execute(stmt)
722     #print stmt
723    
724     numshapes, shapetype, mins, maxs = shp.info()
725 bh 1656 wkttype = shp_to_wkt[shapetype]
726     if force_wkt_type:
727     wkttype = force_wkt_type
728     convert = wkt_converter[wkttype]
729 bh 1605
730     cursor.execute("select AddGeometryColumn('%(dbname)s',"
731 bh 2057 "'%(tablename)s', 'the_geom', %(srid)d, '%(wkttype)s', 2);"
732 bh 1605 % locals())
733 bh 2096 fields.append("the_geom")
734 bh 2057 insert_formats.append("GeometryFromText(%(the_geom)s, %(srid)d)")
735 bh 1605
736 bh 2096 insert = ("INSERT INTO %s (%s) VALUES (%s)"
737     % (tablename, ", ".join(fields), ", ".join(insert_formats)))
738 bh 1605
739     for i in range(numshapes):
740     data = dbf.read_record(i)
741     data["tablename"] = tablename
742 bh 2096 if gid_column:
743     data["gid"] = i + gid_offset
744 bh 2057 data["srid"] = srid
745 bh 1605 data["the_geom"] = convert(shp.read_object(i).vertices())
746     #print insert % data
747     cursor.execute(insert, data)
748    
749 bh 1634 cursor.execute("GRANT SELECT ON %s TO PUBLIC;" % tablename)
750    
751 bh 1605 conn.commit()

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26