core.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. """!@package grass.temporal
  2. @brief GRASS Python scripting module (temporal GIS functions)
  3. Temporal GIS core functions to be used in library modules and scripts.
  4. This module provides the functionality to create the temporal
  5. SQL database and to establish a connection to the database.
  6. Usage:
  7. @code
  8. >>> import grass.temporal as tgis
  9. >>> # Create the temporal database
  10. >>> tgis.init()
  11. >>> # Establish a database connection
  12. >>> dbif, connected = tgis.init_dbif(None)
  13. >>> dbif.connect()
  14. >>> # Execute a SQL statement
  15. >>> dbif.execute_transaction("SELECT datetime(0, 'unixepoch', 'localtime');")
  16. >>> # Mogrify an SQL statement
  17. >>> dbif.mogrify_sql_statement(["SELECT name from raster_base where name = ?",
  18. ... ("precipitation",)])
  19. "SELECT name from raster_base where name = 'precipitation'"
  20. >>> dbif.close()
  21. @endcode
  22. (C) 2008-2011 by the GRASS Development Team
  23. This program is free software under the GNU General Public
  24. License (>=v2). Read the file COPYING that comes with GRASS
  25. for details.
  26. @author Soeren Gebbert
  27. """
  28. import os
  29. import grass.script.core as core
  30. from datetime import datetime
  31. # Import all supported database backends
  32. # Ignore import errors since they are checked later
  33. try:
  34. import sqlite3
  35. except ImportError:
  36. pass
  37. # Postgresql is optional, existence is checked when needed
  38. try:
  39. import psycopg2
  40. import psycopg2.extras
  41. except:
  42. pass
  43. # Global variable that defines the backend
  44. # of the temporal GIS
  45. # It can either be "sqlite" or "pg"
  46. tgis_backend = None
  47. # The version of the temporal framework
  48. # this value must be an integer larger than 0
  49. # Increase this value in case of backward incompatible changes in the TGIS API
  50. tgis_version=1
  51. # The version of the temporal database since framework and database version can differ
  52. # this value must be an integer larger than 0
  53. # Increase this value in case of backward incompatible changes
  54. # temporal database SQL layout
  55. tgis_db_version=1
  56. ###############################################################################
  57. def get_tgis_version():
  58. """!Get the verion number of the temporal framework
  59. @return The version number of the temporal framework as string
  60. """
  61. global tgis_version
  62. return tgis_version
  63. ###############################################################################
  64. def get_tgis_metadata(dbif=None):
  65. """!Return the tgis metadata table as a list of rows (dicts)
  66. or None if not present
  67. @param dbif The database interface to be used
  68. @return The selected rows with key/value comumns or None
  69. """
  70. dbif, connected = init_dbif(dbif)
  71. # Select metadata if the table is present
  72. try:
  73. statement = "SELECT * FROM tgis_metadata;\n"
  74. dbif.cursor.execute(statement)
  75. rows = dbif.cursor.fetchall()
  76. except:
  77. rows = None
  78. if connected:
  79. dbif.close()
  80. return rows
  81. ###############################################################################
  82. def get_temporal_dbmi_init_string():
  83. kv = core.parse_command("t.connect", flags="pg")
  84. grassenv = core.gisenv()
  85. global tgis_backend
  86. if tgis_backend == "sqlite":
  87. if "database" in kv:
  88. string = kv["database"]
  89. string = string.replace("$GISDBASE", grassenv["GISDBASE"])
  90. string = string.replace(
  91. "$LOCATION_NAME", grassenv["LOCATION_NAME"])
  92. return string
  93. else:
  94. core.fatal(_("Unable to initialize the temporal GIS DBMI "
  95. "interface. Use t.connect to specify the driver "
  96. "and the database string"))
  97. elif tgis_backend == "pg":
  98. if "database" in kv:
  99. string = kv["database"]
  100. return string
  101. else:
  102. core.fatal(_("Unable to initialize the temporal GIS DBMI "
  103. "interface. Use t.connect to specify the driver "
  104. "and the database string"))
  105. ###############################################################################
  106. # This variable specifies if the ctypes interface to the grass
  107. # libraries should be used to read map specific data. If set to False
  108. # the grass scripting library will be used to get map informations.
  109. # The advantage of the ctypes inteface is speed, the disadvantage is that
  110. # the GRASS C functions may call G_fatal_error() which exits the process.
  111. # That is not catchable in Python.
  112. use_ctypes_map_access = True
  113. def set_use_ctypes_map_access(use_ctype = True):
  114. """!Define the map access method for the temporal GIS library
  115. Using ctypes to read map metadata is much faster
  116. then using the grass.script interface that calls grass modules.
  117. The disadvantage is that GRASS C-library function will call
  118. G_fatal_error() that will exit the calling process.
  119. GUI developer should set this flag to False.
  120. @param use_ctype True use ctypes interface, False use grass.script interface
  121. """
  122. global use_ctypes_map_access
  123. use_ctypes_map_access = use_ctype
  124. ###############################################################################
  125. def get_use_ctypes_map_access():
  126. """!Return true if ctypes is used for map access """
  127. global use_ctypes_map_access
  128. return use_ctypes_map_access
  129. ###############################################################################
  130. def get_sql_template_path():
  131. base = os.getenv("GISBASE")
  132. base_etc = os.path.join(base, "etc")
  133. return os.path.join(base_etc, "sql")
  134. ###############################################################################
  135. def init():
  136. """!This function set the correct database backend from the environmental variables
  137. and creates the grass location database structure for raster,
  138. vector and raster3d maps as well as for the space-time datasets strds,
  139. str3ds and stvds in case it not exists.
  140. ATTENTION: This functions must be called before any spatio-temporal processing
  141. can be started
  142. """
  143. # We need to set the correct database backend from the environment variables
  144. global tgis_backend
  145. core.run_command("t.connect", flags="c")
  146. kv = core.parse_command("t.connect", flags="pg")
  147. if "driver" in kv:
  148. if kv["driver"] == "sqlite":
  149. tgis_backend = kv["driver"]
  150. try:
  151. import sqlite3
  152. except ImportError:
  153. core.error("Unable to locate the sqlite SQL Python interface module sqlite3.")
  154. raise
  155. dbmi = sqlite3
  156. elif kv["driver"] == "pg":
  157. tgis_backend = kv["driver"]
  158. try:
  159. import psycopg2
  160. except ImportError:
  161. core.error("Unable to locate the Postgresql SQL Python interface module psycopg2.")
  162. raise
  163. dbmi = psycopg2
  164. else:
  165. core.fatal(_("Unable to initialize the temporal DBMI interface. Use "
  166. "t.connect to specify the driver and the database string"))
  167. dbmi = sqlite3
  168. else:
  169. # Set the default sqlite3 connection in case nothing was defined
  170. core.run_command("t.connect", flags="d")
  171. db_exists = False
  172. database = get_temporal_dbmi_init_string()
  173. dbif = SQLDatabaseInterfaceConnection()
  174. # Check if the database already exists
  175. if tgis_backend == "sqlite":
  176. # Check path of the sqlite database
  177. if os.path.exists(database):
  178. dbif.connect()
  179. # Check for raster_base table
  180. dbif.cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='raster_base';")
  181. name = dbif.cursor.fetchone()
  182. if name and name[0] == "raster_base":
  183. db_exists = True
  184. dbif.close()
  185. elif tgis_backend == "pg":
  186. # Connect to database
  187. dbif.connect()
  188. # Check for raster_base table
  189. dbif.cursor.execute("SELECT EXISTS(SELECT * FROM information_schema.tables "
  190. "WHERE table_name=%s)", ('raster_base',))
  191. if dbif.cursor.fetchone()[0]:
  192. db_exists = True
  193. if db_exists:
  194. # Check if we have to add the command column
  195. add_command_col = True
  196. rows = get_tgis_metadata(dbif)
  197. if rows:
  198. for row in rows:
  199. if row["key"] == "tgis_db_version":
  200. version = int(row["value"])
  201. if version >= 1:
  202. add_command_col = False
  203. if add_command_col:
  204. # Try to add the command column to the space time dataset metadata tables
  205. # this is due backward compatibility with old databases
  206. try:
  207. dbif.cursor.execute('ALTER TABLE strds_metadata ADD COLUMN command VARCHAR;')
  208. except:
  209. pass
  210. try:
  211. dbif.cursor.execute('ALTER TABLE str3ds_metadata ADD COLUMN command VARCHAR;')
  212. except:
  213. pass
  214. try:
  215. dbif.cursor.execute('ALTER TABLE stvds_metadata ADD COLUMN command VARCHAR;')
  216. except:
  217. pass
  218. if db_exists == True:
  219. dbif.close()
  220. return
  221. create_temporal_database(dbif, database)
  222. ###############################################################################
  223. def create_temporal_database(dbif, database):
  224. """!This function will create the temporal database
  225. It will create all tables and triggers that are needed to run
  226. the temporal GIS
  227. @param dbif The database interface to be used
  228. """
  229. global tgis_backend
  230. global tgis_version
  231. global tgis_db_version
  232. template_path = get_sql_template_path()
  233. # Read all SQL scripts and templates
  234. map_tables_template_sql = open(os.path.join(
  235. template_path, "map_tables_template.sql"), 'r').read()
  236. raster_metadata_sql = open(os.path.join(
  237. get_sql_template_path(), "raster_metadata_table.sql"), 'r').read()
  238. raster3d_metadata_sql = open(os.path.join(template_path,
  239. "raster3d_metadata_table.sql"),
  240. 'r').read()
  241. vector_metadata_sql = open(os.path.join(template_path,
  242. "vector_metadata_table.sql"),
  243. 'r').read()
  244. raster_views_sql = open(os.path.join(template_path, "raster_views.sql"),
  245. 'r').read()
  246. raster3d_views_sql = open(os.path.join(template_path,
  247. "raster3d_views.sql"), 'r').read()
  248. vector_views_sql = open(os.path.join(template_path, "vector_views.sql"),
  249. 'r').read()
  250. stds_tables_template_sql = open(os.path.join(template_path,
  251. "stds_tables_template.sql"),
  252. 'r').read()
  253. strds_metadata_sql = open(os.path.join(template_path,
  254. "strds_metadata_table.sql"),
  255. 'r').read()
  256. str3ds_metadata_sql = open(os.path.join(template_path,
  257. "str3ds_metadata_table.sql"),
  258. 'r').read()
  259. stvds_metadata_sql = open(os.path.join(template_path,
  260. "stvds_metadata_table.sql"),
  261. 'r').read()
  262. strds_views_sql = open(os.path.join(template_path, "strds_views.sql"),
  263. 'r').read()
  264. str3ds_views_sql = open(os.path.join(template_path, "str3ds_views.sql"),
  265. 'r').read()
  266. stvds_views_sql = open(os.path.join(template_path, "stvds_views.sql"),
  267. 'r').read()
  268. # Create the raster, raster3d and vector tables SQL statements
  269. raster_tables_sql = map_tables_template_sql.replace("GRASS_MAP", "raster")
  270. vector_tables_sql = map_tables_template_sql.replace("GRASS_MAP", "vector")
  271. raster3d_tables_sql = map_tables_template_sql.replace(
  272. "GRASS_MAP", "raster3d")
  273. # Create the space-time raster, raster3d and vector dataset tables
  274. # SQL statements
  275. strds_tables_sql = stds_tables_template_sql.replace("STDS", "strds")
  276. stvds_tables_sql = stds_tables_template_sql.replace("STDS", "stvds")
  277. str3ds_tables_sql = stds_tables_template_sql.replace("STDS", "str3ds")
  278. core.message(_("Create temporal database: %s" % (database)))
  279. if tgis_backend == "sqlite":
  280. # We need to create the sqlite3 database path if it does not exists
  281. tgis_dir = os.path.dirname(database)
  282. if not os.path.exists(tgis_dir):
  283. os.makedirs(tgis_dir)
  284. # Sqlite needs some trigger to emulate the foreign keys
  285. sqlite3_delete_trigger_sql = open(os.path.join(template_path,
  286. "sqlite3_delete_trigger.sql"),
  287. 'r').read()
  288. # Connect now to the database
  289. if not dbif.connected:
  290. dbif.connect()
  291. # Execute the SQL statements for sqlite
  292. # Create the global tables for the native grass datatypes
  293. dbif.execute_transaction(raster_tables_sql)
  294. dbif.execute_transaction(raster_metadata_sql)
  295. dbif.execute_transaction(raster_views_sql)
  296. dbif.execute_transaction(vector_tables_sql)
  297. dbif.execute_transaction(vector_metadata_sql)
  298. dbif.execute_transaction(vector_views_sql)
  299. dbif.execute_transaction(raster3d_tables_sql)
  300. dbif.execute_transaction(raster3d_metadata_sql)
  301. dbif.execute_transaction(raster3d_views_sql)
  302. # Create the tables for the new space-time datatypes
  303. dbif.execute_transaction(strds_tables_sql)
  304. dbif.execute_transaction(strds_metadata_sql)
  305. dbif.execute_transaction(strds_views_sql)
  306. dbif.execute_transaction(stvds_tables_sql)
  307. dbif.execute_transaction(stvds_metadata_sql)
  308. dbif.execute_transaction(stvds_views_sql)
  309. dbif.execute_transaction(str3ds_tables_sql)
  310. dbif.execute_transaction(str3ds_metadata_sql)
  311. dbif.execute_transaction(str3ds_views_sql)
  312. if tgis_backend == "sqlite":
  313. dbif.execute_transaction(sqlite3_delete_trigger_sql)
  314. # Create the tgis metadata table to store the database
  315. # initial configuration
  316. # The metadata table content
  317. metadata = {}
  318. metadata["tgis_version"] = tgis_version
  319. metadata["tgis_db_version"] = tgis_db_version
  320. metadata["has_command_column"] = True
  321. metadata["creation_time"] = datetime.today()
  322. _create_tgis_metadata_table(metadata, dbif)
  323. dbif.close()
  324. ###############################################################################
  325. def _create_tgis_metadata_table(content, dbif=None):
  326. """!Create the temporal gis metadata table which stores all metadata
  327. information about the temporal database.
  328. @param content The dictionary that stores the key:value metadata
  329. that should be stored in the metadata table
  330. @param dbif The database interface to be used
  331. """
  332. dbif, connected = init_dbif(dbif)
  333. statement = "CREATE TABLE tgis_metadata (key VARCHAR NOT NULL, value VARCHAR);\n";
  334. dbif.execute_transaction(statement)
  335. for key in content.keys():
  336. statement = "INSERT INTO tgis_metadata (key, value) VALUES " + \
  337. "(\'%s\' , \'%s\');\n"%(str(key), str(content[key]))
  338. dbif.execute_transaction(statement)
  339. if connected:
  340. dbif.close()
  341. ###############################################################################
  342. class SQLDatabaseInterfaceConnection():
  343. """!This class represents the database interface connection
  344. and provides access to the chisen backend modules.
  345. The following DBMS are supported:
  346. - sqlite via the sqlite3 standard library
  347. - postgresql via psycopg2
  348. """
  349. def __init__(self):
  350. self.connected = False
  351. global tgis_backend
  352. if tgis_backend == "sqlite":
  353. self.dbmi = sqlite3
  354. else:
  355. self.dbmi = psycopg2
  356. def rollback(self):
  357. """
  358. Roll back the last transaction. This must be called
  359. in case a new query should be performed after a db error.
  360. This is only relevant for postgresql database.
  361. """
  362. if self.dbmi.__name__ == "psycopg2":
  363. if self.connected:
  364. self.connection.rollback()
  365. def connect(self):
  366. """!Connect to the DBMI to execute SQL statements
  367. Supported backends are sqlite3 and postgresql
  368. """
  369. self.database = get_temporal_dbmi_init_string()
  370. #print "Connect to", self.database
  371. if self.dbmi.__name__ == "sqlite3":
  372. self.connection = self.dbmi.connect(self.database,
  373. detect_types = self.dbmi.PARSE_DECLTYPES | self.dbmi.PARSE_COLNAMES)
  374. self.connection.row_factory = self.dbmi.Row
  375. self.connection.isolation_level = None
  376. self.cursor = self.connection.cursor()
  377. self.cursor.execute("PRAGMA synchronous = OFF")
  378. self.cursor.execute("PRAGMA journal_mode = MEMORY")
  379. elif self.dbmi.__name__ == "psycopg2":
  380. self.connection = self.dbmi.connect(self.database)
  381. #self.connection.set_isolation_level(dbmi.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
  382. self.cursor = self.connection.cursor(
  383. cursor_factory = self.dbmi.extras.DictCursor)
  384. self.connected = True
  385. def close(self):
  386. """!Close the DBMI connection"""
  387. #print "Close connection to", self.database
  388. self.connection.commit()
  389. self.cursor.close()
  390. self.connected = False
  391. def mogrify_sql_statement(self, content):
  392. """!Return the SQL statement and arguments as executable SQL string
  393. @param content The content as tuple with two entries, the first
  394. entry is the SQL statement with DBMI specific
  395. place holder (?), the second entry is the argument
  396. list that should substitue the place holder.
  397. Usage:
  398. @code
  399. >>> init()
  400. >>> dbif = SQLDatabaseInterfaceConnection()
  401. >>> dbif.mogrify_sql_statement(["SELECT ctime FROM raster_base WHERE id = ?",
  402. ... ["soil@PERMANENT",]])
  403. "SELECT ctime FROM raster_base WHERE id = 'soil@PERMANENT'"
  404. @endcode
  405. """
  406. sql = content[0]
  407. args = content[1]
  408. if self.dbmi.__name__ == "psycopg2":
  409. if len(args) == 0:
  410. return sql
  411. else:
  412. if self.connected:
  413. try:
  414. return self.cursor.mogrify(sql, args)
  415. except:
  416. print sql, args
  417. raise
  418. else:
  419. self.connect()
  420. statement = self.cursor.mogrify(sql, args)
  421. self.close()
  422. return statement
  423. elif self.dbmi.__name__ == "sqlite3":
  424. if len(args) == 0:
  425. return sql
  426. else:
  427. # Unfortunately as sqlite does not support
  428. # the transformation of sql strings and qmarked or
  429. # named arguments we must make our hands dirty
  430. # and do it by ourself. :(
  431. # Doors are open for SQL injection because of the
  432. # limited python sqlite3 implementation!!!
  433. pos = 0
  434. count = 0
  435. maxcount = 100
  436. statement = sql
  437. while count < maxcount:
  438. pos = statement.find("?", pos + 1)
  439. if pos == -1:
  440. break
  441. if args[count] is None:
  442. statement = "%sNULL%s" % (statement[0:pos],
  443. statement[pos + 1:])
  444. elif isinstance(args[count], (int, long)):
  445. statement = "%s%d%s" % (statement[0:pos], args[count],
  446. statement[pos + 1:])
  447. elif isinstance(args[count], float):
  448. statement = "%s%f%s" % (statement[0:pos], args[count],
  449. statement[pos + 1:])
  450. else:
  451. # Default is a string, this works for datetime
  452. # objects too
  453. statement = "%s\'%s\'%s" % (statement[0:pos],
  454. str(args[count]),
  455. statement[pos + 1:])
  456. count += 1
  457. return statement
  458. def execute_transaction(self, statement):
  459. """!Execute a transactional SQL statement
  460. The BEGIN and END TRANSACTION statements will be added automatically
  461. to the sql statement
  462. @param statement The executable SQL statement or SQL script
  463. """
  464. connected = False
  465. if not self.connected:
  466. self.connect()
  467. connected = True
  468. sql_script = ""
  469. sql_script += "BEGIN TRANSACTION;\n"
  470. sql_script += statement
  471. sql_script += "END TRANSACTION;"
  472. try:
  473. if self.dbmi.__name__ == "sqlite3":
  474. self.cursor.executescript(statement)
  475. else:
  476. self.cursor.execute(statement)
  477. self.connection.commit()
  478. except:
  479. if connected:
  480. self.close()
  481. core.error(_("Unable to execute transaction:\n %(sql)s" % \
  482. {"sql":statement}))
  483. raise
  484. if connected:
  485. self.close()
  486. ###############################################################################
  487. def init_dbif(dbif):
  488. """!This method checks if the database interface connection exists,
  489. if not a new one will be created, connected and True will be returned
  490. Usage code sample:
  491. @code
  492. dbif, connect = tgis.init_dbif(None)
  493. sql = dbif.mogrify_sql_statement(["SELECT * FROM raster_base WHERE ? = ?"],
  494. ["id", "soil@PERMANENT"])
  495. dbif.execute_transaction(sql)
  496. if connect:
  497. dbif.close()
  498. @endcode
  499. """
  500. if dbif is None:
  501. dbif = SQLDatabaseInterfaceConnection()
  502. dbif.connect()
  503. return dbif, True
  504. return dbif, False
  505. ###############################################################################
  506. if __name__ == "__main__":
  507. import doctest
  508. doctest.testmod()