passerelle/passerelle/utils/api.py

258 lines
9.3 KiB
Python

# passerelle - uniform access to multiple data sources and services
# Copyright (C) 2016 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import inspect
from django.urls import reverse
from django.utils.dateparse import parse_date
from django.utils.safestring import mark_safe
# make APIError available from this module
from .jsonresponse import APIError # noqa pylint: disable=unused-import
class endpoint:
do_not_call_in_templates = True
def __init__(
self,
serializer_type='json-api',
perm='can_access',
methods=None,
name=None,
pattern=None,
wrap_response=False,
description=None,
description_get=None,
description_put=None,
description_post=None,
description_patch=None,
description_delete=None,
long_description=None,
long_description_get=None,
long_description_put=None,
long_description_post=None,
long_description_patch=None,
long_description_delete=None,
example_pattern=None,
parameters=None,
cache_duration=None,
post=None,
show=True,
show_undocumented_params=True,
display_order=0,
display_category='',
json_schema_response=None,
datasource=False,
# helper to define the POST json schema
post_json_schema=None,
):
self.perm = perm
if self.perm == 'OPEN':
self.perm = None
self.methods = methods or ['get']
self.serializer_type = serializer_type
self.pattern = pattern
self.name = name
self.wrap_response = wrap_response
self.descriptions = {
'get': description_get or description,
'put': description_put or description,
'post': description_post or description,
'patch': description_patch or description,
'delete': description_delete or description,
}
self.long_descriptions = {
'get': long_description_get or long_description,
'put': long_description_put or long_description,
'post': long_description_post or long_description,
'patch': long_description_patch or long_description,
'delete': long_description_delete or long_description,
}
self.example_pattern = example_pattern
self.parameters = parameters or {}
self.cache_duration = cache_duration
if post_json_schema:
post = post or {}
schema = post.setdefault('request_body', {}).setdefault('schema', {})
assert not schema.get('application/json'), 'a json schema was already set in the post argument'
schema['application/json'] = post_json_schema
self.post = post
if post:
self.methods = ['post']
if post.get('description'):
self.descriptions['post'] = post.get('description')
if post.get('long_description'):
self.long_descriptions['post'] = post.get('long_description')
self.show = show
self.show_undocumented_params = show_undocumented_params
self.display_order = display_order
self.display_category = display_category
self.response_schemas = {}
if json_schema_response:
self.response_schemas['application/json'] = json_schema_response
self.datasource = datasource
def __call__(self, func):
func.endpoint_info = self
if not self.name:
self.name = func.__name__
self.func = func
return func
@property
def display_category_order(self):
if not self.display_category:
# no category, put it at the end
return 99999999
# self.object is attached in BaseResource.get_endpoints_infos method
if self.display_category not in self.object._category_ordering:
# category without ordering, put it at the end, just before no category
return 99999998
return self.object._category_ordering.index(self.display_category)
def get_example_params(self):
return {
x: self.parameters[x]['example_value']
for x in self.parameters or {}
if x in self.parameters and 'example_value' in self.parameters[x]
}
def get_query_parameters(self):
query_parameters = []
for param, param_value in self.get_example_params().items():
if param in (self.example_pattern or ''):
continue
query_parameters.append((param, param_value))
return query_parameters
def example_url(self):
kwargs = {
'connector': self.object.get_connector_slug(),
'slug': self.object.slug,
'endpoint': self.name,
}
if self.example_pattern:
kwargs['rest'] = self.example_pattern.format(**self.get_example_params())
query_string = ''
query_parameters = self.get_query_parameters()
if query_parameters:
query_string = '?' + '&'.join(['%s=%s' % x for x in query_parameters])
return reverse('generic-endpoint', kwargs=kwargs) + query_string
def example_url_as_html(self):
kwargs = {
'connector': self.object.get_connector_slug(),
'slug': self.object.slug,
'endpoint': self.name,
}
if self.example_pattern:
kwargs['rest'] = self.example_pattern.format(
**{x: '$%s$' % x for x in self.get_example_params().keys()}
)
url = reverse('generic-endpoint', kwargs=kwargs)
for param in self.get_example_params():
url = url.replace('$%s$' % param, '<i class="varname">%s</i>' % param)
query_string = ''
query_parameters = self.get_query_parameters()
if query_parameters:
query_string = '?' + '&amp;'.join(
['%s=<i class="varname">%s</i>' % (x[0], x[0]) for x in query_parameters]
)
return mark_safe(url + query_string)
def has_params(self):
argspec = inspect.getfullargspec(self.func)
return len(argspec.args) > 2 # (self, request)
@property
def description(self):
return self.descriptions.get(self.http_method)
@property
def long_description(self):
return self.long_descriptions.get(self.http_method)
@property
def body_schemas(self):
if (
self.http_method == 'post'
and self.post
and 'request_body' in self.post
and 'schema' in self.post['request_body']
):
return self.post['request_body']['schema']
return {}
def get_params(self):
def type_to_str(value):
if isinstance(value, bool):
return 'boolean'
elif isinstance(value, int):
return 'integer'
elif isinstance(value, float):
return 'float'
elif isinstance(value, str):
try:
if parse_date(value):
return 'date'
except ValueError:
pass
params = []
available_params = self.parameters
spec = inspect.getfullargspec(self.func)
defaults = dict(zip(reversed(spec.args), reversed(spec.defaults or [])))
if self.show_undocumented_params:
available_params = {
arg: {} for arg in spec.args[2:] if arg != 'post_data' and not arg in self.parameters
}
available_params.update(self.parameters)
for param, info in available_params.items():
param_info = {'name': param}
if info.get('description'):
param_info['description'] = info['description']
if 'blank' in info:
param_info['blank'] = bool(info['blank'])
typ = None
if 'type' in info:
typ = info['type']
if typ == 'int':
typ = 'integer'
elif typ == 'bool':
typ = 'boolean'
elif 'example_value' in info:
typ = type_to_str(info['example_value'])
if param in defaults:
param_info['optional'] = True
param_info['default_value'] = defaults[param]
if not typ:
typ = type_to_str(defaults[param])
if info.get('optional') is True:
param_info['optional'] = True
if typ:
param_info['type'] = typ
params.append(param_info)
order = {name: i for i, name in enumerate(spec.args)}
params.sort(key=lambda x: (order.get(x['name'], 999), x['name']))
return params