rest_client.py
Go to the documentation of this file.
1 # Copyright 2021 Roboception GmbH
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions are met:
5 #
6 # * Redistributions of source code must retain the above copyright
7 # notice, this list of conditions and the following disclaimer.
8 #
9 # * Redistributions in binary form must reproduce the above copyright
10 # notice, this list of conditions and the following disclaimer in the
11 # documentation and/or other materials provided with the distribution.
12 #
13 # * Neither the name of the {copyright_holder} nor the names of its
14 # contributors may be used to endorse or promote products derived from
15 # this software without specific prior written permission.
16 #
17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18 # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20 # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
21 # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22 # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23 # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24 # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25 # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26 # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 
29 from __future__ import absolute_import
30 
31 from functools import partial
32 
33 import rospy
34 
35 import json
36 import re
37 import sys
38 
39 import requests
40 from requests.adapters import HTTPAdapter
41 from requests.packages.urllib3.util.retry import Retry
42 
43 from ddynamic_reconfigure_python.ddynamic_reconfigure import DDynamicReconfigure
44 
45 from .message_converter import convert_dictionary_to_ros_message, convert_ros_message_to_dictionary
46 
47 def requests_retry_session(retries=3,
48  backoff_factor=0.3,
49  status_forcelist=[429],
50  session=None):
51  """"
52  A requests session that will retry on TOO_MANY_REQUESTS.
53 
54  E.g. replace requests.get() with requests_retry_session().get()
55  """
56  session = session or requests.Session()
57  retry = Retry(
58  total=retries,
59  read=retries,
60  connect=retries,
61  backoff_factor=backoff_factor,
62  status_forcelist=status_forcelist,
63  )
64  adapter = HTTPAdapter(max_retries=retry)
65  session.mount('http://', adapter)
66  session.mount('https://', adapter)
67  return session
68 
69 
70 class RestClient(object):
71 
72  def __init__(self, rest_name, ignored_parameters=[]):
73  self.rest_name = rest_name
74  self.ignored_parameters = ignored_parameters
75  self.rest_services = []
76  self.ddr = None
77 
78  rospy.init_node(rest_name + '_client', log_level=rospy.INFO)
79 
80  self.host = rospy.get_param('~host', '')
81  if not self.host:
82  rospy.logerr('host is not set')
83  sys.exit(1)
84 
85  self.pipeline = rospy.get_param('~pipeline', 0)
86 
87  self.api_node_prefix = 'http://{}/api/v2/pipelines/{}/nodes/{}'.format(self.host, self.pipeline, self.rest_name)
88 
89  self._setup_ddr()
90 
92  try:
93  url = '{}/parameters'.format(self.api_node_prefix)
94  res = requests_retry_session().get(url)
95  if res.status_code != 200:
96  rospy.logerr("Getting parameters failed with status code: %d", res.status_code)
97  return []
98  return res.json()
99  except Exception as e:
100  rospy.logerr(str(e))
101  return []
102 
103  def _set_rest_parameters(self, parameters):
104  try:
105  url = '{}/parameters'.format(self.api_node_prefix)
106  res = requests_retry_session().put(url, json=parameters)
107  j = res.json()
108  rospy.logdebug("set parameters response: %s", json.dumps(j, indent=2))
109  if 'return_code' in j and j['return_code']['value'] != 0:
110  rospy.logwarn("Setting parameter failed: %s", j['return_code']['message'])
111  return []
112  if res.status_code != 200:
113  rospy.logerr("Setting parameters failed with status code: %d", res.status_code)
114  return []
115  return j
116  except Exception as e:
117  rospy.logerr(str(e))
118  return []
119 
120  def _setup_ddr(self):
121  self.ddr = DDynamicReconfigure(rospy.get_name())
122  rest_params = [p for p in self._get_rest_parameters() if p['name'] not in self.ignored_parameters]
123 
124  def enum_method_from_param(p):
125  if p['type'] != 'string':
126  return ""
127  enum_matches = re.findall(r'.*\[(?P<enum>.+)\].*', p['description'])
128  if not enum_matches:
129  return ""
130  enum_names = [str(e.strip()) for e in enum_matches[0].split(',')]
131  enum_list = [self.ddr.const(n, 'str', n, n) for n in enum_names]
132  return self.ddr.enum(enum_list, p['name'] + '_enum')
133 
134  for p in rest_params:
135  level = 0
136  edit_method = enum_method_from_param(p)
137  if p['type'] == 'int32':
138  self.ddr.add(p['name'], 'int', level, p['description'], p['default'], p['min'], p['max'])
139  elif p['type'] == 'float64':
140  self.ddr.add(p['name'], 'double', level, p['description'], p['default'], p['min'], p['max'])
141  elif p['type'] == 'string':
142  self.ddr.add(p['name'], 'str', level, p['description'], str(p['default']), edit_method=edit_method)
143  elif p['type'] == 'bool':
144  self.ddr.add(p['name'], 'bool', level, p['description'], p['default'])
145  else:
146  rospy.logwarn("Unsupported parameter type: %s", p['type'])
147 
148  self.ddr.start(self._dyn_rec_callback)
149 
150  def _dyn_rec_callback(self, config, level):
151  rospy.logdebug("Received reconf call: " + str(config))
152  new_rest_params = [{'name': n, 'value': config[n]} for n in self.ddr.get_variable_names() if n in config]
153  if new_rest_params:
154  returned_params = self._set_rest_parameters(new_rest_params)
155  for p in returned_params:
156  if p['name'] not in config:
157  rospy.logerr("param %s not in config", p['name'])
158  continue
159  config[p['name']] = p['value']
160  return config
161 
162  def call_rest_service(self, name, srv_type=None, request=None):
163  try:
164  args = {}
165  if request is not None:
166  # convert ROS request to JSON (with custom API mappings)
167  args = convert_ros_message_to_dictionary(request)
168  rospy.logdebug('calling {} with args: {}'.format(name, args))
169 
170  url = '{}/services/{}'.format(self.api_node_prefix, name)
171  res = requests_retry_session().put(url, json={'args': args})
172 
173  j = res.json()
174  rospy.logdebug("{} rest response: {}".format(name, json.dumps(j, indent=2)))
175  rc = j['response'].get('return_code')
176  if rc is not None and rc['value'] < 0:
177  rospy.logwarn("service {} returned an error: [{}] {}".format(name, rc['value'], rc['message']))
178 
179  # convert to ROS response
180  if srv_type is not None:
181  response = convert_dictionary_to_ros_message(srv_type._response_class(), j['response'], strict_mode=False)
182  else:
183  response = j['response']
184  except Exception as e:
185  rospy.logerr(str(e))
186  if srv_type is not None:
187  response = srv_type._response_class()
188  if hasattr(response, 'return_code'):
189  response.return_code.value = -1000
190  response.return_code.message = str(e)
191  return response
192 
193  def add_rest_service(self, srv_type, srv_name, callback):
194  """create a service and inject the REST-API service name"""
195  srv = rospy.Service(rospy.get_name() + "/" + srv_name, srv_type, partial(callback, srv_name, srv_type))
196  self.rest_services.append(srv)
rc_reason_clients.rest_client.RestClient.rest_services
rest_services
Definition: rest_client.py:75
ddynamic_reconfigure_python::ddynamic_reconfigure::DDynamicReconfigure
rc_reason_clients.rest_client.RestClient._setup_ddr
def _setup_ddr(self)
Definition: rest_client.py:120
rc_reason_clients.rest_client.RestClient.host
host
Definition: rest_client.py:80
rc_reason_clients.rest_client.RestClient.add_rest_service
def add_rest_service(self, srv_type, srv_name, callback)
Definition: rest_client.py:193
rc_reason_clients.rest_client.requests_retry_session
def requests_retry_session(retries=3, backoff_factor=0.3, status_forcelist=[429], session=None)
Definition: rest_client.py:47
rc_reason_clients.rest_client.RestClient._dyn_rec_callback
def _dyn_rec_callback(self, config, level)
Definition: rest_client.py:150
rc_reason_clients.rest_client.RestClient.__init__
def __init__(self, rest_name, ignored_parameters=[])
Definition: rest_client.py:72
ddynamic_reconfigure_python::ddynamic_reconfigure
rc_reason_clients.rest_client.RestClient._set_rest_parameters
def _set_rest_parameters(self, parameters)
Definition: rest_client.py:103
rc_reason_clients.rest_client.RestClient.pipeline
pipeline
Definition: rest_client.py:85
rc_reason_clients.rest_client.RestClient.ddr
ddr
Definition: rest_client.py:76
rc_reason_clients.rest_client.RestClient
Definition: rest_client.py:70
rc_reason_clients.rest_client.RestClient.ignored_parameters
ignored_parameters
Definition: rest_client.py:74
rc_reason_clients.rest_client.RestClient.call_rest_service
def call_rest_service(self, name, srv_type=None, request=None)
Definition: rest_client.py:162
rc_reason_clients.rest_client.RestClient.api_node_prefix
api_node_prefix
Definition: rest_client.py:87
rc_reason_clients.rest_client.RestClient._get_rest_parameters
def _get_rest_parameters(self)
Definition: rest_client.py:91
rc_reason_clients.rest_client.RestClient.rest_name
rest_name
Definition: rest_client.py:73


rc_reason_clients
Author(s):
autogenerated on Sat Nov 23 2024 03:19:24