views_api.py 8.9 KB

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