| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- """
- This module defines a FlaskApp, a Connexion application to wrap a Flask application.
- """
- import datetime
- import logging
- import pathlib
- from decimal import Decimal
- from types import FunctionType # NOQA
- import flask
- import werkzeug.exceptions
- from flask import json, signals
- from ..apis.flask_api import FlaskApi
- from ..exceptions import ProblemException
- from ..problem import problem
- from .abstract import AbstractApp
- logger = logging.getLogger('connexion.app')
- class FlaskApp(AbstractApp):
- def __init__(self, import_name, server='flask', extra_files=None, **kwargs):
- """
- :param extra_files: additional files to be watched by the reloader, defaults to the swagger specs of added apis
- :type extra_files: list[str | pathlib.Path], optional
- See :class:`~connexion.AbstractApp` for additional parameters.
- """
- super().__init__(import_name, FlaskApi, server=server, **kwargs)
- self.extra_files = extra_files or []
- def create_app(self):
- app = flask.Flask(self.import_name, **self.server_args)
- app.json_encoder = FlaskJSONEncoder
- app.url_map.converters['float'] = NumberConverter
- app.url_map.converters['int'] = IntegerConverter
- return app
- def get_root_path(self):
- return pathlib.Path(self.app.root_path)
- def set_errors_handlers(self):
- for error_code in werkzeug.exceptions.default_exceptions:
- self.add_error_handler(error_code, self.common_error_handler)
- self.add_error_handler(ProblemException, self.common_error_handler)
- def common_error_handler(self, exception):
- """
- :type exception: Exception
- """
- if isinstance(exception, ProblemException):
- response = problem(
- status=exception.status, title=exception.title, detail=exception.detail,
- type=exception.type, instance=exception.instance, headers=exception.headers,
- ext=exception.ext)
- else:
- if not isinstance(exception, werkzeug.exceptions.HTTPException):
- exception = werkzeug.exceptions.InternalServerError()
- response = problem(title=exception.name,
- detail=exception.description,
- status=exception.code,
- headers=exception.get_headers())
- if response.status_code >= 500:
- signals.got_request_exception.send(self.app, exception=exception)
- return FlaskApi.get_response(response)
- def add_api(self, specification, **kwargs):
- api = super().add_api(specification, **kwargs)
- self.app.register_blueprint(api.blueprint)
- if isinstance(specification, (str, pathlib.Path)):
- self.extra_files.append(self.specification_dir / specification)
- return api
- def add_error_handler(self, error_code, function):
- # type: (int, FunctionType) -> None
- self.app.register_error_handler(error_code, function)
- def run(self,
- port=None,
- server=None,
- debug=None,
- host=None,
- extra_files=None,
- **options): # pragma: no cover
- """
- Runs the application on a local development server.
- :param host: the host interface to bind on.
- :type host: str
- :param port: port to listen to
- :type port: int
- :param server: which wsgi server to use
- :type server: str | None
- :param debug: include debugging information
- :type debug: bool
- :param extra_files: additional files to be watched by the reloader.
- :type extra_files: Iterable[str | pathlib.Path]
- :param options: options to be forwarded to the underlying server
- """
- # this functions is not covered in unit tests because we would effectively testing the mocks
- # overwrite constructor parameter
- if port is not None:
- self.port = port
- elif self.port is None:
- self.port = 5000
- self.host = host or self.host or '0.0.0.0'
- if server is not None:
- self.server = server
- if debug is not None:
- self.debug = debug
- if extra_files is not None:
- self.extra_files.extend(extra_files)
- logger.debug('Starting %s HTTP server..', self.server, extra=vars(self))
- if self.server == 'flask':
- self.app.run(self.host, port=self.port, debug=self.debug,
- extra_files=self.extra_files, **options)
- elif self.server == 'tornado':
- try:
- import tornado.httpserver
- import tornado.ioloop
- import tornado.wsgi
- except ImportError:
- raise Exception('tornado library not installed')
- wsgi_container = tornado.wsgi.WSGIContainer(self.app)
- http_server = tornado.httpserver.HTTPServer(wsgi_container, **options)
- http_server.listen(self.port, address=self.host)
- logger.info('Listening on %s:%s..', self.host, self.port)
- tornado.ioloop.IOLoop.instance().start()
- elif self.server == 'gevent':
- try:
- import gevent.pywsgi
- except ImportError:
- raise Exception('gevent library not installed')
- http_server = gevent.pywsgi.WSGIServer((self.host, self.port), self.app, **options)
- logger.info('Listening on %s:%s..', self.host, self.port)
- http_server.serve_forever()
- else:
- raise Exception(f'Server {self.server} not recognized')
- class FlaskJSONEncoder(json.JSONEncoder):
- def default(self, o):
- if isinstance(o, datetime.datetime):
- if o.tzinfo:
- # eg: '2015-09-25T23:14:42.588601+00:00'
- return o.isoformat('T')
- else:
- # No timezone present - assume UTC.
- # eg: '2015-09-25T23:14:42.588601Z'
- return o.isoformat('T') + 'Z'
- if isinstance(o, datetime.date):
- return o.isoformat()
- if isinstance(o, Decimal):
- return float(o)
- return json.JSONEncoder.default(self, o)
- class NumberConverter(werkzeug.routing.BaseConverter):
- """ Flask converter for OpenAPI number type """
- regex = r"[+-]?[0-9]*(\.[0-9]*)?"
- def to_python(self, value):
- return float(value)
- class IntegerConverter(werkzeug.routing.BaseConverter):
- """ Flask converter for OpenAPI integer type """
- regex = r"[+-]?[0-9]+"
- def to_python(self, value):
- return int(value)
|