123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- # -*- coding: utf-8 -*-
- from flask import Blueprint, make_response, request, Response, current_app
- from flask.views import MethodView
- from collections import OrderedDict
- import sys
- import json
- import datetime
- import time
- from . import utils, db
- from .models import ISP, CoveredArea
- ispdbapi = Blueprint('ispdbapi', __name__)
- def output_json(data, code, headers=None):
- """Makes a Flask response with a JSON encoded body"""
- def encode(obj):
- if isinstance(obj, datetime.datetime):
- return obj.isoformat()
- if hasattr(obj, '__json__'):
- return obj.__json__()
- indent = 4 if not request.is_xhr else None
- dumped = json.dumps(data, indent=indent, default=encode)
- dumped += '\n'
- resp = make_response(dumped, code)
- resp.headers.extend(headers or {})
- return resp
- class REST(object):
- DEFAULT_MIMETYPE = 'application/json'
- OUTPUT_MIMETYPES = {
- 'application/json': output_json
- }
- @classmethod
- def accepted_mimetypes(cls, default_mime=DEFAULT_MIMETYPE):
- am = [m for m, q in request.accept_mimetypes]
- if default_mime:
- am += [default_mime]
- return am
- @classmethod
- def match_mimetype(cls):
- for accepted_mime in cls.accepted_mimetypes():
- if accepted_mime in cls.OUTPUT_MIMETYPES:
- return accepted_mime, cls.OUTPUT_MIMETYPES[accepted_mime]
- @classmethod
- def negociated_resp(cls, data, code, headers={}):
- output_mime, output_func = cls.match_mimetype()
- resp = output_func(data, code, headers)
- resp.headers['Content-Type'] = output_mime
- return resp
- @classmethod
- def marsh_error(cls, error):
- return cls.negociated_resp({
- 'error': dict(error)
- }, error.status_code)
- class RESTException(Exception):
- def __init__(self, status_code, msg, error_type=None):
- super(RESTException, self).__init__()
- self.status_code = status_code
- self.message = msg
- self.error_type = error_type
- def __iter__(self):
- return {
- 'error_type': self.error_type,
- 'message': self.message
- }.iteritems()
- def __json__(self):
- return {
- 'error': dict(self)
- }
- class RESTSimpleError(RESTException):
- def __init__(self):
- pass
- class ObjectNotFound(RESTSimpleError):
- status_code = 404
- message = 'Object not found'
- error_type = 'ispdb.api.ObjectNotFound'
- class InternalError(RESTSimpleError):
- status_code = 500
- message = 'There was an error while processing your request'
- error_type = 'ispdb.api.InternalError'
- class Resource(MethodView, REST):
- def __init__(self, *args, **kwargs):
- super(Resource, self).__init__(*args, **kwargs)
- def dispatch_request(self, *args, **kwargs):
- meth = getattr(self, request.method.lower(), None)
- if not meth:
- return self.negociated_resp(None, 405, None) # 405 Method not allowed
- resp = meth(*args, **kwargs)
- if isinstance(resp, Response):
- return resp
- data, code, headers = (None,) * 3
- if isinstance(resp, tuple):
- data, code, headers = resp + (None,) * (3 - len(resp))
- data = resp if data is None else data
- code = 200 if code is None else code
- headers = {} if headers is None else headers
- resp = self.negociated_resp(data, code, headers)
- return resp
- def get_range(self):
- range_ = request.args.get('range')
- if not range_:
- return None
- try:
- range_ = map(int, filter(None, range_.split(',', 1)))
- return range_
- except ValueError:
- return None
- def apply_range(self, query, range_):
- return query.slice(*range_) if len(range_) > 1 else query.offset(range_[0])
- def handle_list(self, query, cb, paginate=10, out_var=None):
- res = OrderedDict()
- res['total_items'] = query.count()
- range_ = self.get_range()
- if range_:
- query = self.apply_range(query, range_)
- items = [cb(o) for o in query]
- res['range'] = ','.join(map(str, range_))
- elif paginate:
- page = request.args.get('page', 1)
- per_page = request.args.get('per_page', paginate)
- try:
- page = int(page)
- except ValueError:
- page = 1
- try:
- per_page = int(per_page)
- except ValueError:
- per_page = paginate
- pgn = query.paginate(page, per_page=per_page, error_out=False)
- items = [cb(o) for o in pgn.items]
- res['page'] = pgn.page
- res['num_pages'] = pgn.pages
- res['per_page'] = pgn.per_page
- if out_var is None:
- out_var = query.column_descriptions[0]['name'].lower() + 's'
- res[out_var] = items
- return res
- class ISPResource(Resource):
- """
- /isp/
- GET - list all ISPs
- /isp/<int:isp_id>/
- GET - return ISP with the given id
- """
- def isp_to_dict(self, isp):
- r = OrderedDict()
- r['id'] = isp.id
- r['is_ffdn_member'] = isp.is_ffdn_member
- r['json_url'] = isp.json_url
- r['date_added'] = utils.tosystemtz(isp.date_added)
- if isp.last_update_success:
- r['last_update'] = utils.tosystemtz(isp.last_update_success)
- else:
- r['last_update'] = None
- r['ispformat'] = isp.json
- return r
- def get(self, isp_id=None):
- if isp_id is not None:
- s = ISP.query.filter_by(id=isp_id, is_disabled=False).scalar()
- if not s:
- raise ObjectNotFound
- if s.json_url:
- # compute next update time, based on crawler cron interval.
- interval = current_app.config['CRAWLER_CRON_INTERVAL']
- nupdt = time.mktime(s.next_update.timetuple())
- nupdt = datetime.datetime.fromtimestamp(nupdt - (nupdt % interval) + interval + 60)
- now = datetime.datetime.utcnow()
- if nupdt > now:
- max_age = (nupdt - now).seconds
- else:
- max_age = current_app.config['CRAWLER_MIN_CACHE_TIME']
- else:
- # default max-age for isp using the form
- max_age = current_app.config['CRAWLER_MIN_CACHE_TIME']
- headers = {'Cache-Control': 'max-age=' + str(max_age)}
- return self.isp_to_dict(s), 200, headers
- else:
- s = ISP.query.filter_by(is_disabled=False)
- return self.handle_list(s, self.isp_to_dict)
- class CoveredAreaResource(Resource):
- """
- /covered_area/
- GET - list all covered areas
- /covered_area/<int:area_id>/
- GET - return covered area with the given id
- /isp/<int:isp_id>/covered_area/
- GET - return covered areas for the given ISP
- """
- def ca_to_dict(self, ca):
- r = OrderedDict()
- r['id'] = ca.id
- if not self.isp_id:
- r['isp'] = OrderedDict()
- r['isp']['id'] = ca.isp_id
- r['isp']['name'] = ca.isp.name
- r['isp']['shortname'] = ca.isp.shortname
- r['name'] = ca.name
- r['geojson'] = json.loads(ca.area_geojson) if ca.area_geojson else None
- return r
- def get(self, area_id=None, isp_id=None):
- self.area_id = area_id
- self.isp_id = isp_id
- if area_id is not None:
- raise ObjectNotFound
- s = CoveredArea.query.get_or_404(area_id)
- return self.ca_to_dict(s)
- else:
- s = CoveredArea.query.filter(ISP.is_disabled == False)\
- .options(db.joinedload('isp'),
- db.defer('isp.json'),
- db.defer('area'),
- db.undefer('area_geojson'))
- if isp_id:
- if not ISP.query.filter_by(id=isp_id, is_disabled=False).scalar():
- raise ObjectNotFound
- s = s.filter(CoveredArea.isp_id == isp_id)
- return self.handle_list(s, self.ca_to_dict, out_var='covered_areas')
- @ispdbapi.route('/<path:notfound>')
- def path_not_found(notfound):
- "catch all"
- return REST.marsh_error(RESTException(404, 'path not found', 'ispdb.api.PathNotFound'))
- @ispdbapi.errorhandler(404)
- def resource_not_found(e):
- return REST.marsh_error(RESTException(404, 'not found'))
- @ispdbapi.errorhandler(RESTException)
- def handle_rest_error(e):
- return REST.marsh_error(e)
- @ispdbapi.errorhandler(Exception)
- def handle_generic_exception(e):
- "Return a REST-formated error response instead of the standard 500 html template"
- current_app.log_exception(sys.exc_info())
- return REST.marsh_error(InternalError())
- isp_view = ISPResource.as_view('isp_api')
- ispdbapi.add_url_rule('/v1/isp/', defaults={'isp_id': None},
- view_func=isp_view, methods=['GET'])
- ispdbapi.add_url_rule('/v1/isp/<int:isp_id>/', view_func=isp_view,
- methods=['GET'])
- @ispdbapi.route('/v1/isp/export_urls/')
- @ispdbapi.route('/v1/isp/all_your_urls_are_belong_to_us/')
- def all_urls():
- """
- This resource allows to simply export all ISP-format URLs in our DB
- without pulling all ISP data.
- """
- isps = db.session.query(ISP.id, ISP.json_url).filter(ISP.json_url != None)
- return REST.negociated_resp({
- 'isps': [{'id': isp.id, 'json_url': isp.json_url} for isp in isps]
- }, 200)
- ca_view = CoveredAreaResource.as_view('covered_area_api')
- ispdbapi.add_url_rule('/v1/covered_area/', defaults={'area_id': None},
- view_func=ca_view, methods=['GET'])
- ispdbapi.add_url_rule('/v1/covered_area/<int:area_id>/', view_func=ca_view,
- methods=['GET'])
- ispdbapi.add_url_rule('/v1/isp/<int:isp_id>/covered_areas/', view_func=ca_view,
- methods=['GET'])
|