doc.py 7.5KB

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