bosdyn-client 5.0.1.2__py3-none-any.whl → 5.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. bosdyn/client/access_controlled_door_util.py +206 -0
  2. bosdyn/client/arm_surface_contact.py +2 -2
  3. bosdyn/client/async_tasks.py +3 -2
  4. bosdyn/client/audio_visual_helpers.py +3 -2
  5. bosdyn/client/autowalk.py +0 -2
  6. bosdyn/client/command_line.py +72 -15
  7. bosdyn/client/common.py +1 -1
  8. bosdyn/client/data_acquisition.py +3 -5
  9. bosdyn/client/data_acquisition_helpers.py +0 -3
  10. bosdyn/client/data_acquisition_plugin.py +1 -2
  11. bosdyn/client/data_acquisition_plugin_service.py +3 -2
  12. bosdyn/client/data_acquisition_store.py +1 -7
  13. bosdyn/client/data_buffer.py +5 -4
  14. bosdyn/client/directory_registration.py +3 -2
  15. bosdyn/client/estop.py +3 -2
  16. bosdyn/client/fault.py +1 -1
  17. bosdyn/client/gps/aggregator_client.py +2 -4
  18. bosdyn/client/gps/gps_listener.py +5 -7
  19. bosdyn/client/gps/ntrip_client.py +12 -3
  20. bosdyn/client/graph_nav.py +67 -13
  21. bosdyn/client/hazard_avoidance.py +119 -0
  22. bosdyn/client/image.py +5 -4
  23. bosdyn/client/image_service_helpers.py +6 -7
  24. bosdyn/client/ir_enable_disable.py +1 -1
  25. bosdyn/client/keepalive.py +4 -2
  26. bosdyn/client/lease.py +3 -2
  27. bosdyn/client/lease_validator.py +0 -1
  28. bosdyn/client/log_status.py +57 -3
  29. bosdyn/client/map_processing.py +2 -4
  30. bosdyn/client/network_compute_bridge_client.py +4 -6
  31. bosdyn/client/payload.py +2 -3
  32. bosdyn/client/payload_registration.py +11 -10
  33. bosdyn/client/power.py +84 -27
  34. bosdyn/client/processors.py +27 -2
  35. bosdyn/client/recording.py +3 -3
  36. bosdyn/client/robot_command.py +22 -22
  37. bosdyn/client/robot_state.py +1 -1
  38. bosdyn/client/sdk.py +2 -3
  39. bosdyn/client/service_customization_helpers.py +1 -1
  40. bosdyn/client/spot_cam/audio.py +1 -2
  41. bosdyn/client/spot_cam/health.py +1 -1
  42. bosdyn/client/spot_cam/lighting.py +1 -1
  43. bosdyn/client/spot_cam/media_log.py +1 -1
  44. bosdyn/client/spot_cam/network.py +3 -2
  45. bosdyn/client/spot_cam/power.py +1 -1
  46. bosdyn/client/spot_cam/ptz.py +1 -1
  47. bosdyn/client/spot_cam/streamquality.py +1 -1
  48. bosdyn/client/spot_cam/version.py +1 -1
  49. bosdyn/client/spot_check.py +5 -6
  50. bosdyn/client/url_validation_util.py +220 -0
  51. bosdyn/client/util.py +2 -4
  52. bosdyn/client/world_object.py +1 -1
  53. {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/METADATA +3 -3
  54. bosdyn_client-5.1.1.dist-info/RECORD +106 -0
  55. bosdyn_client-5.0.1.2.dist-info/RECORD +0 -103
  56. {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/WHEEL +0 -0
  57. {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/top_level.txt +0 -0
@@ -8,27 +8,26 @@
8
8
  import collections
9
9
  import time
10
10
 
11
- from google.protobuf import any_pb2, wrappers_pb2
12
-
13
- from bosdyn import geometry
14
11
  from bosdyn.api import (arm_command_pb2, basic_command_pb2, full_body_command_pb2, geometry_pb2,
15
- gripper_command_pb2, mobility_command_pb2, payload_estimation_pb2,
16
- robot_command_pb2, robot_command_service_pb2_grpc, synchronized_command_pb2,
17
- trajectory_pb2)
12
+ mobility_command_pb2, payload_estimation_pb2, robot_command_pb2,
13
+ robot_command_service_pb2_grpc, synchronized_command_pb2, trajectory_pb2)
18
14
 
19
15
  # isort: off
20
16
  # isort: on
17
+ from google.protobuf import any_pb2, wrappers_pb2
18
+
19
+ from bosdyn import geometry
21
20
  from bosdyn.api.spot import robot_command_pb2 as spot_command_pb2
22
21
  from bosdyn.client.common import (BaseClient, error_factory, error_pair,
23
22
  handle_common_header_errors, handle_lease_use_result_errors,
24
23
  handle_unset_status_error)
25
- from bosdyn.util import seconds_to_duration
24
+ from bosdyn.util import now_sec, seconds_to_duration
26
25
 
27
26
  from .exceptions import Error as BaseError
28
27
  from .exceptions import InvalidRequestError, ResponseError, TimedOutError, UnsetStatusError
29
28
  from .frame_helpers import BODY_FRAME_NAME, ODOM_FRAME_NAME, get_se2_a_tform_b
30
29
  from .lease import add_lease_wallet_processors
31
- from .math_helpers import SE2Pose, SE3Pose, SE3Velocity
30
+ from .math_helpers import SE2Pose, SE3Pose
32
31
 
33
32
  # The angles (in radians) that represent the claw gripper open and closed positions.
34
33
  _CLAW_GRIPPER_OPEN_ANGLE = -1.5708
@@ -372,11 +371,12 @@ class RobotCommandClient(BaseClient):
372
371
  return self.call_async(self._stub.RobotCommand, req, _robot_command_value,
373
372
  _robot_command_error, copy_request=False, **kwargs)
374
373
 
375
- def robot_command_feedback(self, robot_command_id, **kwargs):
374
+ def robot_command_feedback(self, robot_command_id=None, **kwargs):
376
375
  """Get feedback from a previously issued command.
377
376
 
378
377
  Args:
379
- robot_command_id: ID of the robot command to get feedback on.
378
+ robot_command_id: ID of the robot command to get feedback on. If blank, will return feedback for the
379
+ current active command if a command is in progress.
380
380
 
381
381
  Raises:
382
382
  RpcError: Problem communicating with the robot.
@@ -386,7 +386,7 @@ class RobotCommandClient(BaseClient):
386
386
  return self.call(self._stub.RobotCommandFeedback, req, None, _robot_command_feedback_error,
387
387
  copy_request=False, **kwargs)
388
388
 
389
- def robot_command_feedback_async(self, robot_command_id, **kwargs):
389
+ def robot_command_feedback_async(self, robot_command_id=None, **kwargs):
390
390
  """Async version of robot_command_feedback().
391
391
 
392
392
  Args:
@@ -1928,18 +1928,18 @@ def blocking_command(command_client, command, check_status_fn, end_time_secs=Non
1928
1928
  basic_command_pb2.RobotCommandFeedbackStatus.Status.Name(feedback_status)),
1929
1929
  response)
1930
1930
 
1931
- start_time = time.time()
1931
+ start_time = now_sec()
1932
1932
  end_time = start_time + timeout_sec
1933
1933
  update_time = 1.0 / update_frequency
1934
1934
 
1935
1935
  command_id = command_client.robot_command(command, timeout=timeout_sec,
1936
1936
  end_time_secs=end_time_secs)
1937
1937
 
1938
- now = time.time()
1938
+ now = now_sec()
1939
1939
  while now < end_time:
1940
1940
  time_until_timeout = end_time - now
1941
1941
  rpc_timeout = max(time_until_timeout, 1)
1942
- start_call_time = time.time()
1942
+ start_call_time = now_sec()
1943
1943
  try:
1944
1944
  response = command_client.robot_command_feedback(command_id, timeout=rpc_timeout)
1945
1945
  except TimedOutError:
@@ -1977,9 +1977,9 @@ def blocking_command(command_client, command, check_status_fn, end_time_secs=Non
1977
1977
  if check_status_fn(response):
1978
1978
  return
1979
1979
 
1980
- delta_t = time.time() - start_call_time
1980
+ delta_t = now_sec() - start_call_time
1981
1981
  time.sleep(max(min(delta_t, update_time), 0.0))
1982
- now = time.time()
1982
+ now = now_sec()
1983
1983
 
1984
1984
  raise CommandTimedOutError(
1985
1985
  "Took longer than {:.1f} seconds to execute the command.".format(now - start_time))
@@ -2079,9 +2079,9 @@ def block_until_arm_arrives(command_client, cmd_id, timeout_sec=None):
2079
2079
  arm_command.proto for more information about why a trajectory would succeed or fail.
2080
2080
  """
2081
2081
  if timeout_sec is not None:
2082
- start_time = time.time()
2082
+ start_time = now_sec()
2083
2083
  end_time = start_time + timeout_sec
2084
- now = time.time()
2084
+ now = now_sec()
2085
2085
 
2086
2086
  while timeout_sec is None or now < end_time:
2087
2087
  feedback_resp = command_client.robot_command_feedback(cmd_id)
@@ -2114,7 +2114,7 @@ def block_until_arm_arrives(command_client, cmd_id, timeout_sec=None):
2114
2114
  return False
2115
2115
 
2116
2116
  time.sleep(0.1)
2117
- now = time.time()
2117
+ now = now_sec()
2118
2118
  return False
2119
2119
 
2120
2120
 
@@ -2145,9 +2145,9 @@ def block_for_trajectory_cmd(
2145
2145
  """
2146
2146
 
2147
2147
  if timeout_sec is not None:
2148
- start_time = time.time()
2148
+ start_time = now_sec()
2149
2149
  end_time = start_time + timeout_sec
2150
- now = time.time()
2150
+ now = now_sec()
2151
2151
 
2152
2152
  while timeout_sec is None or now < end_time:
2153
2153
  feedback_resp = command_client.robot_command_feedback(cmd_id)
@@ -2173,7 +2173,7 @@ def block_for_trajectory_cmd(
2173
2173
  return True
2174
2174
 
2175
2175
  time.sleep(feedback_interval_secs)
2176
- now = time.time()
2176
+ now = now_sec()
2177
2177
 
2178
2178
  if logger is not None:
2179
2179
  logger.info('block_for_trajectory_cmd: timeout exceeded.')
@@ -131,7 +131,7 @@ class RobotStateClient(BaseClient):
131
131
 
132
132
  class RobotStateStreamingClient(BaseClient):
133
133
  """Client for the RobotState service.
134
-
134
+
135
135
  This client is in BETA and may undergo changes in future releases.
136
136
  """
137
137
  default_service_name = 'robot-state-streaming'
bosdyn/client/sdk.py CHANGED
@@ -14,6 +14,7 @@ import os
14
14
  import platform
15
15
  from enum import Enum
16
16
 
17
+ import __main__
17
18
  import jwt
18
19
  from deprecated.sphinx import deprecated
19
20
 
@@ -87,10 +88,8 @@ BOSDYN_RESOURCE_ROOT = os.environ.get('BOSDYN_RESOURCE_ROOT',
87
88
 
88
89
  def generate_client_name(prefix=''):
89
90
  """Returns a descriptive client name for API clients with an optional prefix."""
90
- import bosdyn.client.__main__
91
91
  try:
92
- process_info = '{}-{}'.format(os.path.basename(bosdyn.client.__main__.__file__),
93
- os.getpid())
92
+ process_info = '{}-{}'.format(os.path.basename(__main__.__file__), os.getpid())
94
93
  except AttributeError:
95
94
  process_info = '{}'.format(os.getpid())
96
95
  machine_name = platform.node()
@@ -149,7 +149,7 @@ def oneof_param_to_dict(oneof_param: OneOfParam, oneof_spec: OneOfParam.Spec,
149
149
 
150
150
 
151
151
  def check_types_match(param, proto_type):
152
- if type(param) != proto_type:
152
+ if type(param) is not proto_type:
153
153
  return CustomParamError(
154
154
  status=CustomParamError.STATUS_INVALID_VALUE,
155
155
  error_messages=[
@@ -12,9 +12,8 @@ _LOGGER = logging.getLogger(__name__)
12
12
 
13
13
  from google.protobuf.wrappers_pb2 import FloatValue
14
14
 
15
- from bosdyn.api import data_chunk_pb2
16
15
  from bosdyn.api.spot_cam import audio_pb2, service_pb2_grpc
17
- from bosdyn.client.common import BaseClient, common_header_errors, handle_common_header_errors
16
+ from bosdyn.client.common import BaseClient, handle_common_header_errors
18
17
 
19
18
 
20
19
  class AudioClient(BaseClient):
@@ -11,7 +11,7 @@ import logging
11
11
  _LOGGER = logging.getLogger(__name__)
12
12
 
13
13
  from bosdyn.api.spot_cam import health_pb2, service_pb2_grpc
14
- from bosdyn.client.common import BaseClient, common_header_errors, handle_common_header_errors
14
+ from bosdyn.client.common import BaseClient, handle_common_header_errors
15
15
 
16
16
 
17
17
  class HealthClient(BaseClient):
@@ -11,7 +11,7 @@ import logging
11
11
  _LOGGER = logging.getLogger(__name__)
12
12
 
13
13
  from bosdyn.api.spot_cam import LED_pb2, service_pb2_grpc
14
- from bosdyn.client.common import BaseClient, common_header_errors, handle_common_header_errors
14
+ from bosdyn.client.common import BaseClient, handle_common_header_errors
15
15
 
16
16
 
17
17
  class LightingClient(BaseClient):
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
13
13
  from deprecated.sphinx import deprecated
14
14
 
15
15
  from bosdyn.api.spot_cam import logging_pb2, service_pb2_grpc
16
- from bosdyn.client.common import BaseClient, common_header_errors, handle_common_header_errors
16
+ from bosdyn.client.common import BaseClient, handle_common_header_errors
17
17
 
18
18
 
19
19
  class MediaLogClient(BaseClient):
@@ -27,7 +27,7 @@ class NetworkClient(BaseClient):
27
27
  super(NetworkClient, self).__init__(service_pb2_grpc.NetworkServiceStub)
28
28
 
29
29
  def get_ice_configuration(self, **kwargs):
30
- """Set ICE configuration on Spot CAM. This overrides all existing configured servers"""
30
+ """Get ICE configuration from Spot CAM"""
31
31
  request = network_pb2.GetICEConfigurationRequest()
32
32
  return self.call(self._stub.GetICEConfiguration, request, self._ice_servers_from_response,
33
33
  self._ice_network_error_from_response, copy_request=False, **kwargs)
@@ -41,7 +41,7 @@ class NetworkClient(BaseClient):
41
41
 
42
42
 
43
43
  def set_ice_configuration(self, ice_servers, **kwargs):
44
- """Get ICE configuration from Spot CAM"""
44
+ """Set ICE configuration on Spot CAM. This overrides all existing configured servers"""
45
45
  request = self._set_ice_configuration_request(ice_servers)
46
46
  return self.call(self._stub.SetICEConfiguration, request, None,
47
47
  self._ice_network_error_from_response, copy_request=False, **kwargs)
@@ -67,3 +67,4 @@ class NetworkClient(BaseClient):
67
67
  @handle_common_header_errors
68
68
  def _ice_network_error_from_response(response): # pylint: disable=unused-argument
69
69
  return None
70
+
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
13
13
  from google.protobuf.wrappers_pb2 import BoolValue
14
14
 
15
15
  from bosdyn.api.spot_cam import power_pb2, service_pb2_grpc
16
- from bosdyn.client.common import BaseClient, common_header_errors, handle_common_header_errors
16
+ from bosdyn.client.common import BaseClient, handle_common_header_errors
17
17
 
18
18
 
19
19
  class PowerClient(BaseClient):
@@ -13,7 +13,7 @@ _LOGGER = logging.getLogger(__name__)
13
13
  from google.protobuf.wrappers_pb2 import FloatValue, Int32Value
14
14
 
15
15
  from bosdyn.api.spot_cam import ptz_pb2, service_pb2_grpc
16
- from bosdyn.client.common import BaseClient, common_header_errors, handle_common_header_errors
16
+ from bosdyn.client.common import BaseClient, common_header_errors
17
17
  from bosdyn.client.math_helpers import recenter_value_mod
18
18
 
19
19
 
@@ -11,7 +11,7 @@ import logging
11
11
  _LOGGER = logging.getLogger(__name__)
12
12
 
13
13
  from bosdyn.api.spot_cam import service_pb2_grpc, streamquality_pb2
14
- from bosdyn.client.common import BaseClient, common_header_errors, handle_common_header_errors
14
+ from bosdyn.client.common import BaseClient, handle_common_header_errors
15
15
 
16
16
 
17
17
  class StreamQualityClient(BaseClient):
@@ -11,7 +11,7 @@ import logging
11
11
  _LOGGER = logging.getLogger(__name__)
12
12
 
13
13
  from bosdyn.api.spot_cam import service_pb2_grpc, version_pb2
14
- from bosdyn.client.common import BaseClient, common_header_errors, handle_common_header_errors
14
+ from bosdyn.client.common import BaseClient, handle_common_header_errors
15
15
 
16
16
 
17
17
  class VersionClient(BaseClient):
@@ -7,12 +7,11 @@
7
7
  import collections
8
8
  import time
9
9
 
10
- from urllib3 import Timeout
11
-
12
10
  from bosdyn.api.spot import spot_check_pb2, spot_check_service_pb2_grpc
13
11
  from bosdyn.client.common import (BaseClient, error_factory, handle_common_header_errors,
14
12
  handle_lease_use_result_errors, handle_unset_status_error)
15
13
  from bosdyn.client.exceptions import LeaseUseError, ResponseError, TimedOutError
14
+ from bosdyn.util import now_sec
16
15
 
17
16
 
18
17
  class SpotCheckError(ResponseError):
@@ -191,7 +190,7 @@ def run_spot_check(spot_check_client, lease, timeout_sec=212, update_frequency=0
191
190
  Raises:
192
191
  bosdyn.client.exceptions.Error: Throws on any error failure.
193
192
  """
194
- start_time = time.time()
193
+ start_time = now_sec()
195
194
  end_time = start_time + timeout_sec
196
195
  update_time = 1.0 / update_frequency
197
196
  # Start spot check procedure.
@@ -201,7 +200,7 @@ def run_spot_check(spot_check_client, lease, timeout_sec=212, update_frequency=0
201
200
  spot_check_client.spot_check_command(req)
202
201
  # Check spot check feedback.
203
202
  feedback_req = spot_check_pb2.SpotCheckFeedbackRequest()
204
- while (time.time() < end_time):
203
+ while (now_sec() < end_time):
205
204
  time.sleep(update_time)
206
205
  res = spot_check_client.spot_check_feedback(feedback_req)
207
206
  if (res.state == spot_check_pb2.SpotCheckFeedbackResponse.STATE_WAITING_FOR_COMMAND or
@@ -234,7 +233,7 @@ def run_camera_calibration(spot_check_client, lease, timeout_sec=1200, update_fr
234
233
  Raises:
235
234
  bosdyn.client.exceptions.Error: Throws on any calibration failure.
236
235
  """
237
- start_time = time.time()
236
+ start_time = now_sec()
238
237
  end_time = start_time + timeout_sec
239
238
  update_time = 1.0 / update_frequency
240
239
  # Start camera calibration procedure.
@@ -244,7 +243,7 @@ def run_camera_calibration(spot_check_client, lease, timeout_sec=1200, update_fr
244
243
  spot_check_client.camera_calibration_command(req)
245
244
  # Check camera calibration feedback.
246
245
  feedback_req = spot_check_pb2.CameraCalibrationFeedbackRequest()
247
- while (time.time() < end_time):
246
+ while (now_sec() < end_time):
248
247
  time.sleep(update_time)
249
248
  res = spot_check_client.camera_calibration_feedback(feedback_req)
250
249
  if (res.status == spot_check_pb2.CameraCalibrationFeedbackResponse.STATUS_SUCCESS):
@@ -0,0 +1,220 @@
1
+ # Copyright (c) 2023 Boston Dynamics, Inc. All rights reserved.
2
+ #
3
+ # Downloading, reproducing, distributing or otherwise using the SDK Software
4
+ # is subject to the terms and conditions of the Boston Dynamics Software
5
+ # Development Kit License (20191101-BDSDK-SL).
6
+
7
+
8
+ import enum
9
+ import ipaddress
10
+ import logging
11
+ import socket
12
+ from contextlib import contextmanager
13
+ from pathlib import Path
14
+ from urllib.parse import urlparse, urlunparse
15
+
16
+ import requests
17
+
18
+ MAX_REDIRECTS = 3
19
+ SO_BINDTODEVICE = getattr(socket, "SO_BINDTODEVICE", 25)
20
+
21
+ _LOGGER = logging.getLogger(__name__)
22
+
23
+
24
+
25
+ class InterfaceNameNotFound(Exception):
26
+ """Raised when a specified network interface is not present on the
27
+ system."""
28
+
29
+ def __init__(self, name: str):
30
+ super().__init__(f"Interface '{name}' is not found on system.")
31
+
32
+
33
+
34
+
35
+ class BindAdapter(requests.adapters.HTTPAdapter):
36
+ """Allows binding to a specific network interface and enforcing a custom
37
+ Host header for HTTP."""
38
+
39
+ def __init__(self, is_robot=True, interface=None, resolved_ip=None, assert_hostname=None,
40
+ force_host=None, *args, **kwargs):
41
+ if not is_robot:
42
+ self.interface = interface
43
+ self.resolved_ip = resolved_ip
44
+ self.assert_hostname = assert_hostname
45
+ self.force_host = force_host
46
+ super().__init__(*args, **kwargs)
47
+
48
+ def send(self, request, **kwargs):
49
+ """Override the send method to enforce a custom Host header for
50
+ HTTP."""
51
+ if self.force_host:
52
+ request.headers["Host"] = self.force_host
53
+ return super().send(request, **kwargs)
54
+
55
+ def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
56
+ host_params, pool_kwargs = super().build_connection_pool_key_attributes(
57
+ request, verify, cert)
58
+
59
+ if self.resolved_ip:
60
+ host_params["host"] = self.resolved_ip
61
+ else:
62
+ raise ValueError("BindAdapter requires a resolved IP address to connect to.")
63
+
64
+ if host_params.get("scheme") == "https":
65
+ vanity = self.assert_hostname or (urlparse(request.url).hostname) or self.force_host
66
+ if vanity:
67
+ pool_kwargs["server_hostname"] = vanity # SNI
68
+ pool_kwargs["assert_hostname"] = vanity # cert hostname check
69
+
70
+ if self.interface:
71
+ so = pool_kwargs.get("socket_options") or []
72
+ so.append((socket.SOL_SOCKET, SO_BINDTODEVICE, (self.interface + "\0").encode("utf-8")))
73
+ pool_kwargs["socket_options"] = so
74
+
75
+ conn = self.poolmanager.connection_from_host(
76
+ host=host_params["host"],
77
+ port=host_params.get("port"),
78
+ scheme=host_params.get("scheme", "https"),
79
+ pool_kwargs=pool_kwargs,
80
+ )
81
+
82
+ return conn
83
+
84
+
85
+ @contextmanager
86
+ def create_bound_session(is_robot=True, interface=None, resolved_ip=None, sni_hostname=None,
87
+ force_host=None):
88
+ """Creates a session bound to optional interface with optional SNI
89
+ hostname."""
90
+ session = requests.Session()
91
+ adapter = BindAdapter(is_robot, interface=interface, resolved_ip=resolved_ip,
92
+ assert_hostname=sni_hostname, force_host=force_host)
93
+ session.mount("http://", adapter)
94
+ session.mount("https://", adapter)
95
+
96
+ try:
97
+ yield session
98
+ finally:
99
+ session.close()
100
+
101
+
102
+
103
+
104
+ def validate_url(url):
105
+ """Checks if any IP address resolved from a URL is in the blacklist. First
106
+ checks if the hostname is already a valid IP address.
107
+
108
+ Args:
109
+ url: The URL to check.
110
+
111
+ Returns:
112
+ A Tuple, where the first value is whether or not the given url was valid.
113
+ If True, the second value is a dict containing the url and hostname,
114
+ if False, the second value is an error statement of what went wrong.
115
+ """
116
+ try:
117
+ # The URL here could be a vanity name or IP address (IPv4 or IPv6), with or without a port, e.g., example.com, example.com:1234, 1.1.1.1, 1.1.1.1:1234, [::ffff:101:101], or [::ffff:101:101]:1234.
118
+ parsed_url = urlparse(url)
119
+ ret = {"parsed_url": parsed_url}
120
+
121
+ try:
122
+ # This try block is for the case where the host is an explicit IP address, e.g., 1.1.1.1 or [::ffff:101:101].
123
+ ip_address = ipaddress.ip_address(parsed_url.hostname)
124
+ # NOTE: this isn't actually a resolved IP, it's just the IP we were given.
125
+ ret["resolved_ip"] = str(ip_address)
126
+ return (True, ret)
127
+ except ValueError:
128
+ pass
129
+
130
+ try:
131
+ # This try block is for the case where the host is a vanity name, e.g., example.com. Note that this can resolve to either an IPv4 or IPv6 address.
132
+ ip_address = socket.getaddrinfo(parsed_url.hostname, port=parsed_url.port)[0][4][0]
133
+ ret["resolved_ip"] = str(ip_address)
134
+ return (True, ret)
135
+ except Exception as e:
136
+ status = f"No IP addresses resolved for URL: {url}"
137
+ _LOGGER.error(f"validate_url exception: {e}\nstatus: {status}")
138
+ return (False, status)
139
+ except ValueError:
140
+ return (False, f"Invalid URL format: {url}")
141
+
142
+
143
+ def safe_api_call(method, url, sni_hostname, timeout, is_robot=True, interface=None,
144
+ **request_data):
145
+ """Make an API call to a URL, validating the URL and checking for
146
+ redirects. Will attempt to bind the provided network interface.
147
+
148
+ Args:
149
+ method (str): method for HTTP request to use
150
+ url (str): URL to make the request to
151
+ sni_hostname (str): Hostname to assert for the Request
152
+ timeout (float): Timeout for the request
153
+ interface (str, optional): Network interface to bind all HTTP calls to, use ("WIFI", "LTE", "ETHERNET"). Will override default interface if provided, currently set to WIFI = "wlp5s0".
154
+
155
+ Returns:
156
+ Tuple[Response|None, str]: A tuple containing the response object (or None) and a status message.
157
+ """
158
+ num_redirects = 0
159
+ url_to_check = url
160
+ status = ""
161
+
162
+ # Allow only 3 redirects per API call
163
+ while num_redirects < MAX_REDIRECTS:
164
+ (url_valid, return_value) = validate_url(url_to_check)
165
+ if url_valid:
166
+ parsed_url = return_value["parsed_url"]
167
+ resolved_ip = return_value["resolved_ip"]
168
+ status = f"Validation of {url} successful with resolved_ip: {resolved_ip}"
169
+ try:
170
+ with create_bound_session(is_robot, interface, resolved_ip, sni_hostname,
171
+ sni_hostname) as session:
172
+ # Potential argument injection through user-controlled keys and values in request_data.
173
+ # This is made secure by the webserver's JSON schema allowing only specifically named fields.
174
+ # Disable automatic redirects, so we can track the new hostname before the call is made.
175
+ response = session.request(method, urlunparse(parsed_url), timeout=timeout,
176
+ allow_redirects=False, **request_data)
177
+
178
+ if 300 <= response.status_code < 400:
179
+ redirect_location = response.headers.get("Location")
180
+ url_to_check = redirect_location
181
+ num_redirects += 1
182
+ continue
183
+ else:
184
+ return response, status
185
+ except requests.exceptions.RequestException as e:
186
+ if isinstance(e, requests.exceptions.SSLError):
187
+ status = "SSL error occurred. Please upload server SSL certificate to robot."
188
+ elif isinstance(e, requests.exceptions.ConnectTimeout):
189
+ status = "Connection to server timed out. Check firewall, network, route, server, etc."
190
+ elif isinstance(e, requests.exceptions.ReadTimeout):
191
+ status = "Connected to server, but server did not respond in time. Check server logs."
192
+ elif isinstance(e, requests.exceptions.URLRequired):
193
+ status = "URL is required for request."
194
+ elif isinstance(e, requests.exceptions.TooManyRedirects):
195
+ status = "Too many redirects when accessing server."
196
+ elif isinstance(e, requests.exceptions.MissingSchema):
197
+ status = "URL is missing schema (http or https)."
198
+ elif isinstance(e, requests.exceptions.InvalidSchema):
199
+ status = "URL has invalid schema (http and https are supported)."
200
+ elif isinstance(e, requests.exceptions.InvalidURL):
201
+ status = "Invalid URL for request."
202
+ elif isinstance(e, requests.exceptions.InvalidHeader):
203
+ status = "Invalid header(s) in request."
204
+ # This catches all other RequestException types, which are not expected to occur. But, if they do, telling the user what type of exception occurred may help them resolve the problem on their own.
205
+ else:
206
+ status = f"Unknown RequestException of type {e.__class__.__name__} occurred."
207
+ _LOGGER.error(f"safe_api_call exception: {e}\nstatus: {status}")
208
+ except InterfaceNameNotFound as e:
209
+ status = "Check route in config file. Only WIFI, LTE, and ETHERNET are supported."
210
+ _LOGGER.error(f"safe_api_call exception: {e}\nstatus: {status}")
211
+ except Exception as e:
212
+ status = (f"Unknown exception of type {e.__class__.__name__} occurred.")
213
+ _LOGGER.error(f"safe_api_call exception: {e}\nstatus: {status}")
214
+ else:
215
+ status = f"{return_value}"
216
+ return None, status
217
+
218
+ # Don't expect to get here, but if it does there was a problem
219
+ status = f"Max redirects reached on url {url}"
220
+ return None, status
bosdyn/client/util.py CHANGED
@@ -16,7 +16,6 @@ from concurrent import futures
16
16
  from secrets import token_urlsafe
17
17
  from uuid import uuid4
18
18
 
19
- import google.protobuf.descriptor
20
19
  import grpc
21
20
  from deprecated.sphinx import deprecated
22
21
 
@@ -185,9 +184,8 @@ def does_dedup_filter_exist(logger, always_print_logger_levels):
185
184
  Boolean indicating if the DedupLoggingMessages filter already exists and matches the new parameters.
186
185
  """
187
186
  for filt in logger.filters:
188
- if type(
189
- filt
190
- ) == DedupLoggingMessages and filt.always_print_logger_levels == always_print_logger_levels:
187
+ if (type(filt) is DedupLoggingMessages and
188
+ filt.always_print_logger_levels == always_print_logger_levels):
191
189
  return True
192
190
  return False
193
191
 
@@ -7,7 +7,7 @@
7
7
  """For clients to use the world object service"""
8
8
 
9
9
  from bosdyn.api import geometry_pb2 as geom
10
- from bosdyn.api import world_object_pb2, world_object_service_pb2
10
+ from bosdyn.api import world_object_pb2
11
11
  from bosdyn.api import world_object_service_pb2_grpc as world_object_service
12
12
  from bosdyn.client.common import BaseClient, common_header_errors
13
13
  from bosdyn.client.frame_helpers import *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: bosdyn-client
3
- Version: 5.0.1.2
3
+ Version: 5.1.1
4
4
  Summary: Boston Dynamics API client code and interfaces
5
5
  Home-page: https://dev.bostondynamics.com/
6
6
  Author: Boston Dynamics
@@ -15,8 +15,8 @@ Classifier: License :: Other/Proprietary License
15
15
  Classifier: Operating System :: OS Independent
16
16
  Requires-Python: >=3.7
17
17
  Description-Content-Type: text/markdown
18
- Requires-Dist: bosdyn-api (==5.0.1.2)
19
- Requires-Dist: bosdyn-core (==5.0.1.2)
18
+ Requires-Dist: bosdyn-api (==5.1.1)
19
+ Requires-Dist: bosdyn-core (==5.1.1)
20
20
  Requires-Dist: grpcio
21
21
  Requires-Dist: pyjwt
22
22
  Requires-Dist: numpy