diffido.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import os
  4. import json
  5. import logging
  6. from tornado.ioloop import IOLoop
  7. # from lxml.html.diff import htmldiff
  8. from apscheduler.schedulers.tornado import TornadoScheduler
  9. from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
  10. import tornado.httpserver
  11. import tornado.ioloop
  12. import tornado.options
  13. from tornado.options import define, options
  14. import tornado.web
  15. from tornado import gen, escape
  16. CONF_DIR = ''
  17. JOBS_STORE = 'sqlite:///conf/jobs.db'
  18. API_VERSION = '1.0'
  19. SCHEDULES_FILE = 'conf/schedules.json'
  20. logger = logging.getLogger()
  21. logger.setLevel(logging.INFO)
  22. def read_schedules():
  23. if not os.path.isfile(SCHEDULES_FILE):
  24. return {'schedules': {}}
  25. try:
  26. with open(SCHEDULES_FILE, 'r') as fd:
  27. return json.loads(fd.read())
  28. except Exception as e:
  29. logger.error('unable to read %s: %s' % (SCHEDULES_FILE, e))
  30. return {'schedules': {}}
  31. def write_schedules(schedules):
  32. with open(SCHEDULES_FILE, 'w') as fd:
  33. fd.write(json.dumps(schedules, indent=2))
  34. def next_id(schedules):
  35. ids = schedules.get('schedules', {}).keys()
  36. if not ids:
  37. return '1'
  38. return str(max([int(i) for i in ids]) + 1)
  39. def get_schedule(id_, addID=True):
  40. schedules = read_schedules()
  41. data = schedules.get('schedules', {}).get(id_, {})
  42. if addID:
  43. data['id'] = str(id_)
  44. return data
  45. def run_job(id_=None, *args, **kwargs):
  46. schedule = get_schedule(id_, addID=False)
  47. url = schedule.get('url')
  48. if not url:
  49. return
  50. print('Running job id:%s title:%s url: %s' % (id_, schedule.get('title', ''), url))
  51. def scheduler_update(scheduler, id_):
  52. schedule = get_schedule(id_, addID=False)
  53. if not schedule:
  54. return
  55. trigger = schedule.get('trigger')
  56. if trigger not in ('interval', 'cron'):
  57. return
  58. args = {'trigger': trigger}
  59. if trigger == 'interval':
  60. for unit in 'weeks', 'days', 'hours', 'minutes', 'seconds':
  61. if 'interval_%s' % unit not in schedule:
  62. continue
  63. args[unit] = int(schedule['interval_%s' % unit])
  64. scheduler.add_job(run_job, id=id_, replace_existing=True, kwargs={'id_': id_}, **args)
  65. def scheduler_delete(scheduler, id_):
  66. scheduler.remove_job(job_id=id_)
  67. def reset_from_schedules(scheduler):
  68. scheduler.remove_all_jobs()
  69. for key in read_schedules().get('schedules', {}).keys():
  70. scheduler_update(scheduler, id_=key)
  71. class DiffidoBaseException(Exception):
  72. """Base class for diffido custom exceptions.
  73. :param message: text message
  74. :type message: str
  75. :param status: numeric http status code
  76. :type status: int"""
  77. def __init__(self, message, status=400):
  78. super(DiffidoBaseException, self).__init__(message)
  79. self.message = message
  80. self.status = status
  81. class BaseHandler(tornado.web.RequestHandler):
  82. """Base class for request handlers."""
  83. # Cache currently connected users.
  84. _users_cache = {}
  85. # set of documents we're managing (a collection in MongoDB or a table in a SQL database)
  86. document = None
  87. collection = None
  88. # A property to access the first value of each argument.
  89. arguments = property(lambda self: dict([(k, v[0].decode('utf-8'))
  90. for k, v in self.request.arguments.items()]))
  91. @property
  92. def clean_body(self):
  93. """Return a clean dictionary from a JSON body, suitable for a query on MongoDB.
  94. :returns: a clean copy of the body arguments
  95. :rtype: dict"""
  96. return escape.json_decode(self.request.body or '{}')
  97. def write_error(self, status_code, **kwargs):
  98. """Default error handler."""
  99. if isinstance(kwargs.get('exc_info', (None, None))[1], DiffidoBaseException):
  100. exc = kwargs['exc_info'][1]
  101. status_code = exc.status
  102. message = exc.message
  103. else:
  104. message = 'internal error'
  105. self.build_error(message, status=status_code)
  106. def is_api(self):
  107. """Return True if the path is from an API call."""
  108. return self.request.path.startswith('/v%s' % API_VERSION)
  109. def initialize(self, **kwargs):
  110. """Add every passed (key, value) as attributes of the instance."""
  111. for key, value in kwargs.items():
  112. setattr(self, key, value)
  113. def build_error(self, message='', status=400):
  114. """Build and write an error message.
  115. :param message: textual message
  116. :type message: str
  117. :param status: HTTP status code
  118. :type status: int
  119. """
  120. self.set_status(status)
  121. self.write({'error': True, 'message': message})
  122. def build_success(self, message='', status=200):
  123. """Build and write a success message.
  124. :param message: textual message
  125. :type message: str
  126. :param status: HTTP status code
  127. :type status: int
  128. """
  129. self.set_status(status)
  130. self.write({'error': False, 'message': message})
  131. class SchedulesHandler(BaseHandler):
  132. @gen.coroutine
  133. def get(self, id_=None, *args, **kwargs):
  134. if id_ is not None:
  135. self.write({'schedule': get_schedule(id_)})
  136. return
  137. schedules = read_schedules()
  138. self.write(schedules)
  139. @gen.coroutine
  140. def put(self, id_=None, *args, **kwargs):
  141. if id_ is None:
  142. return self.build_error(message='update action requires an ID')
  143. data = self.clean_body
  144. schedules = read_schedules()
  145. if id_ not in schedules.get('schedules', {}):
  146. return self.build_error(message='schedule %s not found' % id_)
  147. schedules['schedules'][id_] = data
  148. write_schedules(schedules)
  149. scheduler_update(scheduler=self.scheduler, id_=id_)
  150. self.write(get_schedule(id_=id_))
  151. @gen.coroutine
  152. def post(self, *args, **kwargs):
  153. data = self.clean_body
  154. schedules = read_schedules()
  155. id_ = next_id(schedules)
  156. schedules['schedules'][id_] = data
  157. write_schedules(schedules)
  158. scheduler_update(scheduler=self.scheduler, id_=id_)
  159. self.write(get_schedule(id_=id_))
  160. @gen.coroutine
  161. def delete(self, id_=None, *args, **kwargs):
  162. if id_ is None:
  163. return self.build_error(message='an ID must be specified')
  164. schedules = read_schedules()
  165. if id_ in schedules.get('schedules', {}):
  166. del schedules['schedules'][id_]
  167. write_schedules(schedules)
  168. scheduler_delete(scheduler=self.scheduler, id_=id_)
  169. self.build_success(message='removed schedule %s' % id_)
  170. class ResetSchedulesHandler(BaseHandler):
  171. @gen.coroutine
  172. def post(self, *args, **kwargs):
  173. reset_from_schedules(self.scheduler)
  174. class TemplateHandler(BaseHandler):
  175. """Handler for the / path."""
  176. app_path = os.path.join(os.path.dirname(__file__), "dist")
  177. @gen.coroutine
  178. def get(self, *args, **kwargs):
  179. page = 'index.html'
  180. if args and args[0]:
  181. page = args[0].strip('/')
  182. arguments = self.arguments
  183. self.render(page, **arguments)
  184. def serve():
  185. jobstores = {'default': SQLAlchemyJobStore(url=JOBS_STORE)}
  186. scheduler = TornadoScheduler(jobstores=jobstores)
  187. scheduler.start()
  188. define('port', default=3210, help='run on the given port', type=int)
  189. define('address', default='', help='bind the server at the given address', type=str)
  190. define('ssl_cert', default=os.path.join(os.path.dirname(__file__), 'ssl', 'diffido_cert.pem'),
  191. help='specify the SSL certificate to use for secure connections')
  192. define('ssl_key', default=os.path.join(os.path.dirname(__file__), 'ssl', 'diffido_key.pem'),
  193. help='specify the SSL private key to use for secure connections')
  194. define('debug', default=False, help='run in debug mode')
  195. define('config', help='read configuration file',
  196. callback=lambda path: tornado.options.parse_config_file(path, final=False))
  197. tornado.options.parse_command_line()
  198. if options.debug:
  199. logger.setLevel(logging.DEBUG)
  200. ssl_options = {}
  201. if os.path.isfile(options.ssl_key) and os.path.isfile(options.ssl_cert):
  202. ssl_options = dict(certfile=options.ssl_cert, keyfile=options.ssl_key)
  203. init_params = dict(listen_port=options.port, logger=logger, ssl_options=ssl_options,
  204. scheduler=scheduler)
  205. _reset_schedules_path = r'schedules/reset'
  206. _schedules_path = r'schedules/?(?P<id_>\d+)?'
  207. application = tornado.web.Application([
  208. ('/api/%s' % _reset_schedules_path, ResetSchedulesHandler, init_params),
  209. (r'/api/v%s/%s' % (API_VERSION, _reset_schedules_path), ResetSchedulesHandler, init_params),
  210. ('/api/%s' % _schedules_path, SchedulesHandler, init_params),
  211. (r'/api/v%s/%s' % (API_VERSION, _schedules_path), SchedulesHandler, init_params),
  212. (r'/?(.*)', TemplateHandler, init_params),
  213. ],
  214. static_path=os.path.join(os.path.dirname(__file__), 'dist/static'),
  215. template_path=os.path.join(os.path.dirname(__file__), 'dist/'),
  216. debug=options.debug)
  217. http_server = tornado.httpserver.HTTPServer(application, ssl_options=ssl_options or None)
  218. logger.info('Start serving on %s://%s:%d', 'https' if ssl_options else 'http',
  219. options.address if options.address else '127.0.0.1',
  220. options.port)
  221. http_server.listen(options.port, options.address)
  222. try:
  223. IOLoop.instance().start()
  224. except (KeyboardInterrupt, SystemExit):
  225. pass
  226. if __name__ == '__main__':
  227. serve()