doc.py 7.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. # -*- coding: utf-8 -*-
  2. import typing
  3. import bottle
  4. from apispec import APISpec
  5. from apispec import Path
  6. from apispec.ext.marshmallow.swagger import schema2jsonschema
  7. from hapic.context import ContextInterface, RouteRepresentation
  8. from hapic.decorator import DECORATION_ATTRIBUTE_NAME
  9. from hapic.decorator import DecoratedController
  10. from hapic.description import ControllerDescription
  11. from hapic.exception import NoRoutesException
  12. from hapic.exception import RouteNotFound
  13. def find_bottle_route(
  14. decorated_controller: DecoratedController,
  15. app: bottle.Bottle,
  16. ):
  17. if not app.routes:
  18. raise NoRoutesException('There is no routes in your bottle app')
  19. reference = decorated_controller.reference
  20. for route in app.routes:
  21. route_token = getattr(
  22. route.callback,
  23. DECORATION_ATTRIBUTE_NAME,
  24. None,
  25. )
  26. match_with_wrapper = route.callback == reference.wrapper
  27. match_with_wrapped = route.callback == reference.wrapped
  28. match_with_token = route_token == reference.token
  29. if match_with_wrapper or match_with_wrapped or match_with_token:
  30. return route
  31. # TODO BS 20171010: Raise exception or print error ? see #10
  32. raise RouteNotFound(
  33. 'Decorated route "{}" was not found in bottle routes'.format(
  34. decorated_controller.name,
  35. )
  36. )
  37. def bottle_generate_operations(
  38. spec,
  39. route: RouteRepresentation,
  40. description: ControllerDescription,
  41. ):
  42. method_operations = dict()
  43. # schema based
  44. if description.input_body:
  45. schema_class = type(description.input_body.wrapper.processor.schema)
  46. method_operations.setdefault('parameters', []).append({
  47. 'in': 'body',
  48. 'name': 'body',
  49. 'schema': {
  50. '$ref': '#/definitions/{}'.format(schema_class.__name__)
  51. }
  52. })
  53. if description.output_body:
  54. schema_class = type(description.output_body.wrapper.processor.schema)
  55. method_operations.setdefault('responses', {})\
  56. [int(description.output_body.wrapper.default_http_code)] = {
  57. 'description': str(description.output_body.wrapper.default_http_code), # nopep8
  58. 'schema': {
  59. '$ref': '#/definitions/{}'.format(schema_class.__name__)
  60. }
  61. }
  62. if description.output_file:
  63. method_operations.setdefault('produces', []).extend(
  64. description.output_file.wrapper.output_types
  65. )
  66. method_operations.setdefault('responses', {})\
  67. [int(description.output_file.wrapper.default_http_code)] = {
  68. 'description': str(description.output_file.wrapper.default_http_code), # nopep8
  69. }
  70. if description.errors:
  71. for error in description.errors:
  72. schema_class = type(error.wrapper.schema)
  73. method_operations.setdefault('responses', {})\
  74. [int(error.wrapper.http_code)] = {
  75. 'description': str(error.wrapper.http_code),
  76. 'schema': {
  77. '$ref': '#/definitions/{}'.format(schema_class.__name__) # nopep8
  78. }
  79. }
  80. # jsonschema based
  81. if description.input_path:
  82. schema_class = type(description.input_path.wrapper.processor.schema)
  83. # TODO: look schema2parameters ?
  84. jsonschema = schema2jsonschema(schema_class, spec=spec)
  85. for name, schema in jsonschema.get('properties', {}).items():
  86. method_operations.setdefault('parameters', []).append({
  87. 'in': 'path',
  88. 'name': name,
  89. 'required': name in jsonschema.get('required', []),
  90. 'type': schema['type']
  91. })
  92. if description.input_query:
  93. schema_class = type(description.input_query.wrapper.processor.schema)
  94. jsonschema = schema2jsonschema(schema_class, spec=spec)
  95. for name, schema in jsonschema.get('properties', {}).items():
  96. method_operations.setdefault('parameters', []).append({
  97. 'in': 'query',
  98. 'name': name,
  99. 'required': name in jsonschema.get('required', []),
  100. 'type': schema['type']
  101. })
  102. if description.input_files:
  103. method_operations.setdefault('consumes', []).append('multipart/form-data')
  104. for field_name, field in description.input_files.wrapper.processor.schema.fields.items():
  105. method_operations.setdefault('parameters', []).append({
  106. 'in': 'formData',
  107. 'name': field_name,
  108. 'required': field.required,
  109. 'type': 'file',
  110. })
  111. operations = {
  112. route.method.lower(): method_operations,
  113. }
  114. return operations
  115. class DocGenerator(object):
  116. def get_doc(
  117. self,
  118. controllers: typing.List[DecoratedController],
  119. context: ContextInterface,
  120. ) -> dict:
  121. spec = APISpec(
  122. title='Swagger Petstore',
  123. version='1.0.0',
  124. plugins=[
  125. # 'apispec.ext.bottle',
  126. 'apispec.ext.marshmallow',
  127. ],
  128. schema_name_resolver_callable=generate_schema_name,
  129. )
  130. schemas = []
  131. # parse schemas
  132. for controller in controllers:
  133. description = controller.description
  134. if description.input_body:
  135. schemas.append(type(
  136. description.input_body.wrapper.processor.schema
  137. ))
  138. if description.input_forms:
  139. schemas.append(type(
  140. description.input_forms.wrapper.processor.schema
  141. ))
  142. if description.output_body:
  143. schemas.append(type(
  144. description.output_body.wrapper.processor.schema
  145. ))
  146. if description.errors:
  147. for error in description.errors:
  148. schemas.append(type(error.wrapper.schema))
  149. for schema in set(schemas):
  150. spec.definition(schema.__name__, schema=schema)
  151. # add views
  152. # with app.test_request_context():
  153. paths = {}
  154. for controller in controllers:
  155. route = context.find_route(controller)
  156. swagger_path = context.get_swagger_path(route.rule)
  157. operations = bottle_generate_operations(
  158. spec,
  159. route,
  160. controller.description,
  161. )
  162. # TODO BS 20171114: TMP code waiting refact of doc
  163. doc_string = controller.reference.get_doc_string()
  164. if doc_string:
  165. for method in operations.keys():
  166. operations[method]['description'] = doc_string
  167. path = Path(path=swagger_path, operations=operations)
  168. if swagger_path in paths:
  169. paths[swagger_path].update(path)
  170. else:
  171. paths[swagger_path] = path
  172. spec.add_path(path)
  173. return spec.to_dict()
  174. # TODO BS 20171109: Must take care of already existing definition names
  175. def generate_schema_name(schema):
  176. return schema.__name__