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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2459 - (show annotations)
Wed Dec 15 11:12:11 2004 UTC (20 years, 2 months ago) by bh
Original Path: trunk/thuban/test/postgissupport.py
File MIME type: text/x-python
File size: 26955 byte(s)
(PostgreSQLServer.is_running): Fix typo
in the doc string and rephrase it a little.

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