core.py 24 KB

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