258 lines
9.3 KiB
Python
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 = '?' + '&'.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
|