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

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

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 1605 by bh, Tue Aug 19 11:00:40 2003 UTC revision 2106 by bh, Fri Mar 12 12:59:33 2004 UTC
# Line 1  Line 1 
1  # Copyright (C) 2003 by Intevation GmbH  # Copyright (C) 2003, 2004 by Intevation GmbH
2  # Authors:  # Authors:
3  # Bernhard Herzog <[email protected]>  # Bernhard Herzog <[email protected]>
4  #  #
# Line 115  class PostgreSQLServer: Line 115  class PostgreSQLServer:
115          self.socket_dir = socket_dir          self.socket_dir = socket_dir
116    
117          # For the client side the socket directory can be used as the          # For the client side the socket directory can be used as the
118          # host the name starts with a slash.          # host if the name starts with a slash.
119          self.host = os.path.abspath(socket_dir)          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          # Map db names to db objects
128          self.known_dbs = {}          self.known_dbs = {}
129    
# Line 135  class PostgreSQLServer: Line 141  class PostgreSQLServer:
141              shutil.rmtree(self.dbdir)              shutil.rmtree(self.dbdir)
142          os.mkdir(self.dbdir)          os.mkdir(self.dbdir)
143    
144          run_command(["initdb", self.dbdir],          run_command(["initdb", "-D", self.dbdir, "-U", self.admin_name],
145                      os.path.join(self.dbdir, "initdb.log"))                      os.path.join(self.dbdir, "initdb.log"))
146    
147          extra_opts = "-p %d" % self.port          extra_opts = "-p %d" % self.port
# Line 150  class PostgreSQLServer: Line 156  class PostgreSQLServer:
156          # server ourselves          # server ourselves
157          self.wait_for_postmaster()          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):      def wait_for_postmaster(self):
163          """Return when the database server is running          """Return when the database server is running
164    
# Line 161  class PostgreSQLServer: Line 170  class PostgreSQLServer:
170          while count < max_count:          while count < max_count:
171              try:              try:
172                  run_command(["psql", "-l", "-p", str(self.port),                  run_command(["psql", "-l", "-p", str(self.port),
173                               "-h", self.host],                               "-h", self.host, "-U", self.admin_name],
174                              os.path.join(self.dbdir, "psql-%d.log" % count))                              os.path.join(self.dbdir, "psql-%d.log" % count))
175              except:              except RuntimeError:
176                  pass                  pass
177                except:
178                    traceback.print_exc()
179              else:              else:
180                  break                  break
181              time.sleep(0.5)              time.sleep(0.5)
# Line 187  class PostgreSQLServer: Line 198  class PostgreSQLServer:
198          run_command(["pg_ctl", "-m", "fast", "-D", self.dbdir, "stop"],          run_command(["pg_ctl", "-m", "fast", "-D", self.dbdir, "stop"],
199                      os.path.join(self.dbdir, "pg_ctl-stop.log"))                      os.path.join(self.dbdir, "pg_ctl-stop.log"))
200    
201      def new_postgis_db(self, dbname, tables = None):      def new_postgis_db(self, dbname, tables = None, reference_systems = None,
202                           views = None):
203          """Create and return a new PostGISDatabase object using self as server          """Create and return a new PostGISDatabase object using self as server
204          """          """
205          db = PostGISDatabase(self, self.postgis_sql, dbname, tables = tables)          db = PostGISDatabase(self, self.postgis_sql, dbname, tables = tables,
206                                 reference_systems = reference_systems,
207                                 views = views)
208          db.initdb()          db.initdb()
209          self.known_dbs[dbname] = db          self.known_dbs[dbname] = db
210          return db          return db
211    
212      def get_static_data_db(self, dbname, tables):      def get_static_data_db(self, dbname, tables, reference_systems, views):
213          """Return a PostGISDatabase for a database with the given static data          """Return a PostGISDatabase for a database with the given static data
214    
215          If no databasse of the name dbname exists, create a new one via          If no databasse of the name dbname exists, create a new one via
# Line 205  class PostgreSQLServer: Line 219  class PostgreSQLServer:
219          indicated data, return that. If the already existing db uses          indicated data, return that. If the already existing db uses
220          different data raise a value error.          different data raise a value error.
221    
222          The tables argument should be a sequence of table specifications          If the database doesn't exist, create a new one via
223          where each specifications is a (tablename, shapefilename) pair.          self.new_postgis_db.
224    
225            The parameters tables and reference_systems have the same
226            meaning as for new_postgis_db.
227          """          """
228          db = self.known_dbs.get(dbname)          db = self.known_dbs.get(dbname)
229          if db is not None:          if db is not None:
230              if db.has_data(tables):              if db.has_data(tables, reference_systems, views):
231                  return db                  return db
232              raise ValueError("PostGISDatabase named %r doesn't have tables %r"              raise ValueError("PostGISDatabase named %r doesn't have tables %r"
233                               % (dbname, tables))                               % (dbname, tables))
234          return self.new_postgis_db(dbname, tables)          return self.new_postgis_db(dbname, tables, reference_systems, views)
235    
236      def get_default_static_data_db(self):      def get_default_static_data_db(self):
237          dbname = "PostGISStaticTests"          dbname = "PostGISStaticTests"
238          tables = [("landmarks", os.path.join("..", "Data", "iceland",          srids = [(1, "proj=longlat datum=WGS84")]
239                                               "cultural_landmark-point.shp")),          tables = [
240                    ("political", os.path.join("..", "Data", "iceland",              # Direct copies of the shapefiles. The shapeids are exactly
241                # the same, except where changed with "gid_offset", of
242                # course.  Note that the test implementation requires that
243                # all the landmard tables use an gid_offset of 1000.
244                ("landmarks", os.path.join("..", "Data", "iceland",
245                                           "cultural_landmark-point.shp"),
246                 [("gid_offset", 1000)]),
247                ("political", os.path.join("..", "Data", "iceland",
248                                               "political.shp")),                                               "political.shp")),
249                    ("roads", os.path.join("..", "Data", "iceland",              ("roads", os.path.join("..", "Data", "iceland",
250                                           "roads-line.shp"))]                                           "roads-line.shp")),
251          return self.get_static_data_db(dbname, tables)  
252                # The polygon data as a MULTIPOLYGON geometry type
253                ("political_multi", os.path.join("..", "Data", "iceland",
254                                                 "political.shp"),
255                 [("force_wkt_type", "MULTIPOLYGON")]),
256    
257                # Copy of landmarks but using an srid != -1
258                ("landmarks_srid", os.path.join("..", "Data", "iceland",
259                                           "cultural_landmark-point.shp"),
260                 [("gid_offset", 1000),
261                  ("srid", 1)]),
262    
263                # Copy of landmarks with a gid column called "point_id" instead
264                # of "gid" and using an srid != -1.
265                ("landmarks_point_id", os.path.join("..", "Data", "iceland",
266                                                    "cultural_landmark-point.shp"),
267                 [("gid_offset", 1000),
268                  ("srid", 1),
269                  ("gid_column", "point_id")]),
270                ]
271            views = [("v_landmarks", "SELECT * FROM landmarks_point_id")]
272            return self.get_static_data_db(dbname, tables, srids, views)
273    
274        def connection_params(self, user):
275            """Return the connection parameters for the given user
276    
277            The return value is a dictionary suitable as keyword argument
278            list to PostGISConnection. The user parameter may be either
279            'admin' to connect as admin or 'user' to connect as an
280            unprivileged user.
281            """
282            return {"host": self.host, "port": self.port,
283                    "user": getattr(self, user + "_name"),
284                    "password": getattr(self, user + "_password")}
285    
286        def connection_string(self, user):
287            """Return (part of) the connection string to pass to psycopg.connect
288    
289            The string contains host, port, user and password. The user
290            parameter must be either 'admin' or 'user', as for
291            connection_params.
292            """
293            params = []
294            for key, value in self.connection_params(user).items():
295                # FIXME: this doesn't do quiting correctly but that
296                # shouldn't be much of a problem (people shouldn't be using
297                # single quotes in filenames anyway :) )
298                params.append("%s='%s'" % (key, value))
299            return " ".join(params)
300    
301        def execute_sql(self, dbname, user, sql):
302            """Execute the sql statament
303    
304            The user parameter us used as in connection_params. The dbname
305            parameter must be the name of a database in the cluster.
306            """
307            conn = psycopg.connect("dbname=%s " % dbname
308                                   + self.connection_string(user))
309            cursor = conn.cursor()
310            cursor.execute(sql)
311            conn.commit()
312            conn.close()
313    
314        def require_authentication(self, required):
315            """Switch authentication requirements on or off
316    
317            When started for the first time no passwords are required. Some
318            tests want to explicitly test whether Thuban's password
319            infrastructure works and switch password authentication on
320            explicitly. When switching it on, there should be a
321            corresponding call to switch it off again in the test case'
322            tearDown method or in a finally: block.
323            """
324            if required:
325                contents = "local all password\n"
326            else:
327                contents = "local all trust\n"
328            f = open(os.path.join(self.dbdir, "pg_hba.conf"), "w")
329            f.write(contents)
330            f.close()
331            run_command(["pg_ctl", "-D", self.dbdir, "reload"],
332                        os.path.join(self.dbdir, "pg_ctl-reload.log"))
333    
334    
335        def create_user(self, username, password):
336            """Create user username with password in the database"""
337            self.execute_sql("template1", "admin",
338                             "CREATE USER %s PASSWORD '%s';" % (username,password))
339    
340        def alter_user(self, username, password):
341            """Change the user username's password in the database"""
342            self.execute_sql("template1", "admin",
343                             "ALTER USER %s PASSWORD '%s';" % (username,password))
344    
345    
346  class PostGISDatabase:  class PostGISDatabase:
347    
348      """A PostGIS database in a PostgreSQLServer"""      """A PostGIS database in a PostgreSQLServer"""
349    
350      def __init__(self, server, postgis_sql, dbname, tables = None):      def __init__(self, server, postgis_sql, dbname, tables = None,
351                     reference_systems = (), views = None):
352            """Initialize the PostGISDatabase
353    
354            Parameters:
355    
356                server -- The PostgreSQLServer instance containing the
357                    database
358    
359                postgis_sql -- Filename of the postgis.sql file with the
360                    postgis initialization code
361    
362                dbname -- The name of the database
363    
364                tables -- Optional description of tables to create in the
365                    new database. If given it should be a list of
366                    (tablename, shapefilename) pairs meaning that a table
367                    tablename will be created with the contents of the given
368                    shapefile or (tablename, shapefilename, extraargs)
369                    triples. The extraargs should be a list of key, value
370                    pairs to use as keyword arguments to upload_shapefile.
371    
372                reference_systems -- Optional description of spatial
373                    reference systems.  If given, it should be a sequence of
374                    (srid, params) pairs where srid is the srid defined by
375                    the proj4 paramter string params.  The srid can be given
376                    as an extra parameter in the tables list.
377    
378                views -- Optional description of views.  If given it should
379                    be a list of (viewname, select_stmt) pairs where
380                    viewname is the name of the view to be created and
381                    select_stmt is the select statement to use as the basis.
382                    The views will be created after the tables and may refer
383                    to them in the select_stmt.
384            """
385          self.server = server          self.server = server
386          self.postgis_sql = postgis_sql          self.postgis_sql = postgis_sql
387          self.dbname = dbname          self.dbname = dbname
388          self.tables = tables          self.tables = tables
389            self.views = views
390            if reference_systems:
391                self.reference_systems = reference_systems
392            else:
393                # Make sure that it's a sequence we can iterate over even if
394                # the parameter's None
395                self.reference_systems = ()
396    
397      def initdb(self):      def initdb(self):
398          """Remove the old db directory and create and initialize a new database          """Remove the old db directory and create and initialize a new database
399          """          """
400          run_command(["createdb", "-p", str(self.server.port),          run_command(["createdb", "-p", str(self.server.port),
401                       "-h", self.server.host, self.dbname],                       "-h", self.server.host, "-U", self.server.admin_name,
402                         self.dbname],
403                      os.path.join(self.server.dbdir, "createdb.log"))                      os.path.join(self.server.dbdir, "createdb.log"))
404          run_command(["createlang", "-p", str(self.server.port),          run_command(["createlang", "-p", str(self.server.port),
405                       "-h", self.server.host, "plpgsql", self.dbname],                       "-h", self.server.host,  "-U", self.server.admin_name,
406                         "plpgsql", self.dbname],
407                      os.path.join(self.server.dbdir, "createlang.log"))                      os.path.join(self.server.dbdir, "createlang.log"))
408          # for some reason psql doesn't exit with an error code if the          # for some reason psql doesn't exit with an error code if the
409          # file given as -f doesn't exist, so we check manually by trying          # file given as -f doesn't exist, so we check manually by trying
# Line 255  class PostGISDatabase: Line 412  class PostGISDatabase:
412          f.close()          f.close()
413          del f          del f
414          run_command(["psql", "-f", self.postgis_sql, "-d", self.dbname,          run_command(["psql", "-f", self.postgis_sql, "-d", self.dbname,
415                       "-p", str(self.server.port), "-h", self.server.host],                       "-p", str(self.server.port), "-h", self.server.host,
416                         "-U", self.server.admin_name],
417                       os.path.join(self.server.dbdir, "psql.log"))                       os.path.join(self.server.dbdir, "psql.log"))
418    
419            self.server.execute_sql(self.dbname, "admin",
420                                    "GRANT SELECT ON geometry_columns TO PUBLIC;")
421            self.server.execute_sql(self.dbname, "admin",
422                                    "GRANT SELECT ON spatial_ref_sys TO PUBLIC;")
423    
424            for srid, params in self.reference_systems:
425                self.server.execute_sql(self.dbname, "admin",
426                                        "INSERT INTO spatial_ref_sys VALUES"
427                                        " (%d, '', %d, '', '%s');"
428                                        % (srid, srid, params))
429          if self.tables is not None:          if self.tables is not None:
430              for tablename, shapefile in self.tables:              def unpack(item):
431                  upload_shapefile(shapefile, self, tablename)                  extra = {"force_wkt_type": None, "gid_offset": 0,
432                             "srid": -1}
433      def has_data(self, tables):                  if len(info) == 2:
434          return self.tables == tables                      tablename, shapefile = info
435                    else:
436                        tablename, shapefile, kw = info
437                        for key, val in kw:
438                            extra[key] = val
439                    return tablename, shapefile, extra
440    
441                for info in self.tables:
442                    tablename, shapefile, kw = unpack(info)
443                    upload_shapefile(shapefile, self, tablename, **kw)
444    
445            if self.views is not None:
446                for viewname, select_stmt in self.views:
447                    self.server.execute_sql(self.dbname, "admin",
448                                            "CREATE VIEW %s AS %s" % (viewname,
449                                                                      select_stmt))
450                    self.server.execute_sql(self.dbname, "admin",
451                                            "GRANT SELECT ON %s TO PUBLIC;"
452                                            % viewname)
453    
454        def has_data(self, tables, reference_systems, views):
455            return (self.tables == tables
456                    and self.reference_systems == reference_systems
457                    and self.views == views)
458    
459    
460  def find_postgis_sql():  def find_postgis_sql():
# Line 322  def reason_for_not_running_tests(): Line 513  def reason_for_not_running_tests():
513         The name of the postgis_sql file is determined by find_postgis_sql()         The name of the postgis_sql file is determined by find_postgis_sql()
514       - psycopg can be imported successfully.       - psycopg can be imported successfully.
515      """      """
516        # run_command currently uses Popen4 which is not available under
517        # Windows, for example.
518        if not hasattr(popen2, "Popen4"):
519            return "Can't run PostGIS test because popen2.Popen4 does not exist"
520    
521      try:      try:
522          run_command(["pg_ctl", "--help"], None)          run_command(["pg_ctl", "--help"], None)
523      except RuntimeError:      except RuntimeError:
# Line 354  def skip_if_no_postgis(): Line 550  def skip_if_no_postgis():
550      if _cannot_run_postgis_tests:      if _cannot_run_postgis_tests:
551          raise support.SkipTest(_cannot_run_postgis_tests)          raise support.SkipTest(_cannot_run_postgis_tests)
552    
553  def point_to_wkt(coords):  def skip_if_addgeometrycolumn_does_not_use_quote_ident():
554        """Skip a test if the AddGeometryColumn function doesn't use quote_ident
555    
556        If the AddGeometryColumn function doesn't use quote_ident it doesn't
557        support unusual table or column names properly, that is, it will
558        fail with errors for names that contain spaces or double quotes.
559    
560        The test performed by this function is a bit simplistic because it
561        only tests whether the string 'quote_ident' occurs anywhere in the
562        postgis.sql file. This will hopefully work because when this was
563        fixed in postgis CVS AddGeometryColumn was the first function to use
564        quote_ident.
565        """
566        f = file(find_postgis_sql())
567        content = f.read()
568        f.close()
569        if content.find("quote_ident") < 0:
570            raise support.SkipTest("AddGeometryColumn doesn't use quote_ident")
571    
572    def coords_to_point(coords):
573      """Return string with a WKT representation of the point in coords"""      """Return string with a WKT representation of the point in coords"""
574      x, y = coords[0]      x, y = coords[0]
575      return "POINT(%r %r)" % (x, y)      return "POINT(%r %r)" % (x, y)
576    
577  def polygon_to_wkt(coords):  def coords_to_polygon(coords):
578      """Return string with a WKT representation of the polygon in coords"""      """Return string with a WKT representation of the polygon in coords"""
579      poly = []      poly = []
580      for ring in coords:      for ring in coords:
581          poly.append(", ".join(["%r %r" % p for p in ring]))          poly.append(", ".join(["%r %r" % p for p in ring]))
582      return "POLYGON((%s))" % "), (".join(poly)      return "POLYGON((%s))" % "), (".join(poly)
583    
584  def arc_to_wkt(coords):  def coords_to_multilinestring(coords):
585      """Return string with a WKT representation of the arc in coords"""      """Return string with a WKT representation of the arc in coords"""
586      poly = []      poly = []
587      for ring in coords:      for ring in coords:
588          poly.append(", ".join(["%r %r" % p for p in ring]))          poly.append(", ".join(["%r %r" % p for p in ring]))
589      return "MULTILINESTRING((%s))" % "), (".join(poly)      return "MULTILINESTRING((%s))" % "), (".join(poly)
590    
591  def upload_shapefile(filename, db, tablename):  def coords_to_multipolygon(coords):
592        """Return string with a WKT representation of the polygon in coords"""
593        poly = []
594        for ring in coords:
595            poly.append(", ".join(["%r %r" % p for p in ring]))
596        return "MULTIPOLYGON(((%s)))" % ")), ((".join(poly)
597    
598    wkt_converter = {
599        "POINT": coords_to_point,
600        "MULTILINESTRING": coords_to_multilinestring,
601        "POLYGON": coords_to_polygon,
602        "MULTIPOLYGON": coords_to_multipolygon,
603        }
604    
605    def upload_shapefile(filename, db, tablename, force_wkt_type = None,
606                         gid_offset = 0, gid_column = "gid", srid = -1):
607        """Upload a shapefile into a new database table
608    
609        Parameters:
610    
611        filename -- The name of the shapefile
612    
613        db -- The PostGISDatabase instance representing the database
614    
615        tablename -- The name of the table to create and into which the data
616                    is to be inserted
617    
618        force_wkt_type -- If given the real WKT geometry type to use instead
619                    of the default that would be chosen based on the type of
620                    the shapefile
621    
622        gid_offset -- A number to add to the shapeid to get the value for
623                    the gid column (default 0)
624    
625        gid_column -- The name of the column with the shape ids.  Default
626                      'gid'.  If None, no gid column will be created.  The
627                      name is directly used in SQL statements, so if it
628                      contains unusualy characters the caller should provide
629                      a suitable quoted string.
630    
631        srid -- The srid of the spatial references system used by the table
632                and the data
633        """
634      import dbflib, shapelib      import dbflib, shapelib
635    
636        # We build this map here because we need shapelib which can only be
637        # imported after support.initthuban has been called which we can't
638        # easily do in this module because it's imported by support.
639        shp_to_wkt = {
640            shapelib.SHPT_POINT: "POINT",
641            shapelib.SHPT_ARC: "MULTILINESTRING",
642            shapelib.SHPT_POLYGON: "POLYGON",
643            }
644    
645      server = db.server      server = db.server
646      dbname = db.dbname      dbname = db.dbname
647      conn = psycopg.connect("host=%s port=%s dbname=%s"      conn = psycopg.connect("dbname=%s " % dbname
648                             % (server.host, server.port, dbname))                             + db.server.connection_string("admin"))
649      cursor = conn.cursor()      cursor = conn.cursor()
650    
651      shp = shapelib.ShapeFile(filename)      shp = shapelib.ShapeFile(filename)
# Line 388  def upload_shapefile(filename, db, table Line 654  def upload_shapefile(filename, db, table
654                 dbflib.FTInteger: "INTEGER",                 dbflib.FTInteger: "INTEGER",
655                 dbflib.FTDouble: "DOUBLE PRECISION"}                 dbflib.FTDouble: "DOUBLE PRECISION"}
656    
657      insert_formats = ["%(gid)s"]      insert_formats = []
658      fields = ["gid INT"]      if gid_column:
659            insert_formats.append("%(gid)s")
660    
661        fields = []
662        fields_decl = []
663        if gid_column:
664            fields.append(gid_column)
665            fields_decl.append("%s INT" % gid_column)
666      for i in range(dbf.field_count()):      for i in range(dbf.field_count()):
667          ftype, name, width, prec = dbf.field_info(i)          ftype, name, width, prec = dbf.field_info(i)
668          fields.append("%s %s" % (name, typemap[ftype]))          fields.append(name)
669            fields_decl.append("%s %s" % (name, typemap[ftype]))
670          insert_formats.append("%%(%s)s" % name)          insert_formats.append("%%(%s)s" % name)
671      stmt = "CREATE TABLE %s (\n    %s\n);" % (tablename,      stmt = "CREATE TABLE %s (\n    %s\n);" % (tablename,
672                                                ",\n    ".join(fields))                                                ",\n    ".join(fields_decl))
673      cursor.execute(stmt)      cursor.execute(stmt)
674      #print stmt      #print stmt
675    
676      numshapes, shapetype, mins, maxs = shp.info()      numshapes, shapetype, mins, maxs = shp.info()
677      if shapetype == shapelib.SHPT_POINT:      wkttype =  shp_to_wkt[shapetype]
678          convert = point_to_wkt      if force_wkt_type:
679          wkttype = "POINT"          wkttype = force_wkt_type
680      elif shapetype == shapelib.SHPT_POLYGON:      convert = wkt_converter[wkttype]
         convert = polygon_to_wkt  
         wkttype = "POLYGON"  
     elif shapetype == shapelib.SHPT_ARC:  
         convert = arc_to_wkt  
         wkttype = "MULTILINESTRING"  
     else:  
         raise ValueError("Unsupported Shapetype %r" % shapetype)  
681    
682      cursor.execute("select AddGeometryColumn('%(dbname)s',"      cursor.execute("select AddGeometryColumn('%(dbname)s',"
683                     "'%(tablename)s', 'the_geom', '-1', '%(wkttype)s', 2);"                     "'%(tablename)s', 'the_geom', %(srid)d, '%(wkttype)s', 2);"
684                     % locals())                     % locals())
685        fields.append("the_geom")
686        insert_formats.append("GeometryFromText(%(the_geom)s, %(srid)d)")
687    
688      insert_formats.append("GeometryFromText(%(the_geom)s, -1)")      insert = ("INSERT INTO %s (%s) VALUES (%s)"
689                  % (tablename, ", ".join(fields), ", ".join(insert_formats)))
     insert = ("INSERT INTO %s VALUES (%s)"  
               % (tablename, ", ".join(insert_formats)))  
690    
691      for i in range(numshapes):      for i in range(numshapes):
692          data = dbf.read_record(i)          data = dbf.read_record(i)
693          data["tablename"] = tablename          data["tablename"] = tablename
694          data["gid"] = i          if gid_column:
695                data["gid"] = i + gid_offset
696            data["srid"] = srid
697          data["the_geom"] = convert(shp.read_object(i).vertices())          data["the_geom"] = convert(shp.read_object(i).vertices())
698          #print insert % data          #print insert % data
699          cursor.execute(insert, data)          cursor.execute(insert, data)
700    
701        cursor.execute("GRANT SELECT ON %s TO PUBLIC;" % tablename)
702    
703      conn.commit()      conn.commit()

Legend:
Removed from v.1605  
changed lines
  Added in v.2106

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26