views_api.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. # -*- coding: utf-8 -*-
  2. from flask import Blueprint, make_response, request, Response, current_app
  3. from flask.views import MethodView
  4. from collections import OrderedDict
  5. import sys
  6. import json
  7. import datetime
  8. import time
  9. from . import utils, db
  10. from .models import ISP, CoveredArea
  11. ispdbapi = Blueprint('ispdbapi', __name__)
  12. def output_json(data, code, headers=None):
  13. """Makes a Flask response with a JSON encoded body"""
  14. def encode(obj):
  15. if isinstance(obj, datetime.datetime):
  16. return obj.isoformat()
  17. if hasattr(obj, '__json__'):
  18. return obj.__json__()
  19. indent = 4 if not request.is_xhr else None
  20. dumped = json.dumps(data, indent=indent, default=encode)
  21. dumped += '\n'
  22. resp = make_response(dumped, code)
  23. resp.headers.extend(headers or {})
  24. return resp
  25. class REST(object):
  26. DEFAULT_MIMETYPE = 'application/json'
  27. OUTPUT_MIMETYPES = {
  28. 'application/json': output_json
  29. }
  30. @classmethod
  31. def accepted_mimetypes(cls, default_mime=DEFAULT_MIMETYPE):
  32. am = [m for m, q in request.accept_mimetypes]
  33. if default_mime:
  34. am += [default_mime]
  35. return am
  36. @classmethod
  37. def match_mimetype(cls):
  38. for accepted_mime in cls.accepted_mimetypes():
  39. if accepted_mime in cls.OUTPUT_MIMETYPES:
  40. return accepted_mime, cls.OUTPUT_MIMETYPES[accepted_mime]
  41. @classmethod
  42. def negociated_resp(cls, data, code, headers={}):
  43. output_mime, output_func = cls.match_mimetype()
  44. resp = output_func(data, code, headers)
  45. resp.headers['Content-Type'] = output_mime
  46. return resp
  47. @classmethod
  48. def marsh_error(cls, error):
  49. return cls.negociated_resp({
  50. 'error': dict(error)
  51. }, error.status_code)
  52. class RESTException(Exception):
  53. def __init__(self, status_code, msg, error_type=None):
  54. super(RESTException, self).__init__()
  55. self.status_code = status_code
  56. self.message = msg
  57. self.error_type = error_type
  58. def __iter__(self):
  59. return {
  60. 'error_type': self.error_type,
  61. 'message': self.message
  62. }.iteritems()
  63. def __json__(self):
  64. return {
  65. 'error': dict(self)
  66. }
  67. class RESTSimpleError(RESTException):
  68. def __init__(self):
  69. pass
  70. class ObjectNotFound(RESTSimpleError):
  71. status_code = 404
  72. message = 'Object not found'
  73. error_type = 'ispdb.api.ObjectNotFound'
  74. class InternalError(RESTSimpleError):
  75. status_code = 500
  76. message = 'There was an error while processing your request'
  77. error_type = 'ispdb.api.InternalError'
  78. class Resource(MethodView, REST):
  79. def __init__(self, *args, **kwargs):
  80. super(Resource, self).__init__(*args, **kwargs)
  81. def dispatch_request(self, *args, **kwargs):
  82. meth = getattr(self, request.method.lower(), None)
  83. if not meth:
  84. return self.negociated_resp(None, 405, None) # 405 Method not allowed
  85. resp = meth(*args, **kwargs)
  86. if isinstance(resp, Response):
  87. return resp
  88. data, code, headers = (None,) * 3
  89. if isinstance(resp, tuple):
  90. data, code, headers = resp + (None,) * (3 - len(resp))
  91. data = resp if data is None else data
  92. code = 200 if code is None else code
  93. headers = {} if headers is None else headers
  94. resp = self.negociated_resp(data, code, headers)
  95. return resp
  96. def get_range(self):
  97. range_ = request.args.get('range')
  98. if not range_:
  99. return None
  100. try:
  101. range_ = map(int, filter(None, range_.split(',', 1)))
  102. return range_
  103. except ValueError:
  104. return None
  105. def apply_range(self, query, range_):
  106. return query.slice(*range_) if len(range_) > 1 else query.offset(range_[0])
  107. def handle_list(self, query, cb, paginate=10, out_var=None):
  108. res = OrderedDict()
  109. res['total_items'] = query.count()
  110. range_ = self.get_range()
  111. if range_:
  112. query = self.apply_range(query, range_)
  113. items = [cb(o) for o in query]
  114. res['range'] = ','.join(map(str, range_))
  115. elif paginate:
  116. page = request.args.get('page', 1)
  117. per_page = request.args.get('per_page', paginate)
  118. try:
  119. page = int(page)
  120. except ValueError:
  121. page = 1
  122. try:
  123. per_page = int(per_page)
  124. except ValueError:
  125. per_page = paginate
  126. pgn = query.paginate(page, per_page=per_page, error_out=False)
  127. items = [cb(o) for o in pgn.items]
  128. res['page'] = pgn.page
  129. res['num_pages'] = pgn.pages
  130. res['per_page'] = pgn.per_page
  131. if out_var is None:
  132. out_var = query.column_descriptions[0]['name'].lower() + 's'
  133. res[out_var] = items
  134. return res
  135. class ISPResource(Resource):
  136. """
  137. /isp/
  138. GET - list all ISPs
  139. /isp/<int:isp_id>/
  140. GET - return ISP with the given id
  141. """
  142. def isp_to_dict(self, isp):
  143. r = OrderedDict()
  144. r['id'] = isp.id
  145. r['is_ffdn_member'] = isp.is_ffdn_member
  146. r['json_url'] = isp.json_url
  147. r['date_added'] = utils.tosystemtz(isp.date_added)
  148. if isp.last_update_success:
  149. r['last_update'] = utils.tosystemtz(isp.last_update_success)
  150. else:
  151. r['last_update'] = None
  152. r['ispformat'] = isp.json
  153. return r
  154. def get(self, isp_id=None):
  155. if isp_id is not None:
  156. s = ISP.query.filter_by(id=isp_id, is_disabled=False).scalar()
  157. if not s:
  158. raise ObjectNotFound
  159. if s.json_url:
  160. # compute next update time, based on crawler cron interval.
  161. interval = current_app.config['CRAWLER_CRON_INTERVAL']
  162. nupdt = time.mktime(s.next_update.timetuple())
  163. nupdt = datetime.datetime.fromtimestamp(nupdt - (nupdt % interval) + interval + 60)
  164. now = datetime.datetime.utcnow()
  165. if nupdt > now:
  166. max_age = (nupdt - now).seconds
  167. else:
  168. max_age = current_app.config['CRAWLER_MIN_CACHE_TIME']
  169. else:
  170. # default max-age for isp using the form
  171. max_age = current_app.config['CRAWLER_MIN_CACHE_TIME']
  172. headers = {'Cache-Control': 'max-age=' + str(max_age)}
  173. return self.isp_to_dict(s), 200, headers
  174. else:
  175. s = ISP.query.filter_by(is_disabled=False)
  176. return self.handle_list(s, self.isp_to_dict)
  177. class CoveredAreaResource(Resource):
  178. """
  179. /covered_area/
  180. GET - list all covered areas
  181. /covered_area/<int:area_id>/
  182. GET - return covered area with the given id
  183. /isp/<int:isp_id>/covered_area/
  184. GET - return covered areas for the given ISP
  185. """
  186. def ca_to_dict(self, ca):
  187. r = OrderedDict()
  188. r['id'] = ca.id
  189. if not self.isp_id:
  190. r['isp'] = OrderedDict()
  191. r['isp']['id'] = ca.isp_id
  192. r['isp']['name'] = ca.isp.name
  193. r['isp']['shortname'] = ca.isp.shortname
  194. r['name'] = ca.name
  195. r['geojson'] = json.loads(ca.area_geojson) if ca.area_geojson else None
  196. return r
  197. def get(self, area_id=None, isp_id=None):
  198. self.area_id = area_id
  199. self.isp_id = isp_id
  200. if area_id is not None:
  201. raise ObjectNotFound
  202. s = CoveredArea.query.get_or_404(area_id)
  203. return self.ca_to_dict(s)
  204. else:
  205. s = CoveredArea.query.filter(ISP.is_disabled == False)\
  206. .options(db.joinedload('isp'),
  207. db.defer('isp.json'),
  208. db.defer('area'),
  209. db.undefer('area_geojson'))
  210. if isp_id:
  211. if not ISP.query.filter_by(id=isp_id, is_disabled=False).scalar():
  212. raise ObjectNotFound
  213. s = s.filter(CoveredArea.isp_id == isp_id)
  214. return self.handle_list(s, self.ca_to_dict, out_var='covered_areas')
  215. @ispdbapi.route('/<path:notfound>')
  216. def path_not_found(notfound):
  217. "catch all"
  218. return REST.marsh_error(RESTException(404, 'path not found', 'ispdb.api.PathNotFound'))
  219. @ispdbapi.errorhandler(404)
  220. def resource_not_found(e):
  221. return REST.marsh_error(RESTException(404, 'not found'))
  222. @ispdbapi.errorhandler(RESTException)
  223. def handle_rest_error(e):
  224. return REST.marsh_error(e)
  225. @ispdbapi.errorhandler(Exception)
  226. def handle_generic_exception(e):
  227. "Return a REST-formated error response instead of the standard 500 html template"
  228. current_app.log_exception(sys.exc_info())
  229. return REST.marsh_error(InternalError())
  230. isp_view = ISPResource.as_view('isp_api')
  231. ispdbapi.add_url_rule('/v1/isp/', defaults={'isp_id': None},
  232. view_func=isp_view, methods=['GET'])
  233. ispdbapi.add_url_rule('/v1/isp/<int:isp_id>/', view_func=isp_view,
  234. methods=['GET'])
  235. @ispdbapi.route('/v1/isp/export_urls/')
  236. @ispdbapi.route('/v1/isp/all_your_urls_are_belong_to_us/')
  237. def all_urls():
  238. """
  239. This resource allows to simply export all ISP-format URLs in our DB
  240. without pulling all ISP data.
  241. """
  242. isps = db.session.query(ISP.id, ISP.json_url).filter(ISP.json_url != None)
  243. return REST.negociated_resp({
  244. 'isps': [{'id': isp.id, 'json_url': isp.json_url} for isp in isps]
  245. }, 200)
  246. ca_view = CoveredAreaResource.as_view('covered_area_api')
  247. ispdbapi.add_url_rule('/v1/covered_area/', defaults={'area_id': None},
  248. view_func=ca_view, methods=['GET'])
  249. ispdbapi.add_url_rule('/v1/covered_area/<int:area_id>/', view_func=ca_view,
  250. methods=['GET'])
  251. ispdbapi.add_url_rule('/v1/isp/<int:isp_id>/covered_areas/', view_func=ca_view,
  252. methods=['GET'])