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._setup_ddr()
86 
88  try:
89  url = 'http://{}/api/v1/nodes/{}/parameters'.format(self.host, self.rest_name)
90  res = requests_retry_session().get(url)
91  if res.status_code != 200:
92  rospy.logerr("Getting parameters failed with status code: %d", res.status_code)
93  return []
94  return res.json()
95  except Exception as e:
96  rospy.logerr(str(e))
97  return []
98 
99  def _set_rest_parameters(self, parameters):
100  try:
101  url = 'http://{}/api/v1/nodes/{}/parameters'.format(self.host, self.rest_name)
102  res = requests_retry_session().put(url, json=parameters)
103  j = res.json()
104  rospy.logdebug("set parameters response: %s", json.dumps(j, indent=2))
105  if 'return_code' in j and j['return_code']['value'] != 0:
106  rospy.logwarn("Setting parameter failed: %s", j['return_code']['message'])
107  return []
108  if res.status_code != 200:
109  rospy.logerr("Setting parameters failed with status code: %d", res.status_code)
110  return []
111  return j
112  except Exception as e:
113  rospy.logerr(str(e))
114  return []
115 
116  def _setup_ddr(self):
117  self.ddr = DDynamicReconfigure(rospy.get_name())
118  rest_params = [p for p in self._get_rest_parameters() if p['name'] not in self.ignored_parameters]
119 
120  def enum_method_from_param(p):
121  if p['type'] != 'string':
122  return ""
123  enum_matches = re.findall(r'.*\[(?P<enum>.+)\].*', p['description'])
124  if not enum_matches:
125  return ""
126  enum_names = [str(e.strip()) for e in enum_matches[0].split(',')]
127  enum_list = [self.ddr.const(n, 'str', n, n) for n in enum_names]
128  return self.ddr.enum(enum_list, p['name'] + '_enum')
129 
130  for p in rest_params:
131  level = 0
132  edit_method = enum_method_from_param(p)
133  if p['type'] == 'int32':
134  self.ddr.add(p['name'], 'int', level, p['description'], p['default'], p['min'], p['max'])
135  elif p['type'] == 'float64':
136  self.ddr.add(p['name'], 'double', level, p['description'], p['default'], p['min'], p['max'])
137  elif p['type'] == 'string':
138  self.ddr.add(p['name'], 'str', level, p['description'], str(p['default']), edit_method=edit_method)
139  elif p['type'] == 'bool':
140  self.ddr.add(p['name'], 'bool', level, p['description'], p['default'])
141  else:
142  rospy.logwarn("Unsupported parameter type: %s", p['type'])
143 
144  self.ddr.start(self._dyn_rec_callback)
145 
146  def _dyn_rec_callback(self, config, level):
147  rospy.logdebug("Received reconf call: " + str(config))
148  new_rest_params = [{'name': n, 'value': config[n]} for n in self.ddr.get_variable_names() if n in config]
149  if new_rest_params:
150  returned_params = self._set_rest_parameters(new_rest_params)
151  for p in returned_params:
152  if p['name'] not in config:
153  rospy.logerr("param %s not in config", p['name'])
154  continue
155  config[p['name']] = p['value']
156  return config
157 
158  def call_rest_service(self, name, srv_type=None, request=None):
159  try:
160  args = {}
161  if request is not None:
162  # convert ROS request to JSON (with custom API mappings)
163  args = convert_ros_message_to_dictionary(request)
164  rospy.logdebug('calling {} with args: {}'.format(name, args))
165 
166  url = 'http://{}/api/v1/nodes/{}/services/{}'.format(self.host, self.rest_name, name)
167  res = requests_retry_session().put(url, json={'args': args})
168 
169  j = res.json()
170  rospy.logdebug("{} rest response: {}".format(name, json.dumps(j, indent=2)))
171  rc = j['response'].get('return_code')
172  if rc is not None and rc['value'] < 0:
173  rospy.logwarn("service {} returned an error: [{}] {}".format(name, rc['value'], rc['message']))
174 
175  # convert to ROS response
176  if srv_type is not None:
177  response = convert_dictionary_to_ros_message(srv_type._response_class(), j['response'], strict_mode=False)
178  else:
179  response = j['response']
180  except Exception as e:
181  rospy.logerr(str(e))
182  if srv_type is not None:
183  response = srv_type._response_class()
184  if hasattr(response, 'return_code'):
185  response.return_code.value = -1000
186  response.return_code.message = str(e)
187  return response
188 
189  def add_rest_service(self, srv_type, srv_name, callback):
190  """create a service and inject the REST-API service name"""
191  srv = rospy.Service(rospy.get_name() + "/" + srv_name, srv_type, partial(callback, srv_name, srv_type))
192  self.rest_services.append(srv)
def __init__(self, rest_name, ignored_parameters=[])
Definition: rest_client.py:72
def call_rest_service(self, name, srv_type=None, request=None)
Definition: rest_client.py:158
def _set_rest_parameters(self, parameters)
Definition: rest_client.py:99
def add_rest_service(self, srv_type, srv_name, callback)
Definition: rest_client.py:189
def _dyn_rec_callback(self, config, level)
Definition: rest_client.py:146
def requests_retry_session(retries=3, backoff_factor=0.3, status_forcelist=[429], session=None)
Definition: rest_client.py:50


rc_reason_clients
Author(s):
autogenerated on Sat Jun 17 2023 02:48:57