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.
- bosdyn/client/access_controlled_door_util.py +206 -0
- bosdyn/client/arm_surface_contact.py +2 -2
- bosdyn/client/async_tasks.py +3 -2
- bosdyn/client/audio_visual_helpers.py +3 -2
- bosdyn/client/autowalk.py +0 -2
- bosdyn/client/command_line.py +72 -15
- bosdyn/client/common.py +1 -1
- bosdyn/client/data_acquisition.py +3 -5
- bosdyn/client/data_acquisition_helpers.py +0 -3
- bosdyn/client/data_acquisition_plugin.py +1 -2
- bosdyn/client/data_acquisition_plugin_service.py +3 -2
- bosdyn/client/data_acquisition_store.py +1 -7
- bosdyn/client/data_buffer.py +5 -4
- bosdyn/client/directory_registration.py +3 -2
- bosdyn/client/estop.py +3 -2
- bosdyn/client/fault.py +1 -1
- bosdyn/client/gps/aggregator_client.py +2 -4
- bosdyn/client/gps/gps_listener.py +5 -7
- bosdyn/client/gps/ntrip_client.py +12 -3
- bosdyn/client/graph_nav.py +67 -13
- bosdyn/client/hazard_avoidance.py +119 -0
- bosdyn/client/image.py +5 -4
- bosdyn/client/image_service_helpers.py +6 -7
- bosdyn/client/ir_enable_disable.py +1 -1
- bosdyn/client/keepalive.py +4 -2
- bosdyn/client/lease.py +3 -2
- bosdyn/client/lease_validator.py +0 -1
- bosdyn/client/log_status.py +57 -3
- bosdyn/client/map_processing.py +2 -4
- bosdyn/client/network_compute_bridge_client.py +4 -6
- bosdyn/client/payload.py +2 -3
- bosdyn/client/payload_registration.py +11 -10
- bosdyn/client/power.py +84 -27
- bosdyn/client/processors.py +27 -2
- bosdyn/client/recording.py +3 -3
- bosdyn/client/robot_command.py +22 -22
- bosdyn/client/robot_state.py +1 -1
- bosdyn/client/sdk.py +2 -3
- bosdyn/client/service_customization_helpers.py +1 -1
- bosdyn/client/spot_cam/audio.py +1 -2
- bosdyn/client/spot_cam/health.py +1 -1
- bosdyn/client/spot_cam/lighting.py +1 -1
- bosdyn/client/spot_cam/media_log.py +1 -1
- bosdyn/client/spot_cam/network.py +3 -2
- bosdyn/client/spot_cam/power.py +1 -1
- bosdyn/client/spot_cam/ptz.py +1 -1
- bosdyn/client/spot_cam/streamquality.py +1 -1
- bosdyn/client/spot_cam/version.py +1 -1
- bosdyn/client/spot_check.py +5 -6
- bosdyn/client/url_validation_util.py +220 -0
- bosdyn/client/util.py +2 -4
- bosdyn/client/world_object.py +1 -1
- {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/METADATA +3 -3
- bosdyn_client-5.1.1.dist-info/RECORD +106 -0
- bosdyn_client-5.0.1.2.dist-info/RECORD +0 -103
- {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/WHEEL +0 -0
- {bosdyn_client-5.0.1.2.dist-info → bosdyn_client-5.1.1.dist-info}/top_level.txt +0 -0
bosdyn/client/robot_command.py
CHANGED
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1980
|
+
delta_t = now_sec() - start_call_time
|
|
1981
1981
|
time.sleep(max(min(delta_t, update_time), 0.0))
|
|
1982
|
-
now =
|
|
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 =
|
|
2082
|
+
start_time = now_sec()
|
|
2083
2083
|
end_time = start_time + timeout_sec
|
|
2084
|
-
now =
|
|
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 =
|
|
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 =
|
|
2148
|
+
start_time = now_sec()
|
|
2149
2149
|
end_time = start_time + timeout_sec
|
|
2150
|
-
now =
|
|
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 =
|
|
2176
|
+
now = now_sec()
|
|
2177
2177
|
|
|
2178
2178
|
if logger is not None:
|
|
2179
2179
|
logger.info('block_for_trajectory_cmd: timeout exceeded.')
|
bosdyn/client/robot_state.py
CHANGED
|
@@ -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(
|
|
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)
|
|
152
|
+
if type(param) is not proto_type:
|
|
153
153
|
return CustomParamError(
|
|
154
154
|
status=CustomParamError.STATUS_INVALID_VALUE,
|
|
155
155
|
error_messages=[
|
bosdyn/client/spot_cam/audio.py
CHANGED
|
@@ -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,
|
|
16
|
+
from bosdyn.client.common import BaseClient, handle_common_header_errors
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
class AudioClient(BaseClient):
|
bosdyn/client/spot_cam/health.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
+
|
bosdyn/client/spot_cam/power.py
CHANGED
|
@@ -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,
|
|
16
|
+
from bosdyn.client.common import BaseClient, handle_common_header_errors
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class PowerClient(BaseClient):
|
bosdyn/client/spot_cam/ptz.py
CHANGED
|
@@ -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
|
|
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,
|
|
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,
|
|
14
|
+
from bosdyn.client.common import BaseClient, handle_common_header_errors
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class VersionClient(BaseClient):
|
bosdyn/client/spot_check.py
CHANGED
|
@@ -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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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
|
|
bosdyn/client/world_object.py
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
19
|
-
Requires-Dist: bosdyn-core (==5.
|
|
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
|