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.
 
 
 
 

355 lines
10 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 locale
  19. import sys
  20. import os.path
  21. import platform
  22. import colorama
  23. from kosmorrolib import Position, get_ephemerides, get_events, get_moon_phase
  24. from kosmorrolib.__version__ import __version__ as kosmorrolib_version
  25. from kosmorrolib.exceptions import OutOfRangeDateError
  26. from datetime import date
  27. from termcolor import colored
  28. from . import dumper, environment, debug
  29. from .date import parse_date
  30. from .geolocation import get_position
  31. from .__version__ import __version__ as kosmorro_version
  32. from .exceptions import (
  33. InvalidOutputFormatError,
  34. UnavailableFeatureError,
  35. OutOfRangeDateError as DateRangeError,
  36. )
  37. from kosmorro.i18n.utils import _, SHORT_DATE_FORMAT
  38. def main():
  39. colorama.init()
  40. env_vars = environment.get_env_vars()
  41. output_formats = get_dumpers()
  42. args = get_args(list(output_formats.keys()))
  43. debug.show_debug_messages = args.show_debug_messages
  44. output_format = args.format
  45. if args.special_action is not None:
  46. return 0 if args.special_action() else 1
  47. try:
  48. compute_date = parse_date(args.date)
  49. except ValueError as error:
  50. print(colored(error.args[0], color="red", attrs=["bold"]))
  51. return -1
  52. try:
  53. position = get_position(args.position) if args.position not in [None, ""] else None
  54. if position is None and env_vars.position is not None:
  55. position = get_position(env_vars.position)
  56. except ValueError as e:
  57. print(colored(str(e), "red"))
  58. return 1
  59. # if output format is not specified, try to use output file extension as output format
  60. if args.output is not None and output_format is None:
  61. file_extension = os.path.splitext(args.output)[-1][1:].lower()
  62. if file_extension:
  63. output_format = file_extension
  64. # default to .txt if output format was not given and output file did not have file extension
  65. if output_format is None:
  66. output_format = "txt"
  67. if output_format == "pdf":
  68. print(
  69. _(
  70. "Save the planet and paper!\n"
  71. "Consider printing your PDF document only if really necessary, and use the other side of the sheet."
  72. )
  73. )
  74. if position is None:
  75. print()
  76. print(
  77. colored(
  78. _(
  79. "PDF output will not contain the ephemerides, because you didn't provide the observation "
  80. "coordinates."
  81. ),
  82. "yellow",
  83. )
  84. )
  85. timezone = args.timezone
  86. if timezone is None and env_vars.timezone is not None:
  87. timezone = int(env_vars.timezone)
  88. elif timezone is None:
  89. timezone = 0
  90. try:
  91. output = get_information(
  92. compute_date,
  93. position,
  94. timezone,
  95. output_format,
  96. args.colors,
  97. args.show_graph,
  98. )
  99. except InvalidOutputFormatError as error:
  100. print(colored(error.msg, "red"))
  101. debug.debug_print(error)
  102. return 3
  103. except UnavailableFeatureError as error:
  104. print(colored(error.msg, "red"))
  105. debug.debug_print(error)
  106. return 2
  107. except DateRangeError as error:
  108. print(colored(error.msg, "red"))
  109. debug.debug_print(error)
  110. return 1
  111. if args.output is not None:
  112. try:
  113. file_content = output.to_string()
  114. opening_mode = get_opening_mode(output_format)
  115. with open(args.output, opening_mode) as output_file:
  116. output_file.write(file_content)
  117. except UnavailableFeatureError as error:
  118. print(colored(error.msg, "red"))
  119. debug.debug_print(error)
  120. return 2
  121. except OSError as error:
  122. print(
  123. colored(
  124. _('The file could not be saved in "{path}": {error}').format(
  125. path=args.output, error=error.strerror
  126. ),
  127. "red",
  128. )
  129. )
  130. debug.debug_print(error)
  131. return 3
  132. elif not output.is_file_output_needed():
  133. print(output)
  134. else:
  135. print(
  136. colored(
  137. _("Please provide a file path to export in this format (--output)."),
  138. color="red",
  139. )
  140. )
  141. return 1
  142. return 0
  143. def get_information(
  144. compute_date: date,
  145. position: Position,
  146. timezone: int,
  147. output_format: str,
  148. colors: bool,
  149. show_graph: bool,
  150. ) -> dumper.Dumper:
  151. if position is not None:
  152. try:
  153. eph = get_ephemerides(
  154. for_date=compute_date, position=position, timezone=timezone
  155. )
  156. except OutOfRangeDateError as error:
  157. raise DateRangeError(error.min_date, error.max_date)
  158. else:
  159. eph = []
  160. try:
  161. moon_phase = get_moon_phase(compute_date)
  162. except OutOfRangeDateError as error:
  163. moon_phase = None
  164. print(
  165. colored(
  166. _(
  167. "Moon phase can only be displayed between {min_date} and {max_date}"
  168. ).format(
  169. min_date=error.min_date.strftime(SHORT_DATE_FORMAT),
  170. max_date=error.max_date.strftime(SHORT_DATE_FORMAT),
  171. ),
  172. "yellow",
  173. )
  174. )
  175. events_list = get_events(compute_date, timezone)
  176. try:
  177. return get_dumpers()[output_format](
  178. ephemerides=eph,
  179. moon_phase=moon_phase,
  180. events=events_list,
  181. date=compute_date,
  182. timezone=timezone,
  183. with_colors=colors,
  184. show_graph=show_graph,
  185. )
  186. except KeyError as error:
  187. raise InvalidOutputFormatError(output_format, list(get_dumpers().keys()))
  188. def get_dumpers() -> {str: dumper.Dumper}:
  189. return {
  190. "txt": dumper.TextDumper,
  191. "json": dumper.JsonDumper,
  192. "pdf": dumper.PdfDumper,
  193. }
  194. def get_opening_mode(format: str) -> str:
  195. if format == "pdf":
  196. return "wb"
  197. return "w"
  198. def output_version() -> bool:
  199. python_version = "%d.%d.%d" % (
  200. sys.version_info[0],
  201. sys.version_info[1],
  202. sys.version_info[2],
  203. )
  204. print("Kosmorro %s" % kosmorro_version)
  205. print(
  206. _(
  207. "Running on Python {python_version} "
  208. "with Kosmorrolib v{kosmorrolib_version}"
  209. ).format(python_version=python_version, kosmorrolib_version=kosmorrolib_version)
  210. )
  211. return True
  212. def get_args(output_formats: [str]):
  213. today = date.today()
  214. parser = argparse.ArgumentParser(
  215. description=_(
  216. "Compute the ephemerides and the events for a given date and a given position on Earth."
  217. ),
  218. epilog=_(
  219. "By default, only the events will be computed for today ({date}).\n"
  220. "To compute also the ephemerides, latitude and longitude arguments"
  221. " are needed."
  222. ).format(date=today.strftime(dumper.FULL_DATE_FORMAT)),
  223. )
  224. parser.add_argument(
  225. "--version",
  226. "-v",
  227. dest="special_action",
  228. action="store_const",
  229. const=output_version,
  230. default=None,
  231. help=_("Show the program version"),
  232. )
  233. parser.add_argument(
  234. "--format",
  235. "-f",
  236. type=str,
  237. default=None,
  238. choices=output_formats,
  239. help=_(
  240. "The format to output the information to. If not provided, the output format "
  241. "will be inferred from the file extension of the output file."
  242. ),
  243. )
  244. parser.add_argument(
  245. "--position",
  246. "-p",
  247. type=str,
  248. default=None,
  249. help=_(
  250. 'The observer\'s position on Earth, in the "{latitude},{longitude}" format.'
  251. "Can also be set in the KOSMORRO_POSITION environment variable."
  252. ),
  253. )
  254. parser.add_argument(
  255. "--date",
  256. "-d",
  257. type=str,
  258. default=today.strftime("%Y-%m-%d"),
  259. help=_(
  260. "The date for which the ephemerides must be calculated. Can be in the YYYY-MM-DD format "
  261. 'or an interval in the "[+-]YyMmDd" format (with Y, M, and D numbers). '
  262. "Defaults to today ({default_date})."
  263. ).format(default_date=today.strftime("%Y-%m-%d")),
  264. )
  265. parser.add_argument(
  266. "--timezone",
  267. "-t",
  268. type=int,
  269. default=None,
  270. help=_(
  271. "The timezone to display the hours in (e.g. 2 for UTC+2 or -3 for UTC-3). "
  272. "Can also be set in the KOSMORRO_TIMEZONE environment variable."
  273. ),
  274. )
  275. parser.add_argument(
  276. "--no-colors",
  277. dest="colors",
  278. action="store_false",
  279. help=_("Disable the colors in the console."),
  280. )
  281. parser.add_argument(
  282. "--output",
  283. "-o",
  284. type=str,
  285. default=None,
  286. help=_(
  287. "A file to export the output to. If not given, the standard output is used. "
  288. "This argument is needed for PDF format."
  289. ),
  290. )
  291. parser.add_argument(
  292. "--no-graph",
  293. dest="show_graph",
  294. action="store_false",
  295. help=_(
  296. "Do not generate a graph to represent the rise and set times in the PDF format."
  297. ),
  298. )
  299. parser.add_argument(
  300. "--debug",
  301. dest="show_debug_messages",
  302. action="store_true",
  303. help=_("Show debugging messages"),
  304. )
  305. return parser.parse_args()
  306. if __name__ == "__main__":
  307. lang, encoding = locale.getlocale()
  308. if platform.system() == "Windows":
  309. locale.setlocale(locale.LC_ALL, lang)
  310. else:
  311. locale.setlocale(locale.LC_ALL, f"{lang}.{encoding}")
  312. try:
  313. sys.exit(main())
  314. except KeyboardInterrupt:
  315. sys.exit(1)