core.py 20 KB

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