list_stds.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549
  1. """
  2. Functions to create space time dataset lists
  3. Usage:
  4. .. code-block:: python
  5. import grass.temporal as tgis
  6. tgis.register_maps_in_space_time_dataset(type, name, maps)
  7. (C) 2012-2022 by the GRASS Development Team
  8. This program is free software under the GNU General Public
  9. License (>=v2). Read the file COPYING that comes with GRASS GIS
  10. for details.
  11. :authors: Soeren Gebbert
  12. :authors: Vaclav Petras
  13. """
  14. import os
  15. from contextlib import contextmanager
  16. import sys
  17. import grass.script as gs
  18. from .core import get_tgis_message_interface, get_available_temporal_mapsets, init_dbif
  19. from .datetime_math import time_delta_to_relative_time
  20. from .factory import dataset_factory
  21. from .open_stds import open_old_stds
  22. ###############################################################################
  23. def get_dataset_list(
  24. type, temporal_type, columns=None, where=None, order=None, dbif=None
  25. ):
  26. """Return a list of time stamped maps or space time datasets of a specific
  27. temporal type that are registered in the temporal database
  28. This method returns a dictionary, the keys are the available mapsets,
  29. the values are the rows from the SQL database query.
  30. :param type: The type of the datasets (strds, str3ds, stvds, raster,
  31. raster_3d, vector)
  32. :param temporal_type: The temporal type of the datasets (absolute,
  33. relative)
  34. :param columns: A comma separated list of columns that will be selected
  35. :param where: A where statement for selected listing without "WHERE"
  36. :param order: A comma separated list of columns to order the
  37. datasets by category
  38. :param dbif: The database interface to be used
  39. :return: A dictionary with the rows of the SQL query for each
  40. available mapset
  41. .. code-block:: python
  42. >>> import grass.temporal as tgis
  43. >>> tgis.core.init()
  44. >>> name = "list_stds_test"
  45. >>> sp = tgis.open_stds.open_new_stds(name=name, type="strds",
  46. ... temporaltype="absolute", title="title", descr="descr",
  47. ... semantic="mean", dbif=None, overwrite=True)
  48. >>> mapset = tgis.get_current_mapset()
  49. >>> stds_list = tgis.list_stds.get_dataset_list("strds", "absolute", columns="name")
  50. >>> rows = stds_list[mapset]
  51. >>> for row in rows:
  52. ... if row["name"] == name:
  53. ... print(True)
  54. True
  55. >>> stds_list = tgis.list_stds.get_dataset_list("strds", "absolute", columns="name,mapset", where="mapset = '%s'"%(mapset))
  56. >>> rows = stds_list[mapset]
  57. >>> for row in rows:
  58. ... if row["name"] == name and row["mapset"] == mapset:
  59. ... print(True)
  60. True
  61. >>> check = sp.delete()
  62. """
  63. id = None
  64. sp = dataset_factory(type, id)
  65. dbif, connection_state_changed = init_dbif(dbif)
  66. mapsets = get_available_temporal_mapsets()
  67. result = {}
  68. for mapset in mapsets.keys():
  69. if temporal_type == "absolute":
  70. table = sp.get_type() + "_view_abs_time"
  71. else:
  72. table = sp.get_type() + "_view_rel_time"
  73. if columns and columns.find("all") == -1:
  74. sql = "SELECT " + str(columns) + " FROM " + table
  75. else:
  76. sql = "SELECT * FROM " + table
  77. if where:
  78. sql += " WHERE " + where
  79. sql += " AND mapset = '%s'" % (mapset)
  80. else:
  81. sql += " WHERE mapset = '%s'" % (mapset)
  82. if order:
  83. sql += " ORDER BY " + order
  84. dbif.execute(sql, mapset=mapset)
  85. rows = dbif.fetchall(mapset=mapset)
  86. if rows:
  87. result[mapset] = rows
  88. if connection_state_changed:
  89. dbif.close()
  90. return result
  91. ###############################################################################
  92. @contextmanager
  93. def _open_output_file(file, encoding="utf-8", **kwargs):
  94. if not file:
  95. yield sys.stdout
  96. elif not isinstance(file, (str, os.PathLike)):
  97. yield file
  98. else:
  99. with open(file, "w", encoding=encoding, **kwargs) as stream:
  100. yield stream
  101. def _write_line(items, separator, file):
  102. if not separator:
  103. separator = ","
  104. output = separator.join([f"{item}" for item in items])
  105. with _open_output_file(file) as stream:
  106. print(f"{output}", file=stream)
  107. def _write_plain(rows, header, separator, file):
  108. def write_plain_row(items, separator, file):
  109. output = separator.join([f"{item}" for item in items])
  110. print(f"{output}", file=file)
  111. with _open_output_file(file) as stream:
  112. # Print the column names if requested
  113. if header:
  114. write_plain_row(items=header, separator=separator, file=stream)
  115. for row in rows:
  116. write_plain_row(items=row, separator=separator, file=stream)
  117. def _write_json(rows, column_names, file):
  118. # Lazy import output format-specific dependencies.
  119. # pylint: disable=import-outside-toplevel
  120. import json
  121. import datetime
  122. class ResultsEncoder(json.JSONEncoder):
  123. """Results encoder for JSON which handles SimpleNamespace objects"""
  124. def default(self, o):
  125. """Handle additional types"""
  126. if isinstance(o, datetime.datetime):
  127. return f"{o}"
  128. return super().default(o)
  129. dict_rows = []
  130. for row in rows:
  131. new_row = {}
  132. for key, value in zip(column_names, row):
  133. new_row[key] = value
  134. dict_rows.append(new_row)
  135. meta = {"column_names": column_names}
  136. with _open_output_file(file) as stream:
  137. json.dump({"data": dict_rows, "metadata": meta}, stream, cls=ResultsEncoder)
  138. def _write_yaml(rows, column_names, file=sys.stdout):
  139. # Lazy import output format-specific dependencies.
  140. # pylint: disable=import-outside-toplevel
  141. import yaml
  142. class NoAliasIndentListSafeDumper(yaml.SafeDumper):
  143. """YAML dumper class which does not create aliases and indents lists
  144. This avoid dates being labeled with &id001 and referenced with *id001.
  145. Instead, same dates are simply repeated.
  146. Lists have their dash-space (- ) indented instead of considering the
  147. dash and space to be a part of indentation. This might be better handled
  148. when https://github.com/yaml/pyyaml/issues/234 is resolved.
  149. """
  150. def ignore_aliases(self, data):
  151. return True
  152. def increase_indent(self, flow=False, indentless=False):
  153. return super().increase_indent(flow=flow, indentless=False)
  154. dict_rows = []
  155. for row in rows:
  156. new_row = {}
  157. for key, value in zip(column_names, row):
  158. new_row[key] = value
  159. dict_rows.append(new_row)
  160. meta = {"column_names": column_names}
  161. with _open_output_file(file) as stream:
  162. print(
  163. yaml.dump(
  164. {"data": dict_rows, "metadata": meta},
  165. Dumper=NoAliasIndentListSafeDumper,
  166. default_flow_style=False,
  167. ),
  168. end="",
  169. file=stream,
  170. )
  171. def _write_csv(rows, column_names, separator, file=sys.stdout):
  172. # Lazy import output format-specific dependencies.
  173. # pylint: disable=import-outside-toplevel
  174. import csv
  175. # Newlines handled by the CSV writter. Set according to the package doc.
  176. with _open_output_file(file, newline="") as stream:
  177. spamwriter = csv.writer(
  178. stream,
  179. delimiter=separator,
  180. quotechar='"',
  181. doublequote=True,
  182. quoting=csv.QUOTE_NONNUMERIC,
  183. lineterminator="\n",
  184. )
  185. if column_names:
  186. spamwriter.writerow(column_names)
  187. for row in rows:
  188. spamwriter.writerow(row)
  189. def _write_table(rows, column_names, output_format, separator, file):
  190. if output_format == "json":
  191. _write_json(rows=rows, column_names=column_names, file=file)
  192. elif output_format == "yaml":
  193. _write_yaml(rows=rows, column_names=column_names, file=file)
  194. elif output_format == "plain":
  195. # No particular reason for this separator expect that this is the original behavior.
  196. if not separator:
  197. separator = "\t"
  198. _write_plain(rows=rows, header=column_names, separator=separator, file=file)
  199. elif output_format == "csv":
  200. if not separator:
  201. separator = ","
  202. _write_csv(rows=rows, column_names=column_names, separator=separator, file=file)
  203. else:
  204. raise ValueError(f"Unknown value '{output_format}' for output_format")
  205. def _get_get_registered_maps_as_objects_with_method(dataset, where, method, gran, dbif):
  206. if method == "deltagaps":
  207. return dataset.get_registered_maps_as_objects_with_gaps(where=where, dbif=dbif)
  208. if method == "delta":
  209. return dataset.get_registered_maps_as_objects(
  210. where=where, order="start_time", dbif=dbif
  211. )
  212. if method == "gran":
  213. if where:
  214. raise ValueError(
  215. f"The where parameter is not supported with method={method}"
  216. )
  217. if gran is not None and gran != "":
  218. return dataset.get_registered_maps_as_objects_by_granularity(
  219. gran=gran, dbif=dbif
  220. )
  221. return dataset.get_registered_maps_as_objects_by_granularity(dbif=dbif)
  222. raise ValueError(f"Invalid method '{method}'")
  223. def _get_get_registered_maps_as_objects_delta_gran(
  224. dataset, where, method, gran, dbif, msgr
  225. ):
  226. maps = _get_get_registered_maps_as_objects_with_method(
  227. dataset=dataset, where=where, method=method, gran=gran, dbif=dbif
  228. )
  229. if not maps:
  230. return []
  231. if isinstance(maps[0], list):
  232. if len(maps[0]) > 0:
  233. first_time, unused = maps[0][0].get_temporal_extent_as_tuple()
  234. else:
  235. msgr.warning(_("Empty map list"))
  236. return []
  237. else:
  238. first_time, unused = maps[0].get_temporal_extent_as_tuple()
  239. records = []
  240. for map_object in maps:
  241. if isinstance(map_object, list):
  242. if len(map_object) > 0:
  243. map_object = map_object[0]
  244. else:
  245. msgr.fatal(_("Empty entry in map list, this should not happen"))
  246. start, end = map_object.get_temporal_extent_as_tuple()
  247. if end:
  248. delta = end - start
  249. else:
  250. delta = None
  251. delta_first = start - first_time
  252. if map_object.is_time_absolute():
  253. if end:
  254. delta = time_delta_to_relative_time(delta)
  255. delta_first = time_delta_to_relative_time(delta_first)
  256. records.append((map_object, start, end, delta, delta_first))
  257. return records
  258. def _get_list_of_maps_delta_gran(dataset, columns, where, method, gran, dbif, msgr):
  259. maps = _get_get_registered_maps_as_objects_delta_gran(
  260. dataset=dataset, where=where, method=method, gran=gran, dbif=dbif, msgr=msgr
  261. )
  262. rows = []
  263. for map_object, start, end, delta, delta_first in maps:
  264. row = []
  265. # Here the names must be the same as in the database
  266. # to make the interface consistent.
  267. for column in columns:
  268. if column == "id":
  269. row.append(map_object.get_id())
  270. elif column == "name":
  271. row.append(map_object.get_name())
  272. elif column == "layer":
  273. row.append(map_object.get_layer())
  274. elif column == "mapset":
  275. row.append(map_object.get_mapset())
  276. elif column == "start_time":
  277. row.append(start)
  278. elif column == "end_time":
  279. row.append(end)
  280. elif column == "interval_length":
  281. row.append(delta)
  282. elif column == "distance_from_begin":
  283. row.append(delta_first)
  284. else:
  285. raise ValueError(f"Unsupported column '{column}'")
  286. rows.append(row)
  287. return rows
  288. def _get_list_of_maps_stds(
  289. element_type,
  290. name,
  291. columns,
  292. order,
  293. where,
  294. method,
  295. output_format,
  296. gran=None,
  297. dbif=None,
  298. ):
  299. dbif, connection_state_changed = init_dbif(dbif)
  300. msgr = get_tgis_message_interface()
  301. dataset = open_old_stds(name, element_type, dbif)
  302. def check_columns(column_names, output_format, element_type):
  303. if element_type != "stvds" and "layer" in columns:
  304. raise ValueError(
  305. f"Column 'layer' is not allowed with temporal type '{element_type}'"
  306. )
  307. if output_format == "line" and len(column_names) > 1:
  308. raise ValueError(
  309. f"'{output_format}' output_format can have only 1 column, "
  310. f"not {len(column_names)}"
  311. )
  312. # This method expects a list of objects for gap detection
  313. if method in ["delta", "deltagaps", "gran"]:
  314. if not columns:
  315. if output_format == "list":
  316. # Only one column is needed.
  317. columns = ["id"]
  318. else:
  319. columns = ["id", "name"]
  320. if element_type == "stvds":
  321. columns.append("layer")
  322. columns.extend(
  323. [
  324. "mapset",
  325. "start_time",
  326. "end_time",
  327. "interval_length",
  328. "distance_from_begin",
  329. ]
  330. )
  331. check_columns(
  332. column_names=columns,
  333. output_format=output_format,
  334. element_type=element_type,
  335. )
  336. rows = _get_list_of_maps_delta_gran(
  337. dataset=dataset,
  338. columns=columns,
  339. where=where,
  340. method=method,
  341. gran=gran,
  342. dbif=dbif,
  343. msgr=msgr,
  344. )
  345. else:
  346. if columns:
  347. check_columns(
  348. column_names=columns,
  349. output_format=output_format,
  350. element_type=element_type,
  351. )
  352. else:
  353. if output_format == "line":
  354. # For list of values, only one column is needed.
  355. columns = ["id"]
  356. else:
  357. columns = ["name", "mapset", "start_time", "end_time"]
  358. if not order:
  359. order = "start_time"
  360. rows = dataset.get_registered_maps(",".join(columns), where, order, dbif)
  361. # End with error for the old, custom formats. Proper formats simply return
  362. # empty result whatever empty is for each format (e.g., empty list for JSON).
  363. if not rows and (output_format in ["plain", "line"]):
  364. dbif.close()
  365. gs.fatal(
  366. _(
  367. "Nothing found in the database for space time dataset <{name}> "
  368. "(type: {element_type}): {detail}"
  369. ).format(
  370. name=dataset.get_id(),
  371. element_type=element_type,
  372. detail=_(
  373. "Dataset is empty or where clause is too constrained or incorrect"
  374. )
  375. if where
  376. else _("Dataset is empty"),
  377. )
  378. )
  379. if connection_state_changed:
  380. dbif.close()
  381. return rows, columns
  382. # The code is compatible with pre-v8.2 versions, but for v9, it needs to be reviewed
  383. # to remove the backwards compatibility which will clean it up.
  384. def list_maps_of_stds(
  385. type, # pylint: disable=redefined-builtin
  386. input, # pylint: disable=redefined-builtin
  387. columns,
  388. order,
  389. where,
  390. separator,
  391. method,
  392. no_header=False,
  393. gran=None,
  394. dbif=None,
  395. outpath=None,
  396. output_format=None,
  397. ):
  398. """List the maps of a space time dataset using different methods
  399. :param type: The type of the maps raster, raster3d or vector
  400. :param input: Name of a space time raster dataset
  401. :param columns: A comma separated list of columns to be printed to stdout
  402. :param order: A comma separated list of columns to order the
  403. maps by category
  404. :param where: A where statement for selected listing without "WHERE"
  405. e.g: start_time < "2001-01-01" and end_time > "2001-01-01"
  406. :param separator: The field separator character between the columns
  407. :param method: String identifier to select a method out of cols,
  408. comma,delta or deltagaps
  409. :param dbif: The database interface to be used
  410. - "cols" Print preselected columns specified by columns
  411. - "comma" Print the map ids ("name@mapset") as comma separated string
  412. - "delta" Print the map ids ("name@mapset") with start time,
  413. end time, relative length of intervals and the relative
  414. distance to the begin
  415. - "deltagaps" Same as "delta" with additional listing of gaps.
  416. Gaps can be easily identified as the id is "None"
  417. - "gran" List map using the granularity of the space time dataset,
  418. columns are identical to deltagaps
  419. :param no_header: Suppress the printing of column names
  420. :param gran: The user defined granule to be used if method=gran is
  421. set, in case gran=None the granule of the space time
  422. dataset is used
  423. :param outpath: The path to file where to save output
  424. """
  425. if not output_format:
  426. if method == "comma":
  427. output_format = "line"
  428. output_format = "plain"
  429. if columns:
  430. if isinstance(columns, str):
  431. columns = columns.split(",")
  432. rows, columns = _get_list_of_maps_stds(
  433. element_type=type,
  434. name=input,
  435. columns=columns,
  436. order=order,
  437. where=where,
  438. method=method,
  439. output_format=output_format,
  440. gran=gran,
  441. dbif=dbif,
  442. )
  443. if output_format == "line":
  444. _write_line(
  445. items=[row[0] for row in rows],
  446. separator=separator,
  447. file=outpath,
  448. )
  449. else:
  450. _write_table(
  451. rows=rows,
  452. column_names=None if no_header else columns,
  453. separator=separator,
  454. output_format=output_format,
  455. file=outpath,
  456. )
  457. ###############################################################################
  458. if __name__ == "__main__":
  459. import doctest
  460. doctest.testmod()