views_api.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  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. resp = meth(*args, **kwargs)
  84. if isinstance(resp, Response):
  85. return resp
  86. data, code, headers = (None,) * 3
  87. if isinstance(resp, tuple):
  88. data, code, headers = resp + (None,) * (3 - len(resp))
  89. data = resp if data is None else data
  90. code = 200 if code is None else code
  91. headers = {} if headers is None else headers
  92. resp = self.negociated_resp(data, code, headers)
  93. return resp
  94. def get_range(self):
  95. range_ = request.args.get('range')
  96. if not range_:
  97. return None
  98. try:
  99. range_ = map(int, filter(None, range_.split(',', 1)))
  100. return range_
  101. except ValueError:
  102. return None
  103. def apply_range(self, query, range_):
  104. return query.slice(*range_) if len(range_) > 1 else query.offset(range_[0])
  105. def handle_list(self, query, cb, paginate=10, out_var=None):
  106. res = OrderedDict()
  107. res['total_items'] = query.count()
  108. range_ = self.get_range()
  109. if range_:
  110. query = self.apply_range(query, range_)
  111. items = [cb(o) for o in query]
  112. res['range'] = ','.join(map(str, range_))
  113. elif paginate:
  114. page = request.args.get('page', 1)
  115. per_page = request.args.get('per_page', paginate)
  116. try:
  117. page = int(page)
  118. except ValueError:
  119. page = 1
  120. try:
  121. per_page = int(per_page)
  122. except ValueError:
  123. per_page = paginate
  124. pgn = query.paginate(page, per_page=per_page, error_out=False)
  125. items = [cb(o) for o in pgn.items]
  126. res['page'] = pgn.page
  127. res['num_pages'] = pgn.pages
  128. res['per_page'] = pgn.per_page
  129. if out_var is None:
  130. out_var = query.column_descriptions[0]['name'].lower() + 's'
  131. res[out_var] = items
  132. return res
  133. class ISPResource(Resource):
  134. """
  135. /isp/
  136. GET - list all ISPs
  137. /isp/<int:isp_id>/
  138. GET - return ISP with the given id
  139. """
  140. def isp_to_dict(self, isp):
  141. r = OrderedDict()
  142. r['id'] = isp.id
  143. r['is_ffdn_member'] = isp.is_ffdn_member
  144. r['json_url'] = isp.json_url
  145. r['date_added'] = utils.tosystemtz(isp.date_added)
  146. if isp.last_update_success:
  147. r['last_update'] = utils.tosystemtz(isp.last_update_success)
  148. else:
  149. r['last_update'] = None
  150. r['ispformat'] = isp.json
  151. return r
  152. def get(self, isp_id=None):
  153. if isp_id is not None:
  154. s = ISP.query.filter_by(id=isp_id, is_disabled=False).scalar()
  155. if not s:
  156. raise ObjectNotFound
  157. if s.json_url:
  158. # compute next update time, based on crawler cron interval.
  159. interval = current_app.config['CRAWLER_CRON_INTERVAL']
  160. nupdt = time.mktime(s.next_update.timetuple())
  161. nupdt = datetime.datetime.fromtimestamp(nupdt - (nupdt % interval) + interval + 60)
  162. now = datetime.datetime.utcnow()
  163. if nupdt > now:
  164. max_age = (nupdt - now).seconds
  165. else:
  166. max_age = current_app.config['CRAWLER_MIN_CACHE_TIME']
  167. else:
  168. # default max-age for isp using the form
  169. max_age = current_app.config['CRAWLER_MIN_CACHE_TIME']
  170. headers = {'Cache-Control': 'max-age=' + str(max_age)}
  171. return self.isp_to_dict(s), 200, headers
  172. else:
  173. s = ISP.query.filter_by(is_disabled=False)
  174. return self.handle_list(s, self.isp_to_dict)
  175. class CoveredAreaResource(Resource):
  176. """
  177. /covered_area/
  178. GET - list all covered areas
  179. /covered_area/<int:area_id>/
  180. GET - return covered area with the given id
  181. /isp/<int:isp_id>/covered_area/
  182. GET - return covered areas for the given ISP
  183. """
  184. def ca_to_dict(self, ca):
  185. r = OrderedDict()
  186. r['id'] = ca.id
  187. if not self.isp_id:
  188. r['isp'] = OrderedDict()
  189. r['isp']['id'] = ca.isp_id
  190. r['isp']['name'] = ca.isp.name
  191. r['isp']['shortname'] = ca.isp.shortname
  192. r['name'] = ca.name
  193. r['geojson'] = json.loads(ca.area_geojson) if ca.area_geojson else None
  194. return r
  195. def get(self, area_id=None, isp_id=None):
  196. self.area_id = area_id
  197. self.isp_id = isp_id
  198. if area_id is not None:
  199. raise ObjectNotFound
  200. s = CoveredArea.query.get_or_404(area_id)
  201. return self.ca_to_dict(s)
  202. else:
  203. s = CoveredArea.query.filter(ISP.is_disabled == False)\
  204. .options(db.joinedload('isp'),
  205. db.defer('isp.json'),
  206. db.defer('area'),
  207. db.undefer('area_geojson'))
  208. if isp_id:
  209. if not ISP.query.filter_by(id=isp_id, is_disabled=False).scalar():
  210. raise ObjectNotFound
  211. s = s.filter(CoveredArea.isp_id == isp_id)
  212. return self.handle_list(s, self.ca_to_dict, out_var='covered_areas')
  213. @ispdbapi.route('/<path:notfound>')
  214. def path_not_found(notfound):
  215. "catch all"
  216. return REST.marsh_error(RESTException(404, 'path not found', 'ispdb.api.PathNotFound'))
  217. @ispdbapi.errorhandler(404)
  218. def resource_not_found(e):
  219. return REST.marsh_error(RESTException(404, 'not found'))
  220. @ispdbapi.errorhandler(RESTException)
  221. def handle_rest_error(e):
  222. return REST.marsh_error(e)
  223. @ispdbapi.errorhandler(Exception)
  224. def handle_generic_exception(e):
  225. "Return a REST-formated error response instead of the standard 500 html template"
  226. current_app.log_exception(sys.exc_info())
  227. return REST.marsh_error(InternalError())
  228. isp_view = ISPResource.as_view('isp_api')
  229. ispdbapi.add_url_rule('/v1/isp/', defaults={'isp_id': None},
  230. view_func=isp_view, methods=['GET'])
  231. ispdbapi.add_url_rule('/v1/isp/<int:isp_id>/', view_func=isp_view,
  232. methods=['GET'])
  233. @ispdbapi.route('/v1/isp/export_urls/')
  234. @ispdbapi.route('/v1/isp/all_your_urls_are_belong_to_us/')
  235. def all_urls():
  236. """
  237. This resource allows to simply export all ISP-format URLs in our DB
  238. without pulling all ISP data.
  239. """
  240. isps = db.session.query(ISP.id, ISP.json_url).filter(ISP.json_url != None)
  241. return REST.negociated_resp({
  242. 'isps': [{'id': isp.id, 'json_url': isp.json_url} for isp in isps]
  243. }, 200)
  244. ca_view = CoveredAreaResource.as_view('covered_area_api')
  245. ispdbapi.add_url_rule('/v1/covered_area/', defaults={'area_id': None},
  246. view_func=ca_view, methods=['GET'])
  247. ispdbapi.add_url_rule('/v1/covered_area/<int:area_id>/', view_func=ca_view,
  248. methods=['GET'])
  249. ispdbapi.add_url_rule('/v1/isp/<int:isp_id>/covered_areas/', view_func=ca_view,
  250. methods=['GET'])