123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485 |
- # -*- coding: utf-8 -*-
- import os
- import typing
- import uuid
- import functools
- try: # Python 3.5+
- from http import HTTPStatus
- except ImportError:
- from http import client as HTTPStatus
-
- from hapic.buffer import DecorationBuffer
- from hapic.context import ContextInterface
- from hapic.decorator import DecoratedController
- from hapic.decorator import DECORATION_ATTRIBUTE_NAME
- from hapic.decorator import ControllerReference
- from hapic.decorator import ExceptionHandlerControllerWrapper
- from hapic.decorator import InputBodyControllerWrapper
- from hapic.decorator import InputHeadersControllerWrapper
- from hapic.decorator import InputPathControllerWrapper
- from hapic.decorator import InputQueryControllerWrapper
- from hapic.decorator import InputFilesControllerWrapper
- from hapic.decorator import OutputBodyControllerWrapper
- from hapic.decorator import OutputHeadersControllerWrapper
- from hapic.decorator import OutputFileControllerWrapper
- from hapic.description import InputBodyDescription
- from hapic.description import ErrorDescription
- from hapic.description import InputFormsDescription
- from hapic.description import InputHeadersDescription
- from hapic.description import InputPathDescription
- from hapic.description import InputQueryDescription
- from hapic.description import InputFilesDescription
- from hapic.description import OutputBodyDescription
- from hapic.description import OutputHeadersDescription
- from hapic.description import OutputFileDescription
- from hapic.doc import DocGenerator
- from hapic.processor import ProcessorInterface
- from hapic.processor import MarshmallowInputProcessor
- from hapic.processor import MarshmallowInputFilesProcessor
- from hapic.processor import MarshmallowOutputProcessor
- from hapic.error import ErrorBuilderInterface
-
-
- # TODO: Gérer les cas ou c'est une liste la réponse (items, item_nb), see #12
- # TODO: Confusion nommage body/json/forms, see #13
-
-
- class Hapic(object):
- def __init__(self):
- self._buffer = DecorationBuffer()
- self._controllers = [] # type: typing.List[DecoratedController]
- self._context = None # type: ContextInterface
- self._error_builder = None # type: ErrorBuilderInterface
- self.doc_generator = DocGenerator()
-
- # This local function will be pass to different components
- # who will need context but declared (like with decorator)
- # before context declaration
- def context_getter():
- return self._context
-
- # This local function will be pass to different components
- # who will need error_builder but declared (like with decorator)
- # before error_builder declaration
- def error_builder_getter():
- return self._context.get_default_error_builder()
-
- self._context_getter = context_getter
- self._error_builder_getter = error_builder_getter
-
- # TODO: Permettre la surcharge des classes utilisés ci-dessous, see #14
-
- @property
- def controllers(self) -> typing.List[DecoratedController]:
- return self._controllers
-
- @property
- def context(self) -> ContextInterface:
- return self._context
-
- def set_context(self, context: ContextInterface) -> None:
- assert not self._context
- self._context = context
-
- def reset_context(self) -> None:
- self._context = None
-
- def with_api_doc(self, tags: typing.List['str']=None):
- """
- Permit to generate doc about a controller. Use as a decorator:
-
- ```
- @hapic.with_api_doc()
- def my_controller(self):
- # ...
- ```
-
- What it do: Register this controller with all previous given
- information like `@hapic.input_path(...)` etc.
-
- :param tags: list of string tags (OpenApi)
- :return: The decorator
- """
- # FIXME BS 20171228: Documenter sur ce que ça fait vraiment (tester:
- # on peut l'enlever si on veut pas generer la doc ?)
- tags = tags or [] # FDV
-
- def decorator(func):
- @functools.wraps(func)
- def wrapper(*args, **kwargs):
- return func(*args, **kwargs)
-
- token = uuid.uuid4().hex
- setattr(wrapper, DECORATION_ATTRIBUTE_NAME, token)
- setattr(func, DECORATION_ATTRIBUTE_NAME, token)
-
- description = self._buffer.get_description()
- description.tags = tags
-
- reference = ControllerReference(
- wrapper=wrapper,
- wrapped=func,
- token=token,
- )
- decorated_controller = DecoratedController(
- reference=reference,
- description=description,
- name=func.__name__,
- )
- self._buffer.clear()
- self._controllers.append(decorated_controller)
- return wrapper
-
- return decorator
-
- def output_body(
- self,
- schema: typing.Any,
- processor: ProcessorInterface = None,
- context: ContextInterface = None,
- error_http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
- default_http_code: HTTPStatus = HTTPStatus.OK,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- processor = processor or MarshmallowOutputProcessor()
- processor.schema = schema
- context = context or self._context_getter
- decoration = OutputBodyControllerWrapper(
- context=context,
- processor=processor,
- error_http_code=error_http_code,
- default_http_code=default_http_code,
- )
-
- def decorator(func):
- self._buffer.output_body = OutputBodyDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- def output_headers(
- self,
- schema: typing.Any,
- processor: ProcessorInterface = None,
- context: ContextInterface = None,
- error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
- default_http_code: HTTPStatus = HTTPStatus.OK,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- processor = processor or MarshmallowOutputProcessor()
- processor.schema = schema
- context = context or self._context_getter
-
- decoration = OutputHeadersControllerWrapper(
- context=context,
- processor=processor,
- error_http_code=error_http_code,
- default_http_code=default_http_code,
- )
-
- def decorator(func):
- self._buffer.output_headers = OutputHeadersDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- # TODO BS 20171102: Think about possibilities to validate output ?
- # (with mime type, or validator)
- def output_file(
- self,
- output_types: typing.List[str],
- default_http_code: HTTPStatus = HTTPStatus.OK,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- decoration = OutputFileControllerWrapper(
- output_types=output_types,
- default_http_code=default_http_code,
- )
-
- def decorator(func):
- self._buffer.output_file = OutputFileDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- def input_headers(
- self,
- schema: typing.Any,
- processor: ProcessorInterface = None,
- context: ContextInterface = None,
- error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
- default_http_code: HTTPStatus = HTTPStatus.OK,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- processor = processor or MarshmallowInputProcessor()
- processor.schema = schema
- context = context or self._context_getter
-
- decoration = InputHeadersControllerWrapper(
- context=context,
- processor=processor,
- error_http_code=error_http_code,
- default_http_code=default_http_code,
- )
-
- def decorator(func):
- self._buffer.input_headers = InputHeadersDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- def input_path(
- self,
- schema: typing.Any,
- processor: ProcessorInterface = None,
- context: ContextInterface = None,
- error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
- default_http_code: HTTPStatus = HTTPStatus.OK,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- processor = processor or MarshmallowInputProcessor()
- processor.schema = schema
- context = context or self._context_getter
-
- decoration = InputPathControllerWrapper(
- context=context,
- processor=processor,
- error_http_code=error_http_code,
- default_http_code=default_http_code,
- )
-
- def decorator(func):
- self._buffer.input_path = InputPathDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- def input_query(
- self,
- schema: typing.Any,
- processor: ProcessorInterface = None,
- context: ContextInterface = None,
- error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
- default_http_code: HTTPStatus = HTTPStatus.OK,
- as_list: typing.List[str]=None,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- processor = processor or MarshmallowInputProcessor()
- processor.schema = schema
- context = context or self._context_getter
-
- decoration = InputQueryControllerWrapper(
- context=context,
- processor=processor,
- error_http_code=error_http_code,
- default_http_code=default_http_code,
- as_list=as_list,
- )
-
- def decorator(func):
- self._buffer.input_query = InputQueryDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- def input_body(
- self,
- schema: typing.Any,
- processor: ProcessorInterface = None,
- context: ContextInterface = None,
- error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
- default_http_code: HTTPStatus = HTTPStatus.OK,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- processor = processor or MarshmallowInputProcessor()
- processor.schema = schema
- context = context or self._context_getter
-
- decoration = InputBodyControllerWrapper(
- context=context,
- processor=processor,
- error_http_code=error_http_code,
- default_http_code=default_http_code,
- )
-
- def decorator(func):
- self._buffer.input_body = InputBodyDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- def input_forms(
- self,
- schema: typing.Any,
- processor: ProcessorInterface=None,
- context: ContextInterface=None,
- error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
- default_http_code: HTTPStatus = HTTPStatus.OK,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- processor = processor or MarshmallowInputProcessor()
- processor.schema = schema
- context = context or self._context_getter
-
- decoration = InputBodyControllerWrapper(
- context=context,
- processor=processor,
- error_http_code=error_http_code,
- default_http_code=default_http_code,
- )
-
- def decorator(func):
- self._buffer.input_forms = InputFormsDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- def input_files(
- self,
- schema: typing.Any,
- processor: ProcessorInterface=None,
- context: ContextInterface=None,
- error_http_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
- default_http_code: HTTPStatus = HTTPStatus.OK,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- processor = processor or MarshmallowInputFilesProcessor()
- processor.schema = schema
- context = context or self._context_getter
-
- decoration = InputFilesControllerWrapper(
- context=context,
- processor=processor,
- error_http_code=error_http_code,
- default_http_code=default_http_code,
- )
-
- def decorator(func):
- self._buffer.input_files = InputFilesDescription(decoration)
- return decoration.get_wrapper(func)
- return decorator
-
- def handle_exception(
- self,
- handled_exception_class: typing.Type[Exception]=Exception,
- http_code: HTTPStatus = HTTPStatus.INTERNAL_SERVER_ERROR,
- error_builder: ErrorBuilderInterface=None,
- context: ContextInterface = None,
- ) -> typing.Callable[[typing.Callable[..., typing.Any]], typing.Any]:
- context = context or self._context_getter
- error_builder = error_builder or self._error_builder_getter
-
- decoration = ExceptionHandlerControllerWrapper(
- handled_exception_class,
- context,
- error_builder=error_builder,
- http_code=http_code,
- )
-
- def decorator(func):
- self._buffer.errors.append(ErrorDescription(decoration))
- return decoration.get_wrapper(func)
- return decorator
-
- def generate_doc(self, title: str='', description: str='') -> dict:
- """
- See hapic.doc.DocGenerator#get_doc docstring
- :param title: Title of generated doc
- :param description: Description of generated doc
- :return: dict containing apispec doc
- """
- return self.doc_generator.get_doc(
- self._controllers,
- self.context,
- title=title,
- description=description,
- )
-
- def save_doc_in_file(
- self,
- file_path: str,
- title: str='',
- description: str='',
- ) -> None:
- """
- See hapic.doc.DocGenerator#get_doc docstring
- :param file_path: The file path to write doc in YAML format
- :param title: Title of generated doc
- :param description: Description of generated doc
- """
- self.doc_generator.save_in_file(
- file_path,
- controllers=self._controllers,
- context=self.context,
- title=title,
- description=description,
- )
-
- def add_documentation_view(
- self,
- route: str,
- title: str='',
- description: str='',
- ) -> None:
- # Ensure "/" at end of route, else web browser will not consider it as
- # a path
- if not route.endswith('/'):
- route = '{}/'.format(route)
-
- swaggerui_path = os.path.join(
- os.path.dirname(os.path.abspath(__file__)),
- 'static',
- 'swaggerui',
- )
-
- # Documentation file view
- doc_yaml = self.doc_generator.get_doc_yaml(
- controllers=self._controllers,
- context=self.context,
- title=title,
- description=description,
- )
-
- def spec_yaml_view(*args, **kwargs):
- """
- Method to return swagger generated yaml spec file.
-
- This method will be call as a framework view, like those,
- it need to handle the default arguments of a framework view.
- As frameworks have different arguments patterns, we should
- allow any arguments patterns (args, kwargs).
- """
- return self.context.get_response(
- doc_yaml,
- mimetype='text/x-yaml',
- http_code=HTTPStatus.OK,
- )
-
- # Prepare views html content
- doc_index_path = os.path.join(swaggerui_path, 'index.html')
- with open(doc_index_path, 'r') as doc_page:
- doc_page_content = doc_page.read()
- doc_page_content = doc_page_content.replace(
- '{{ spec_uri }}',
- 'spec.yml',
- )
-
- # Declare the swaggerui view
- def api_doc_view(*args, **kwargs):
- """
- Method to return html index view of swagger ui.
-
- This method will be call as a framework view, like those,
- it need to handle the default arguments of a framework view.
- As frameworks have different arguments patterns, we should
- allow any arguments patterns (args, kwargs).
- """
- return self.context.get_response(
- doc_page_content,
- http_code=HTTPStatus.OK,
- mimetype='text/html',
- )
-
- # Add a view to generate the html index page of swagger-ui
- self.context.add_view(
- route=route,
- http_method='GET',
- view_func=api_doc_view,
- )
-
- # Add a doc yaml view
- self.context.add_view(
- route=os.path.join(route, 'spec.yml'),
- http_method='GET',
- view_func=spec_yaml_view,
- )
-
- # Add swagger directory as served static dir
- self.context.serve_directory(
- route,
- swaggerui_path,
- )
|