oca-technical/odoo-bringout-oca-rest-framework-base_rest/base_rest/components/service.py
2025-08-29 15:43:03 +02:00

217 lines
7 KiB
Python

# Copyright 2018 ACSONE SA/NV
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
import logging
from werkzeug import Response
from werkzeug.exceptions import NotFound
from odoo.http import request
from odoo.addons.component.core import AbstractComponent
from ..apispec.base_rest_service_apispec import BaseRestServiceAPISpec
from ..tools import ROUTING_DECORATOR_ATTR
_logger = logging.getLogger(__name__)
def to_int(val):
# The javascript VM ducktape only use float and so pass float
# to the api, the werkzeug request interpret params as unicode
# so we have to convert params from string to float to int
if isinstance(val, int):
return val
if val:
return int(float(val))
return None
def to_bool(val):
return val in ("true", "True", "1", True)
def skip_secure_params(func):
"""
Used to decorate methods
:param func:
:return:
"""
func.skip_secure_params = True
return func
def skip_secure_response(func):
"""
Used to decorate methods
:param func:
:return:
"""
func.skip_secure_response = True
return func
class BaseRestService(AbstractComponent):
_name = "base.rest.service"
_description = None # description included into the openapi doc
_is_rest_service_component = True # marker to retrieve REST components
def _prepare_extra_log(self, func, params, secure_params, res):
httprequest = request.httprequest
headers = dict(httprequest.headers)
return {
"application": "Rest Service",
"request_url": httprequest.url,
"request_method": httprequest.method,
"params": params,
"headers": headers,
"secure_params": secure_params,
"res": res,
"status": 200,
}
def _log_call(self, func, params, secure_params, res):
"""If you want to enjoy the advanced log install the module
logging_json"""
if request:
httprequest = request.httprequest
extra = self._prepare_extra_log(func, params, secure_params, res)
args = [httprequest.url, httprequest.method]
message = "REST call url %s method %s"
_logger.debug(message, *args, extra=extra)
def _prepare_input_params(self, method, params):
"""
Internal method used to process the input_param parameter. The
result will be used to call the final method. The processing is
delegated to the `resapi.RestMethodParam` instance specified by the
restapi.method` decorator on the method.
:param method:
:param params:
:return:
"""
method_name = method.__name__
if hasattr(method, "skip_secure_params"):
return params
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
if not routing:
_logger.warning(
"Method %s is not a public method of service %s",
method_name,
self._name,
)
raise NotFound()
input_param = routing["input_param"]
if input_param:
return input_param.from_params(self, params)
return {}
def _prepare_response(self, method, result):
"""
Internal method used to process the result of the method called by the
controller. The result of this process is returned to the controller
The processing is delegated to the `resapi.RestMethodParam` instance
specified by the `restapi.method` decorator on the method.
:param method: method
:param response:
:return: dict/json or `http.Response`
"""
method_name = method
if callable(method):
method_name = method.__name__
if hasattr(method, "skip_secure_response"):
return result
routing = getattr(method, ROUTING_DECORATOR_ATTR, None)
output_param = routing["output_param"]
if not output_param:
_logger.warning(
"DEPRECATED: You must define an output schema for method %s "
"in service %s",
method_name,
self._name,
)
return result
return output_param.to_response(self, result)
def dispatch(self, method_name, *args, params=None):
"""
This method dispatch the call to the final method.
Before the call parameters are processed by the
`restapi.RestMethodParam` object specified as input_param object.
The result of the method is therefore given to the
`restapi.RestMethodParam` object specified as output_param to build
the final response returned by the service
:param method_name:
:param *args: query path paramters args
:param params: A dictionary with the parameters of the method. Once
secured and sanitized, these parameters will be passed
to the method as keyword args.
:return:
"""
method = getattr(self, method_name, object())
params = params or {}
secure_params = self._prepare_input_params(method, params)
if isinstance(secure_params, dict):
# for backward compatibility methods expecting json params
# are declared as m(self, p1=None, p2=None) or m(self, **params)
res = method(*args, **secure_params)
else:
res = method(*args, secure_params)
self._log_call(method, params, secure_params, res)
if isinstance(res, Response):
return res
return self._prepare_response(method, res)
def _validator_delete(self):
"""
Default validator for delete method.
By default delete should never be called with parameters.
"""
return {}
def _validator_get(self):
"""
Default validator for get method.
By default get should not be called with parameters.
"""
return {}
def _get_api_spec(self, **params):
return BaseRestServiceAPISpec(self, **params)
def to_openapi(self, **params):
"""
Return the description of this REST service as an OpenAPI json document
:return: json document
"""
api_spec = self._get_api_spec(**params)
api_spec.generate_paths()
return api_spec.to_dict()
def _get_openapi_default_parameters(self):
return []
def _get_openapi_default_responses(self):
return {
"400": {"description": "One of the given parameter is not valid"},
"401": {
"description": "The user is not authorized. Authentication "
"is required"
},
"404": {"description": "Requested resource not found"},
"403": {
"description": "You don't have the permission to access the "
"requested resource."
},
}
@property
def request(self):
return self.work.request
@property
def controller(self):
return self.work.controller