You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

393 line
12 KiB

  1. #!/usr/bin/env python3
  2. # Kosmorro - Compute The Next Ephemerides
  3. # Copyright (C) 2019 Jérôme Deuchnord <jerome@deuchnord.fr>
  4. #
  5. # This program is free software: you can redistribute it and/or modify
  6. # it under the terms of the GNU Affero General Public License as
  7. # published by the Free Software Foundation, either version 3 of the
  8. # License, or (at your option) any later version.
  9. #
  10. # This program is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. import argparse
  18. import argcomplete
  19. import sys
  20. import os.path
  21. import pytz
  22. from babel.dates import format_date
  23. from kosmorrolib import Position, get_ephemerides, get_events, get_moon_phase
  24. from kosmorrolib.exceptions import OutOfRangeDateError
  25. from datetime import date
  26. from . import dumper, environment, debug
  27. from .date import parse_date
  28. from .geolocation import get_position
  29. from .utils import (
  30. KOSMORRO_VERSION,
  31. KOSMORROLIB_VERSION,
  32. colored,
  33. set_colors_activated,
  34. print_stderr,
  35. get_timezone,
  36. )
  37. from .exceptions import (
  38. InvalidOutputFormatError,
  39. UnavailableFeatureError,
  40. OutOfRangeDateError as DateRangeError,
  41. )
  42. from kosmorro.i18n.utils import _
  43. def run():
  44. env_vars = environment.get_env_vars()
  45. output_formats = get_dumpers()
  46. args = get_args(list(output_formats.keys()))
  47. debug.show_debug_messages = args.show_debug_messages
  48. output_format = args.format
  49. set_colors_activated(args.colors)
  50. if args.completion is not None:
  51. return 0 if output_completion(args.completion) else 1
  52. if args.special_action is not None:
  53. return 0 if args.special_action() else 1
  54. try:
  55. compute_date = parse_date(args.date)
  56. except ValueError as error:
  57. print_stderr(colored(error.args[0], color="red", attrs=["bold"]))
  58. return -1
  59. position = None
  60. if args.position not in [None, ""]:
  61. position = get_position(args.position)
  62. elif env_vars.position not in [None, ""]:
  63. position = get_position(env_vars.position)
  64. # if output format is not specified, try to use output file extension as output format
  65. if args.output is not None and output_format is None:
  66. file_extension = os.path.splitext(args.output)[-1][1:].lower()
  67. if file_extension:
  68. output_format = file_extension
  69. # default to .txt if output format was not given and output file did not have file extension
  70. if output_format is None:
  71. output_format = "txt"
  72. if output_format == "pdf":
  73. print(
  74. _(
  75. "Save the planet and paper!\n"
  76. "Consider printing your PDF document only if really necessary, and use the other side of the sheet."
  77. )
  78. )
  79. if position is None:
  80. print_stderr(
  81. colored(
  82. _(
  83. "PDF output will not contain the ephemerides, because you didn't provide the observation "
  84. "coordinates."
  85. ),
  86. "yellow",
  87. )
  88. )
  89. timezone = 0
  90. try:
  91. if args.timezone is not None:
  92. timezone = get_timezone(args.timezone, compute_date)
  93. elif env_vars.tz is not None:
  94. timezone = get_timezone(env_vars.tz, compute_date)
  95. elif env_vars.timezone is not None:
  96. print_stderr(
  97. colored(
  98. _(
  99. "Environment variable KOSMORRO_TIMEZONE is deprecated. Use TZ instead, which is more standard."
  100. ),
  101. "yellow",
  102. )
  103. )
  104. timezone = get_timezone(env_vars.timezone, compute_date)
  105. except pytz.UnknownTimeZoneError as error:
  106. print_stderr(
  107. colored(
  108. _("Unknown timezone: {timezone}").format(timezone=error.args[0]),
  109. color="red",
  110. )
  111. )
  112. return -1
  113. try:
  114. use_colors = not environment.NO_COLOR and args.colors
  115. output = get_information(
  116. compute_date,
  117. position,
  118. timezone,
  119. output_format,
  120. use_colors,
  121. args.show_graph,
  122. )
  123. except InvalidOutputFormatError as error:
  124. print_stderr(colored(error.msg, "red"))
  125. debug.debug_print(error)
  126. return 3
  127. except UnavailableFeatureError as error:
  128. print_stderr(colored(error.msg, "red"))
  129. debug.debug_print(error)
  130. return 2
  131. except DateRangeError as error:
  132. print_stderr(colored(error.msg, "red"))
  133. debug.debug_print(error)
  134. return 1
  135. if args.output is not None:
  136. try:
  137. file_content = output.to_string()
  138. opening_mode = get_opening_mode(output_format)
  139. with open(args.output, opening_mode) as output_file:
  140. output_file.write(file_content)
  141. except UnavailableFeatureError as error:
  142. print_stderr(colored(error.msg, "red"))
  143. debug.debug_print(error)
  144. return 2
  145. except OSError as error:
  146. print_stderr(
  147. colored(
  148. _('The file could not be saved in "{path}": {error}').format(
  149. path=args.output, error=error.strerror
  150. ),
  151. "red",
  152. )
  153. )
  154. debug.debug_print(error)
  155. return 3
  156. elif not output.is_file_output_needed():
  157. print(output)
  158. else:
  159. print_stderr(
  160. colored(
  161. _("Please provide a file path to export in this format (--output)."),
  162. color="red",
  163. )
  164. )
  165. return 1
  166. return 0
  167. def get_information(
  168. compute_date: date,
  169. position: Position,
  170. timezone: int,
  171. output_format: str,
  172. colors: bool,
  173. show_graph: bool,
  174. ) -> dumper.Dumper:
  175. try:
  176. if position is not None:
  177. eph = get_ephemerides(
  178. for_date=compute_date, position=position, timezone=timezone
  179. )
  180. else:
  181. eph = []
  182. try:
  183. moon_phase = get_moon_phase(for_date=compute_date, timezone=timezone)
  184. except OutOfRangeDateError as error:
  185. moon_phase = None
  186. print_stderr(
  187. colored(
  188. _(
  189. "Moon phase can only be computed between {min_date} and {max_date}"
  190. ).format(
  191. min_date=format_date(error.min_date, "long"),
  192. max_date=format_date(error.max_date, "long"),
  193. ),
  194. "yellow",
  195. )
  196. )
  197. events_list = get_events(compute_date, timezone)
  198. return get_dumpers()[output_format](
  199. ephemerides=eph,
  200. moon_phase=moon_phase,
  201. events=events_list,
  202. date=compute_date,
  203. timezone=timezone,
  204. with_colors=colors,
  205. show_graph=show_graph,
  206. )
  207. except KeyError as error:
  208. raise InvalidOutputFormatError(output_format, list(get_dumpers().keys()))
  209. except OutOfRangeDateError as error:
  210. raise DateRangeError(error.min_date, error.max_date)
  211. def get_dumpers() -> {str: dumper.Dumper}:
  212. return {
  213. "txt": dumper.TextDumper,
  214. "json": dumper.JsonDumper,
  215. "pdf": dumper.PdfDumper,
  216. "tex": dumper.LatexDumper,
  217. }
  218. def get_opening_mode(format: str) -> str:
  219. if format == "pdf":
  220. return "wb"
  221. return "w"
  222. def output_version() -> bool:
  223. python_version = "%d.%d.%d" % (
  224. sys.version_info[0],
  225. sys.version_info[1],
  226. sys.version_info[2],
  227. )
  228. print("Kosmorro %s" % KOSMORRO_VERSION)
  229. print(
  230. _(
  231. "Running on Python {python_version} "
  232. "with Kosmorrolib v{kosmorrolib_version}"
  233. ).format(python_version=python_version, kosmorrolib_version=KOSMORROLIB_VERSION)
  234. )
  235. return True
  236. def get_args(output_formats: [str]):
  237. today = date.today()
  238. parser = argparse.ArgumentParser(
  239. description=_(
  240. "Compute the ephemerides and the events for a given date and a given position on Earth."
  241. ),
  242. epilog=_(
  243. "By default, only the events will be computed for today.\n"
  244. "To compute also the ephemerides, latitude and longitude arguments are needed."
  245. ),
  246. )
  247. parser.add_argument(
  248. "--version",
  249. "-v",
  250. dest="special_action",
  251. action="store_const",
  252. const=output_version,
  253. default=None,
  254. help=_("Show the program version"),
  255. )
  256. parser.add_argument(
  257. "--format",
  258. "-f",
  259. type=str,
  260. default=None,
  261. choices=output_formats,
  262. help=_(
  263. "The format to output the information to. If not provided, the output format "
  264. "will be inferred from the file extension of the output file."
  265. ),
  266. )
  267. parser.add_argument(
  268. "--position",
  269. "-p",
  270. type=str,
  271. default=None,
  272. help=_(
  273. 'The observer\'s position on Earth, in the "{latitude},{longitude}" format. '
  274. "Can also be set in the KOSMORRO_POSITION environment variable."
  275. ),
  276. )
  277. parser.add_argument(
  278. "--date",
  279. "-d",
  280. type=str,
  281. default=today.strftime("%Y-%m-%d"),
  282. help=_(
  283. "The date for which the ephemerides must be calculated. Can be in the YYYY-MM-DD format "
  284. 'or an interval in the "[+-]YyMmDd" format (with Y, M, and D numbers). '
  285. "Defaults to current date."
  286. ).format(default_date=today.strftime("%Y-%m-%d")),
  287. )
  288. parser.add_argument(
  289. "--timezone",
  290. "-t",
  291. type=str,
  292. default=None,
  293. help=_(
  294. "The timezone to use to display the hours. It can be either a number (e.g. 1 for UTC+1) or a timezone name (e.g. Europe/Paris). "
  295. "See https://en.wikipedia.org/wiki/List_of_tz_database_time_zones to find your timezone. "
  296. "Can also be set in the TZ environment variable."
  297. ),
  298. )
  299. parser.add_argument(
  300. "--no-colors",
  301. dest="colors",
  302. action="store_false",
  303. help=_("Disable the colors in the console."),
  304. )
  305. parser.add_argument(
  306. "--output",
  307. "-o",
  308. type=str,
  309. default=None,
  310. help=_(
  311. "A file to export the output to. If not given, the standard output is used. "
  312. "This argument is needed for PDF format."
  313. ),
  314. )
  315. parser.add_argument(
  316. "--no-graph",
  317. dest="show_graph",
  318. action="store_false",
  319. help=_(
  320. "Do not generate a graph to represent the rise and set times in the LaTeX or PDF file."
  321. ),
  322. )
  323. parser.add_argument(
  324. "--debug",
  325. dest="show_debug_messages",
  326. action="store_true",
  327. help=_("Show debugging messages"),
  328. )
  329. argcomplete.autocomplete(parser)
  330. parser.add_argument(
  331. "--completion",
  332. type=str,
  333. help=_("Print a script allowing completion for your shell"),
  334. )
  335. return parser.parse_args()
  336. def output_completion(shell: str) -> bool:
  337. shellcode = argcomplete.shellcode([sys.argv[0]], shell=shell)
  338. if shellcode == "":
  339. print_stderr(
  340. colored(_("No completion script available for this shell."), "red")
  341. )
  342. return False
  343. print(shellcode)
  344. return True
  345. def main():
  346. sys.exit(run())