bosdyn-orbit 4.0.0__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/__init__.py ADDED
@@ -0,0 +1,7 @@
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
+ __path__ = __import__('pkgutil').extend_path(__path__, __name__)
@@ -0,0 +1,7 @@
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
+
bosdyn/orbit/client.py ADDED
@@ -0,0 +1,822 @@
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
+ """ The client uses a web API to send HTTPs requests to a number of REStful endpoints using the Requests library.
8
+ """
9
+ from collections.abc import Iterable
10
+
11
+ import requests
12
+
13
+ import bosdyn.orbit.utils as utils
14
+ from bosdyn.orbit.exceptions import UnauthenticatedClientError
15
+
16
+ DEFAULT_HEADERS = {'Accept': 'application/json'}
17
+ OCTET_HEADER = {'Content-type': 'application/octet-stream', 'Accept': 'application/octet-stream'}
18
+
19
+
20
+ class Client():
21
+ """Client for the Orbit web API"""
22
+
23
+ def __init__(self, hostname: str, verify: bool = True, cert: str = None):
24
+ """ Initializes the attributes of the Client object.
25
+
26
+ Args:
27
+ hostname: the IP address associated with the instance
28
+ verify(path to a CA bundle or Boolean): controls whether we verify the server’s TLS certificate
29
+ Note that verify=False makes your application vulnerable to man-in-the-middle (MitM) attacks
30
+ Defaults to True
31
+ cert(.pem file or a tuple with ('cert', 'key') pair): a local cert to use as client side certificate
32
+ Note that the private key to your local certificate must be unencrypted because Requests does not support using encrypted keys.
33
+ Defaults to None. For additional configurations, use the member Session object "_session" in accordance with Requests library
34
+ Raises:
35
+ RequestExceptions: exceptions thrown by the Requests library
36
+ """
37
+ # The hostname of the instance
38
+ self._hostname = hostname
39
+ # Set a Session object to persist certain parameters across requests
40
+ self._session = requests.Session()
41
+ # Set SSL verification strategy
42
+ self._session.verify = verify
43
+ # Client Side Certificates
44
+ self._session.cert = cert
45
+ # Initialize session
46
+ self._session.get(f'https://{self._hostname}')
47
+ # Set default headers for self._session
48
+ self._session.headers.update(DEFAULT_HEADERS)
49
+ # Set x-csrf-token for self._session
50
+ self._session.headers.update({'x-csrf-token': self._session.cookies['x-csrf-token']})
51
+ # Flag indicating that the Client is authenticated
52
+ self._is_authenticated = False
53
+
54
+ def authenticate_with_api_token(self, api_token: str = None) -> requests.Response:
55
+ """ Authorizes the client using the provided API token obtained from the instance.
56
+ Must call before using other client functions.
57
+
58
+ Args:
59
+ api_token: the API token obtained from the instance
60
+ Raises:
61
+ RequestExceptions: exceptions thrown by the Requests library
62
+ """
63
+ if not api_token:
64
+ api_token = utils.get_api_token()
65
+ # Set API token for self._session
66
+ self._session.headers.update({'Authorization': 'Bearer ' + api_token})
67
+ # Check the validity of the API token
68
+ authenticate_response = self._session.get(
69
+ f'https://{self._hostname}/api/v0/api_token/authenticate')
70
+ if authenticate_response.ok:
71
+ self._is_authenticated = True
72
+ else:
73
+ print('Client: Login failed: {} Please, obtain a valid API token from the instance!'.
74
+ format(authenticate_response.text))
75
+ # Remove the invalid API token from session headers
76
+ self._session.headers.pop('Authorization')
77
+ return authenticate_response
78
+
79
+ def get_resource(self, path: str, **kwargs) -> requests.Response:
80
+ """ Base function for getting a resource in /api/v0/
81
+
82
+ Args:
83
+ path: the path appended to /api/v0/
84
+ kwargs(**): a variable number of keyword arguments for the get request
85
+ Raises:
86
+ RequestExceptions: exceptions thrown by the Requests library
87
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
88
+ Returns:
89
+ requests.Response: the response associated with the get request
90
+ """
91
+ if not self._is_authenticated:
92
+ raise UnauthenticatedClientError()
93
+ return self._session.get(f'https://{self._hostname}/api/v0/{path}/', **kwargs)
94
+
95
+ def post_resource(self, path: str, **kwargs) -> requests.Response:
96
+ """ Base function for posting a resource in /api/v0/
97
+
98
+ Args:
99
+ path: the path appended to /api/v0/
100
+ kwargs(**): a variable number of keyword arguments for the post request
101
+ Raises:
102
+ RequestExceptions: exceptions thrown by the Requests library
103
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
104
+ Returns:
105
+ The response associated with the post request
106
+ """
107
+ if not self._is_authenticated and path != "login":
108
+ raise UnauthenticatedClientError()
109
+ return self._session.post(f'https://{self._hostname}/api/v0/{path}', **kwargs)
110
+
111
+ def delete_resource(self, path: str, **kwargs) -> requests.Response:
112
+ """ Base function for deleting a resource in /api/v0/
113
+
114
+ Args:
115
+ path: the path appended to /api/v0/
116
+ kwargs(**): a variable number of keyword arguments for the delete request
117
+ Raises:
118
+ RequestExceptions: exceptions thrown by the Requests library
119
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
120
+ Returns:
121
+ The response associated with the delete request
122
+ """
123
+ if not self._is_authenticated:
124
+ raise UnauthenticatedClientError()
125
+ return self._session.delete(f'https://{self._hostname}/api/v0/{path}', **kwargs)
126
+
127
+ def get_version(self, **kwargs) -> requests.Response:
128
+ """ Retrieves version info.
129
+
130
+ Args:
131
+ kwargs(**): a variable number of keyword arguments for the get request
132
+ Raises:
133
+ RequestExceptions: exceptions thrown by the Requests library
134
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
135
+ Returns:
136
+ requests.Response: the response associated with the get request
137
+ """
138
+ return self.get_resource("version", **kwargs)
139
+
140
+ def get_system_time(self, **kwargs) -> requests.Response:
141
+ """ Returns the current system time.
142
+
143
+ Args:
144
+ kwargs(**): a variable number of keyword arguments for the get request
145
+ Raises:
146
+ RequestExceptions: exceptions thrown by the Requests library
147
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
148
+ Returns:
149
+ requests.Response: the response associated with the get request
150
+ """
151
+ return self.get_resource("settings/system-time", **kwargs)
152
+
153
+ def get_robots(self, **kwargs) -> requests.Response:
154
+ """ Returns robots on the specified instance.
155
+
156
+ Args:
157
+ kwargs(**): a variable number of keyword arguments for the get request
158
+ Raises:
159
+ RequestExceptions: exceptions thrown by the Requests library
160
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
161
+ Returns:
162
+ requests.Response: the response associated with the get request
163
+ """
164
+ return self.get_resource("robots", **kwargs)
165
+
166
+ def get_robot_by_hostname(self, hostname: str, **kwargs) -> requests.Response:
167
+ """ Returns a robot on given a hostname of a specific robot.
168
+
169
+ Args:
170
+ hostname: the IP address associated with the desired robot on the instance
171
+ kwargs(**): a variable number of keyword arguments for the get request
172
+ Raises:
173
+ RequestExceptions: exceptions thrown by the Requests library
174
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
175
+ Returns:
176
+ requests.Response: the response associated with the get request
177
+ """
178
+ return self.get_resource(f'robots/{hostname}', **kwargs)
179
+
180
+ def get_site_walks(self, **kwargs) -> requests.Response:
181
+ """ Returns site walks on the specified instance
182
+
183
+ Args:
184
+ kwargs(**): a variable number of keyword arguments for the get request
185
+ Raises:
186
+ RequestExceptions: exceptions thrown by the Requests library
187
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
188
+ Returns:
189
+ requests.Response: the response associated with the get request
190
+ """
191
+ return self.get_resource("site_walks", **kwargs)
192
+
193
+ def get_site_walk_by_id(self, uuid: str, **kwargs) -> requests.Response:
194
+ """ Given a site walk uuid, returns a site walk on the specified instance
195
+
196
+ Args:
197
+ uuid: the ID associated with the site walk
198
+ kwargs(**): a variable number of keyword arguments for the get request
199
+ Raises:
200
+ RequestExceptions: exceptions thrown by the Requests library
201
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
202
+ Returns:
203
+ requests.Response: the response associated with the get request
204
+ """
205
+ return self.get_resource(f'site_walks/{uuid}', **kwargs)
206
+
207
+ def get_site_elements(self, **kwargs) -> requests.Response:
208
+ """ Returns site elements on the specified instance
209
+
210
+ Args:
211
+ kwargs(**): a variable number of keyword arguments for the get request
212
+ Raises:
213
+ RequestExceptions: exceptions thrown by the Requests library
214
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
215
+ Returns:
216
+ requests.Response: the response associated with the get request
217
+ """
218
+ return self.get_resource("site_elements", **kwargs)
219
+
220
+ def get_site_element_by_id(self, uuid: str, **kwargs) -> requests.Response:
221
+ """ Given a site element uuid, returns a site element on the specified instance
222
+
223
+ Args:
224
+ uuid: the ID associated with the site element
225
+ kwargs(**): a variable number of keyword arguments for the get request
226
+ Raises:
227
+ RequestExceptions: exceptions thrown by the Requests library
228
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
229
+ Returns:
230
+ requests.Response: the response associated with the get request
231
+ """
232
+ return self.get_resource(f'site_elements/{uuid}', **kwargs)
233
+
234
+ def get_site_docks(self, **kwargs) -> requests.Response:
235
+ """ Returns site docks on the specified instance
236
+
237
+ Args:
238
+ kwargs(**): a variable number of keyword arguments for the get request
239
+ Raises:
240
+ RequestExceptions: exceptions thrown by the Requests library
241
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
242
+ Returns:
243
+ requests.Response: the response associated with the get request
244
+ """
245
+ return self.get_resource("site_docks", **kwargs)
246
+
247
+ def get_site_dock_by_id(self, uuid: str, **kwargs) -> requests.Response:
248
+ """ Given a site dock uuid, returns a site dock on the specified instance
249
+
250
+ Args:
251
+ uuid: the ID associated with the site dock
252
+ kwargs(**): a variable number of keyword arguments for the get request
253
+ Raises:
254
+ RequestExceptions: exceptions thrown by the Requests library
255
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
256
+ Returns:
257
+ requests.Response: the response associated with the get request
258
+ """
259
+ return self.get_resource(f'site_docks/{uuid}', **kwargs)
260
+
261
+ def get_calendar(self, **kwargs) -> requests.Response:
262
+ """ Returns calendar events on the specified instance
263
+
264
+ Args:
265
+ kwargs(**): a variable number of keyword arguments for the get request
266
+ Raises:
267
+ RequestExceptions: exceptions thrown by the Requests library
268
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
269
+ Returns:
270
+ requests.Response: the response associated with the get request
271
+ """
272
+ return self.get_resource("calendar/schedule", **kwargs)
273
+
274
+ def get_run_events(self, **kwargs) -> requests.Response:
275
+ """ Given a dictionary of query params, returns run events
276
+
277
+ Args:
278
+ kwargs(**): a variable number of keyword arguments for the get request
279
+ Raises:
280
+ RequestExceptions: exceptions thrown by the Requests library
281
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
282
+ Returns:
283
+ requests.Response: the response associated with the get request
284
+ """
285
+ return self.get_resource("run_events", **kwargs)
286
+
287
+ def get_run_event_by_id(self, uuid: str, **kwargs) -> requests.Response:
288
+ """ Given a runEventUuid, returns a run event
289
+
290
+ Args:
291
+ uuid: the ID associated with the run event
292
+ kwargs(**): a variable number of keyword arguments for the get request
293
+ Raises:
294
+ RequestExceptions: exceptions thrown by the Requests library
295
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
296
+ Returns:
297
+ requests.Response: the response associated with the get request
298
+ """
299
+ return self.get_resource(f'run_events/{uuid}', **kwargs)
300
+
301
+ def get_run_captures(self, **kwargs) -> requests.Response:
302
+ """ Given a dictionary of query params, returns run captures
303
+
304
+ Args:
305
+ kwargs(**): a variable number of keyword arguments for the get request
306
+ Raises:
307
+ RequestExceptions: exceptions thrown by the Requests library
308
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
309
+ Returns:
310
+ requests.Response: the response associated with the get request
311
+ """
312
+ return self.get_resource("run_captures", **kwargs)
313
+
314
+ def get_run_capture_by_id(self, uuid: str, **kwargs) -> requests.Response:
315
+ """ Given a runCaptureUuid, returns a run capture
316
+
317
+ Args:
318
+ uuid: the ID associated with the run capture
319
+ kwargs(**): a variable number of keyword arguments for the get request
320
+ Raises:
321
+ RequestExceptions: exceptions thrown by the Requests library
322
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
323
+ Returns:
324
+ requests.Response: the response associated with the get request
325
+ """
326
+ return self.get_resource(f'run_captures/{uuid}', **kwargs)
327
+
328
+ def get_runs(self, **kwargs) -> requests.Response:
329
+ """ Given a dictionary of query params, returns runs
330
+
331
+ Args:
332
+ kwargs(**): a variable number of keyword arguments for the get request
333
+ Raises:
334
+ RequestExceptions: exceptions thrown by the Requests library
335
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
336
+ Returns:
337
+ requests.Response: the response associated with the get request
338
+ """
339
+ return self.get_resource("runs", **kwargs)
340
+
341
+ def get_run_by_id(self, uuid: str, **kwargs) -> requests.Response:
342
+ """ Given a runUuid, returns a run
343
+
344
+ Args:
345
+ uuid: the ID associated with the run
346
+ kwargs(**): a variable number of keyword arguments for the get request
347
+ Raises:
348
+ RequestExceptions: exceptions thrown by the Requests library
349
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
350
+ Returns:
351
+ requests.Response: the response associated with the get request
352
+ """
353
+ return self.get_resource(f'runs/{uuid}', **kwargs)
354
+
355
+ def get_run_archives_by_id(self, uuid: str, **kwargs) -> requests.Response:
356
+ """ Given a runUuid, returns run archives
357
+
358
+ Args:
359
+ uuid: the ID associated with the run
360
+ kwargs(**): a variable number of keyword arguments for the get request
361
+ Raises:
362
+ RequestExceptions: exceptions thrown by the Requests library
363
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
364
+ Returns:
365
+ requests.Response: the response associated with the get request
366
+ """
367
+ return self.get_resource(f'run_archives/{uuid}', **kwargs)
368
+
369
+ def get_image(self, url: str, **kwargs) -> 'urllib3.response.HTTPResponse':
370
+ """ Given a data capture url, returns a decoded image
371
+
372
+ Args:
373
+ url: the url associated with the data capture in the form of https://hostname + RunCapture["dataUrl"].
374
+ kwargs(**): a variable number of keyword arguments for the get request
375
+ Raises:
376
+ RequestExceptions: exceptions thrown by the Requests library
377
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
378
+ Returns:
379
+ urllib3.response.HTTPResponse: the decoded response associated with the get request
380
+ """
381
+ if not self._is_authenticated:
382
+ raise UnauthenticatedClientError()
383
+ response = self._session.get(url, stream=True, **kwargs)
384
+ response.raise_for_status()
385
+ response.raw.decode_content = True
386
+ return response.raw
387
+
388
+ def get_image_response(self, url: str, **kwargs) -> requests.Response:
389
+ """ Given a data capture url, returns an image response
390
+
391
+ Args:
392
+ url: the url associated with the data capture in the form of https://hostname + RunCapture["dataUrl"]
393
+ kwargs(**): a variable number of keyword arguments for the get request
394
+ Raises:
395
+ RequestExceptions: exceptions thrown by the Requests library
396
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
397
+ Returns:
398
+ requests.Response: the image response associated with the get request
399
+ """
400
+ if not self._is_authenticated:
401
+ raise UnauthenticatedClientError()
402
+ response = self._session.get(url, stream=True, **kwargs)
403
+ return response
404
+
405
+ def get_webhook(self, **kwargs) -> requests.Response:
406
+ """ Returns webhook on the specified instance
407
+
408
+ Args:
409
+ kwargs(**): a variable number of keyword arguments for the get request
410
+ Raises:
411
+ RequestExceptions: exceptions thrown by the Requests library
412
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
413
+ Returns:
414
+ requests.Response: the response associated with the get request
415
+ """
416
+ return self.get_resource("webhooks", **kwargs)
417
+
418
+ def get_webhook_by_id(self, uuid: str, **kwargs) -> requests.Response:
419
+ """ Given a uuid, returns a specific webhook instance
420
+
421
+ Args:
422
+ uuid: the ID associated with the webhook
423
+ kwargs(**): a variable number of keyword arguments for the get request
424
+ Raises:
425
+ RequestExceptions: exceptions thrown by the Requests library
426
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
427
+ Returns:
428
+ requests.Response: the response associated with the get request
429
+ """
430
+ return self.get_resource(f'webhooks/{uuid}', **kwargs)
431
+
432
+ def get_robot_info(self, robot_nickname: str, **kwargs) -> requests.Response:
433
+ """ Given a robot nickname, returns information about the robot
434
+
435
+ Args:
436
+ robot_nickname: the nickname of the robot
437
+ kwargs(**): a variable number of keyword arguments for the get request
438
+ Raises:
439
+ RequestExceptions: exceptions thrown by the Requests library
440
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
441
+ Returns:
442
+ requests.Response: the response associated with the post request
443
+ """
444
+ return self.get_resource(f'robot-session/{robot_nickname}/session', **kwargs)
445
+
446
+ def post_export_as_walk(self, site_walk_uuid: str, **kwargs) -> requests.Response:
447
+ """ Given a site walk uuid, it exports the walks_pb2.Walk equivalent
448
+
449
+ Args:
450
+ site_walk_uuid: the ID associated with the site walk
451
+ kwargs(**): a variable number of keyword arguments for the get request
452
+ Raises:
453
+ RequestExceptions: exceptions thrown by the Requests library
454
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
455
+ Returns:
456
+ requests.Response: the response associated with the post request
457
+ """
458
+ return self.post_resource(f'site_walks/export_as_walk/{site_walk_uuid}', **kwargs)
459
+
460
+ def post_import_from_walk(self, **kwargs) -> requests.Response:
461
+ """ Given a walk data, imports it to the specified instance
462
+
463
+ Args:
464
+ kwargs(**): a variable number of keyword arguments for the post request
465
+ Raises:
466
+ RequestExceptions: exceptions thrown by the Requests library
467
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
468
+ Returns:
469
+ requests.Response: the response associated with the post request
470
+ """
471
+ return self.post_resource("site_walks/import_from_walk", **kwargs)
472
+
473
+ def post_site_element(self, **kwargs) -> requests.Response:
474
+ """ Create a site element. It also updates a pre-existing Site Element using the associated UUID.
475
+
476
+ Args:
477
+ kwargs(**): a variable number of keyword arguments for the post request
478
+ Raises:
479
+ RequestExceptions: exceptions thrown by the Requests library
480
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
481
+ Returns:
482
+ requests.Response: the response associated with the post request
483
+ """
484
+ return self.post_resource("site_elements", **kwargs)
485
+
486
+ def post_site_walk(self, **kwargs) -> requests.Response:
487
+ """ Create a site walk. It also updates a pre-existing Site Walk using the associated UUID.
488
+
489
+ Args:
490
+ kwargs(**): a variable number of keyword arguments for the post request
491
+ Raises:
492
+ RequestExceptions: exceptions thrown by the Requests library
493
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
494
+ Returns:
495
+ requests.Response: the response associated with the post request
496
+ """
497
+ return self.post_resource("site_walks", **kwargs)
498
+
499
+ def post_site_dock(self, **kwargs) -> requests.Response:
500
+ """ Create a site element. It also updates a pre-existing Site Dock using the associated UUID.
501
+
502
+ Args:
503
+ kwargs(**): a variable number of keyword arguments for the post request
504
+ Raises:
505
+ RequestExceptions: exceptions thrown by the Requests library
506
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
507
+ Returns:
508
+ requests.Response: the response associated with the post request
509
+ """
510
+ return self.post_resource("site_docks", **kwargs)
511
+
512
+ def post_robot(self, **kwargs) -> requests.Response:
513
+ """ Add a robot to the specified instance
514
+
515
+ Args:
516
+ kwargs(**): a variable number of keyword arguments for the post request
517
+ Raises:
518
+ RequestExceptions: exceptions thrown by the Requests library
519
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
520
+ Returns:
521
+ requests.Response: the response associated with the post request
522
+ """
523
+ return self.post_resource("robots", **kwargs)
524
+
525
+ def post_calendar_event(self, nickname: str = None, time_ms: int = None, repeat_ms: int = None,
526
+ mission_id: str = None, force_acquire_estop: bool = None,
527
+ require_docked: bool = None, schedule_name: str = None,
528
+ blackout_times: Iterable[dict[str:int]] = None,
529
+ disable_reason: str = None, event_id: str = None,
530
+ **kwargs) -> requests.Response:
531
+ """ This function serves two purposes. It creates a new calendar event on using the following arguments
532
+ when Event ID is not specified. When the Event ID associated with a pre-existing calendar event is specified,
533
+ the function overwrites the attributes of the pre-existing calendar event.
534
+
535
+ Args:
536
+ nickname: the name associated with the robot
537
+ time_ms: the first kickoff time in terms of milliseconds since epoch
538
+ repeat_ms:the delay time in milliseconds for repeating calendar events
539
+ mission_id: the UUID associated with the mission( also known as Site Walk)
540
+ force_acquire_estop: instructs the system to force acquire the estop when the mission kicks off
541
+ require_docked: determines whether the event will require the robot to be docked to start
542
+ schedule_name: the desired name of the calendar event
543
+ blackout_times: a specification for a time period over the course of a week when a schedule should not run
544
+ specified as list of a dictionary defined as {"startMs": <int>, "endMs" : <int>}
545
+ with startMs (inclusive) being the millisecond offset from the beginning of the week (Sunday) when this blackout period starts
546
+ and endMs (exclusive) being the millisecond offset from beginning of the week(Sunday) when this blackout period ends
547
+ disable_reason: (optional) a reason for disabling the calendar event
548
+ event_id: the auto-generated ID for a calendar event that is already posted on the instance.
549
+ This is only useful when editing a pre-existing calendar event.
550
+ kwargs(**): a variable number of keyword arguments for the post request
551
+ Raises:
552
+ RequestExceptions: exceptions thrown by the Requests library
553
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
554
+ Returns:
555
+ requests.Response: the response associated with the post request
556
+ """
557
+ # Check if the input contains the json param that is constructed outside the function
558
+ if 'json' in kwargs:
559
+ return self.post_resource("calendar/schedule", **kwargs)
560
+ # Construct payload based on provided inputs
561
+ payload = {
562
+ "agent": {
563
+ "nickname": nickname
564
+ },
565
+ "schedule": {
566
+ "timeMs": time_ms,
567
+ "repeatMs": repeat_ms,
568
+ "blackouts": blackout_times,
569
+ "disableReason": disable_reason,
570
+ },
571
+ "task": {
572
+ "missionId": mission_id,
573
+ "forceAcquireEstop": force_acquire_estop,
574
+ "requireDocked": require_docked,
575
+ },
576
+ "eventMetadata": {
577
+ "name": schedule_name,
578
+ "eventId": event_id
579
+ },
580
+ }
581
+ return self.post_resource("calendar/schedule", json=payload, **kwargs)
582
+
583
+ def post_calendar_events_disable_all(self, disable_reason: str, **kwargs) -> requests.Response:
584
+ """ Disable all scheduled missions
585
+
586
+ Args:
587
+ disable_reason: Reason for disabling all scheduled missions
588
+ kwargs(**): a variable number of keyword arguments for the post request
589
+ Raises:
590
+ RequestExceptions: exceptions thrown by the Requests library
591
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
592
+ Returns:
593
+ requests.Response: the response associated with the post request
594
+ """
595
+ return self.post_resource("calendar/disable-enable", json={"disableReason": disable_reason},
596
+ **kwargs)
597
+
598
+ def post_calendar_event_disable_by_id(self, event_id: str, disable_reason: str,
599
+ **kwargs) -> requests.Response:
600
+ """ Disable specific scheduled mission by event ID
601
+
602
+ Args:
603
+ event_id: eventId associated with a mission to disable
604
+ disable_reason: Reason for disabling a scheduled mission
605
+ kwargs(**): a variable number of keyword arguments for the post request
606
+ Raises:
607
+ RequestExceptions: exceptions thrown by the Requests library
608
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
609
+ Returns:
610
+ requests.Response: the response associated with the post request
611
+ """
612
+ return self.post_resource("calendar/disable-enable", json={
613
+ "disableReason": disable_reason,
614
+ "eventId": event_id
615
+ }, **kwargs)
616
+
617
+ def post_calendar_events_enable_all(self, **kwargs) -> requests.Response:
618
+ """ Enable all scheduled missions
619
+
620
+ Args:
621
+ kwargs(**): a variable number of keyword arguments for the post request
622
+ Raises:
623
+ RequestExceptions: exceptions thrown by the Requests library
624
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
625
+ Returns:
626
+ requests.Response: the response associated with the post request
627
+ """
628
+ return self.post_resource("calendar/disable-enable", json={"disableReason": ""}, **kwargs)
629
+
630
+ def post_calendar_event_enable_by_id(self, event_id: str, **kwargs) -> requests.Response:
631
+ """ Enable specific scheduled mission by event ID
632
+
633
+ Args:
634
+ event_id: eventId associated with a mission to enable
635
+ kwargs(**): a variable number of keyword arguments for the post request
636
+ Raises:
637
+ RequestExceptions: exceptions thrown by the Requests library
638
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
639
+ Returns:
640
+ requests.Response: the response associated with the post request
641
+ """
642
+ return self.post_resource("calendar/disable-enable", json={
643
+ "disableReason": "",
644
+ "eventId": event_id
645
+ }, **kwargs)
646
+
647
+ def post_webhook(self, **kwargs) -> requests.Response:
648
+ """ Create a webhook instance
649
+
650
+ Args:
651
+ kwargs(**): a variable number of keyword arguments for the post request
652
+ Raises:
653
+ RequestExceptions: exceptions thrown by the Requests library
654
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
655
+ Returns:
656
+ requests.Response: the response associated with the post request
657
+ """
658
+ return self.post_resource("webhooks", **kwargs)
659
+
660
+ def post_webhook_by_id(self, uuid: str, **kwargs) -> requests.Response:
661
+ """ Update an existing webhook instance
662
+
663
+ Args:
664
+ uuid: the ID associated with the desired webhook instance
665
+ kwargs(**): a variable number of keyword arguments for the post request
666
+ Raises:
667
+ RequestExceptions: exceptions thrown by the Requests library
668
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
669
+ Returns:
670
+ requests.Response: the response associated with the post request
671
+ """
672
+ return self.post_resource(f'webhooks/{uuid}', **kwargs)
673
+
674
+ def post_return_to_dock_mission(self, robot_nickname: str, site_dock_uuid: str,
675
+ **kwargs) -> requests.Response:
676
+ """ Generate a mission to send the robot back to the dock
677
+
678
+ Args:
679
+ robot_nickname: the nickname of the robot
680
+ site_dock_uuid: the uuid of the dock to send robot to
681
+ kwargs(**): a variable number of keyword arguments for the post request
682
+ Raises:
683
+ RequestExceptions: exceptions thrown by the Requests library
684
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
685
+ Returns:
686
+ requests.Response: the response associated with the post request
687
+ """
688
+ return self.post_resource('graph/send-robot', json={
689
+ "nickname": robot_nickname,
690
+ "siteDockUuid": site_dock_uuid
691
+ }, **kwargs)
692
+
693
+ def post_dispatch_mission_to_robot(self, robot_nickname: str, driver_id: str, mission_uuid: str,
694
+ delete_mission: bool, force_acquire_estop: bool,
695
+ **kwargs) -> requests.Response:
696
+ """ Dispatch the robot to a mission given a mission uuid
697
+
698
+ Args:
699
+ robot_nickname: the nickname of the robot
700
+ driver_id: the current driver ID of the mission
701
+ mission_uuid: uuid of the mission(also known as Site Walk) to dispatch
702
+ delete_mission: whether to delete the mission after playback
703
+ force_acquire_estop: whether to force acquire E-stop from the previous client
704
+ kwargs(**): a variable number of keyword arguments for the post request
705
+ Raises:
706
+ RequestExceptions: exceptions thrown by the Requests library
707
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
708
+ Returns:
709
+ requests.Response: the response associated with the post request
710
+ """
711
+ # Payload required for dispatching a mission
712
+ payload = {
713
+ "agent": {
714
+ "nickname": robot_nickname
715
+ },
716
+ "schedule": {
717
+ "timeMs": {
718
+ "low": 1,
719
+ "high": 0,
720
+ "unsigned": False
721
+ },
722
+ "repeatMs": {
723
+ "low": 0,
724
+ "high": 0,
725
+ "unsigned": False
726
+ }
727
+ },
728
+ "task": {
729
+ "missionId": mission_uuid,
730
+ "forceAcquireEstop": force_acquire_estop,
731
+ "deleteMission": delete_mission,
732
+ "requireDocked": False
733
+ },
734
+ "eventMetadata": {
735
+ "name": "API Triggered Mission"
736
+ }
737
+ }
738
+ return self.post_resource(
739
+ f'calendar/mission/dispatch/{robot_nickname}?currentDriverId={driver_id}', json=payload,
740
+ **kwargs)
741
+
742
+ def delete_site_walk(self, uuid: str, **kwargs) -> requests.Response:
743
+ """ Given a site walk uuid, deletes the site walk associated with the uuid on the specified instance
744
+
745
+ Args:
746
+ uuid: the ID associated with the desired site walk
747
+ kwargs(**): a variable number of keyword arguments for the delete request
748
+ Raises:
749
+ RequestExceptions: exceptions thrown by the Requests library
750
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
751
+ Returns:
752
+ requests.Response: the response associated with the delete request
753
+ """
754
+ return self.delete_resource(f'site_walks/{uuid}', **kwargs)
755
+
756
+ def delete_robot(self, robot_hostname: str, **kwargs) -> requests.Response:
757
+ """ Given a robot hostname, deletes the robot associated with the hostname on the specified instance
758
+
759
+ Args:
760
+ robot_hostname: the IP address associated with the robot
761
+ kwargs(**): a variable number of keyword arguments for the delete request
762
+ Raises:
763
+ RequestExceptions: exceptions thrown by the Requests library
764
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
765
+ Returns:
766
+ requests.Response: the response associated with the delete request
767
+ """
768
+ return self.delete_resource(f'robots/{robot_hostname}', **kwargs)
769
+
770
+ def delete_calendar_event(self, event_id: str, **kwargs) -> requests.Response:
771
+ """ Delete the specified calendar event on the specified instance
772
+
773
+ Args:
774
+ event_id(string): the ID associated with the calendar event
775
+ kwargs(**): a variable number of keyword arguments for the delete request
776
+ Raises:
777
+ RequestExceptions: exceptions thrown by the Requests library
778
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
779
+ Returns:
780
+ requests.Response: the response associated with the delete request
781
+ """
782
+ return self.delete_resource(f'calendar/schedule/{event_id}', **kwargs)
783
+
784
+ def delete_webhook(self, uuid: str, **kwargs) -> requests.Response:
785
+ """ Delete the specified webhook instance on the specified instance
786
+
787
+ Args:
788
+ uuid: the ID associated with the desired webhook
789
+ kwargs(**): a variable number of keyword arguments for the delete request
790
+ Raises:
791
+ RequestExceptions: exceptions thrown by the Requests library
792
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
793
+ Returns:
794
+ requests.Response: the response associated with the delete request
795
+ """
796
+ return self.delete_resource(f'webhooks/{uuid}', **kwargs)
797
+
798
+
799
+ def create_client(options: 'argparse.Namespace') -> 'bosdyn.orbit.client.Client':
800
+ """ Creates a client object.
801
+
802
+ Args:
803
+ options: User input containing hostname, verification, and certification info.
804
+ Returns:
805
+ client: 'bosdyn.orbit.client.Client' object
806
+ """
807
+ # Determine the value for the argument "verify"
808
+ if options.verify in ["True", "False"]:
809
+ verify = options.verify == "True"
810
+ else:
811
+ print(
812
+ "The provided value for the argument verify [%s] is not either 'True' or 'False'. Assuming verify is set to 'path/to/CA bundle'"
813
+ .format(options.verify))
814
+ verify = options.verify
815
+
816
+ # A client object represents a single instance.
817
+ client = Client(hostname=options.hostname, verify=verify, cert=options.cert)
818
+
819
+ # The client needs to be authenticated before using its functions
820
+ client.authenticate_with_api_token()
821
+
822
+ return client
@@ -0,0 +1,22 @@
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
+ """Relevant exceptions for bosdyn-orbit"""
8
+
9
+
10
+ class Error(Exception):
11
+ """Base exception"""
12
+
13
+
14
+ class UnauthenticatedClientError(Error):
15
+ """The client is not authenticated properly."""
16
+
17
+ def __str__(self):
18
+ return 'The client is not authenticated properly. Run the proper authentication before calling other client functions!'
19
+
20
+
21
+ class WebhookSignatureVerificationError(Error):
22
+ """The webhook signature could not be verified."""
bosdyn/orbit/utils.py ADDED
@@ -0,0 +1,305 @@
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
+ """Utility functions for bosdyn-orbit"""
8
+ import datetime
9
+ import hashlib
10
+ import hmac
11
+ import json
12
+ import os
13
+ import secrets
14
+ import shutil
15
+ import sys
16
+ import time
17
+
18
+ from bosdyn.orbit.exceptions import WebhookSignatureVerificationError
19
+
20
+ API_TOKEN_ENV_VAR = "BOSDYN_ORBIT_CLIENT_API_TOKEN"
21
+ DEFAULT_MAX_MESSAGE_AGE_MS = 5 * 60 * 1000
22
+
23
+
24
+ def get_api_token() -> str:
25
+ """ Obtains an API token from either an environment variable or terminal input
26
+
27
+ Returns
28
+ api_token: the API token obtained from the instance
29
+ """
30
+ api_token = os.environ.get(API_TOKEN_ENV_VAR)
31
+ if not api_token:
32
+ if sys.stdin.isatty():
33
+ print('API Token: ', end='', file=sys.stderr)
34
+ api_token = input()
35
+ return api_token
36
+
37
+
38
+ def get_latest_created_at_for_run_events(client: 'bosdyn.orbit.client.Client',
39
+ params: dict = {}) -> datetime.datetime:
40
+ """ Given a dictionary of query params, returns the max created at time for run events
41
+
42
+ Args:
43
+ client: the client for the web API
44
+ params: the query params associated with the get request
45
+ Raises:
46
+ RequestExceptions: exceptions thrown by the Requests library
47
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
48
+ Returns:
49
+ The max created at time for run events in datetime
50
+ """
51
+ base_params = {'limit': 1, 'orderBy': '-created_at'}
52
+ base_params.update(params)
53
+ latest_resource = client.get_run_events(params=base_params).json()
54
+ if not latest_resource["resources"]:
55
+ client_timestamp_response = client.get_system_time()
56
+ ms_since_epoch = int(client_timestamp_response.json()["msSinceEpoch"])
57
+ return datetime.datetime.utcfromtimestamp(ms_since_epoch / 1000)
58
+ return datetime_from_isostring(latest_resource["resources"][0]["createdAt"])
59
+
60
+
61
+ def get_latest_run_capture_resources(client: 'bosdyn.orbit.client.Client',
62
+ params: dict = {}) -> list:
63
+ """ Given a dictionary of query params, returns the latest run capture resources in json format
64
+
65
+ Args:
66
+ client: the client for Orbit web API
67
+ params: the query params associated with the get request
68
+ Raises:
69
+ RequestExceptions: exceptions thrown by the Requests library
70
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
71
+ Returns:
72
+ A list of resources obtained from a RESTful endpoint
73
+ """
74
+ base_params = {'orderBy': '-created_at'}
75
+ base_params.update(params)
76
+ run_captures = client.get_run_captures(params=base_params).json()
77
+ return run_captures["resources"]
78
+
79
+
80
+ def get_latest_created_at_for_run_captures(client: 'bosdyn.orbit.client.Client',
81
+ params: dict = {}) -> datetime.datetime:
82
+ """ Given a dictionary of query params, returns the max created at time for run captures
83
+
84
+ Args:
85
+ client: the client for Orbit web API
86
+ params: the query params associated with the get request
87
+ Raises:
88
+ RequestExceptions: exceptions thrown by the Requests library
89
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
90
+ Returns:
91
+ The max created at time for run captures in datetime
92
+ """
93
+ base_params = {'limit': 1, 'orderBy': '-created_at'}
94
+ base_params.update(params)
95
+ latest_resource = client.get_run_captures(params=base_params).json()
96
+ if not latest_resource["resources"]:
97
+ client_timestamp_response = client.get_system_time()
98
+ ms_since_epoch = int(client_timestamp_response.json()["msSinceEpoch"])
99
+ return datetime.datetime.utcfromtimestamp(ms_since_epoch / 1000)
100
+ return datetime_from_isostring(latest_resource["resources"][0]["createdAt"])
101
+
102
+
103
+ def get_latest_run_resource(client: 'bosdyn.orbit.client.Client', params: dict = {}) -> list:
104
+ """ Given a dictionary of query params, returns the latest run resource in json format
105
+
106
+ Args:
107
+ client: the client for Orbit web API
108
+ params: the query params associated with the get request
109
+ Raises:
110
+ RequestExceptions: exceptions thrown by the Requests library
111
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
112
+ Returns:
113
+ A list corresponding to a run resource obtained from a RESTful endpoint in json
114
+ """
115
+ base_params = {'limit': 1, 'orderBy': 'newest'}
116
+ base_params.update(params)
117
+ latest_run_json = client.get_runs(params=base_params).json()
118
+ if not latest_run_json['resources']:
119
+ return None
120
+ return latest_run_json['resources'][0]
121
+
122
+
123
+ def get_latest_run_in_progress(client: 'bosdyn.orbit.client.Client', params: dict = {}) -> list:
124
+ """ Given a dictionary of query params, returns the latest running resource in json format
125
+
126
+ Args:
127
+ client: the client for Orbit web API
128
+ params: the query params associated with the get request
129
+ Raises:
130
+ RequestExceptions: exceptions thrown by the Requests library
131
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
132
+ Returns:
133
+ A list corresponding to a run obtained from a RESTful endpoint in json
134
+ """
135
+ base_params = {'orderBy': 'newest'}
136
+ base_params.update(params)
137
+ latest_resources = client.get_runs(params=base_params).json()["resources"]
138
+ for resource in latest_resources:
139
+ if resource["missionStatus"] not in [
140
+ "SUCCESS", "FAILURE", "ERROR", "STOPPED", "NONE", "UNKNOWN"
141
+ ]:
142
+ return resource
143
+ return None
144
+
145
+
146
+ def get_latest_end_time_for_runs(client: 'bosdyn.orbit.client.Client',
147
+ params: dict = {}) -> datetime.datetime:
148
+ """ Given a dictionary of query params, returns the max end time for runs
149
+
150
+ Args:
151
+ client: the client for Orbit web API
152
+ params: the query params associated with the get request
153
+ Raises:
154
+ RequestExceptions: exceptions thrown by the Requests library
155
+ UnauthenticatedClientError: indicates that the client is not authenticated properly
156
+ Returns:
157
+ The max end time for runs in datetime
158
+ """
159
+ base_params = {'limit': 1, 'orderBy': 'newest'}
160
+ base_params.update(params)
161
+ latest_resource = client.get_runs(params=base_params).json()
162
+ if latest_resource.get("resources"):
163
+ latest_end_time = latest_resource.get("resources")[0]["endTime"]
164
+ if latest_end_time:
165
+ return datetime_from_isostring(latest_end_time)
166
+ client_timestamp_response = client.get_system_time()
167
+ ms_since_epoch = int(client_timestamp_response.json()["msSinceEpoch"])
168
+ return datetime.datetime.utcfromtimestamp(ms_since_epoch / 1000)
169
+
170
+
171
+ def write_image(img_raw, image_fp: str) -> None:
172
+ """ Given a raw image and a desired output directory, writes the image to a file
173
+
174
+ Args:
175
+ img_raw(Raw image object): the input raw image
176
+ image_fp: the output filepath for the image
177
+ """
178
+ os.makedirs(os.path.dirname(image_fp), exist_ok=True)
179
+ with open(image_fp, 'wb') as out_file:
180
+ shutil.copyfileobj(img_raw, out_file)
181
+
182
+
183
+ def data_capture_urls_from_run_events(client: 'bosdyn.orbit.client.Client', run_events: list,
184
+ list_of_channel_names: list = None) -> list:
185
+ """ Given run events and list of desired channel names, returns the list of data capture urls
186
+
187
+ Args:
188
+ client: the client for Orbit web API
189
+ run_events: a json representation of run events obtained from a RESTful endpoint
190
+ list_of_channel_names: a list of channel names associated with the desired data captures.
191
+ Defaults to None which returns all the available channels.
192
+ Returns:
193
+ data_urls: a list of urls
194
+ """
195
+ all_run_events_resources = run_events["resources"]
196
+ data_urls = []
197
+ for resource in all_run_events_resources:
198
+ all_data_captures = resource["dataCaptures"]
199
+ for data_capture in all_data_captures:
200
+ if list_of_channel_names is None:
201
+ # check if exists in unique_list or not
202
+ if list_of_channel_names not in data_urls:
203
+ data_urls.append(f'https://{client._hostname}' + data_capture["dataUrl"])
204
+ elif data_capture["channelName"] in list_of_channel_names:
205
+ # check if exists in unique_list or not
206
+ if list_of_channel_names not in data_urls:
207
+ data_urls.append(f'https://{client._hostname}' + data_capture["dataUrl"])
208
+ return data_urls
209
+
210
+
211
+ def data_capture_url_from_run_capture_resources(client: 'bosdyn.orbit.client.Client',
212
+ run_capture_resources: list,
213
+ list_of_channel_names: list = None) -> list:
214
+ """ Given run capture resources and list of desired channel names, returns the list of data capture urls
215
+
216
+ Args:
217
+ client: the client for Orbit web API
218
+ run_capture_resources: a list of resources obtained from a RESTful endpoint
219
+ list_of_channel_names: a list of channel names associated with the desired data captures.
220
+ Defaults to None which returns all the available channels.
221
+ Returns:
222
+ data_urls: a list of urls
223
+ """
224
+ data_urls = []
225
+ for data_capture in run_capture_resources:
226
+ if list_of_channel_names is None:
227
+ # check if exists in unique_list or not
228
+ if list_of_channel_names not in data_urls:
229
+ data_urls.append(f'https://{client._hostname}' + data_capture["dataUrl"])
230
+ elif data_capture["channelName"] in list_of_channel_names:
231
+ # check if exists in unique_list or not
232
+ if list_of_channel_names not in data_urls:
233
+ data_urls.append(f'https://{client._hostname}' + data_capture["dataUrl"])
234
+ return data_urls
235
+
236
+
237
+ def get_action_names_from_run_events(run_events: dict) -> list:
238
+ """ Given run events, returns a list of action names
239
+
240
+ Args:
241
+ run_events: a representation of run events obtained from a RESTful endpoint
242
+ Returns:
243
+ action_names: a list of action names
244
+ """
245
+ all_run_events_resources = run_events["resources"]
246
+ action_names = []
247
+ for resource in all_run_events_resources:
248
+ action_names.append(resource["actionName"])
249
+ return action_names
250
+
251
+
252
+ def datetime_from_isostring(datetime_isostring: str) -> datetime.datetime:
253
+ """ Returns the datetime representation of the iso string representation of time
254
+
255
+ Args:
256
+ datetime_isostring: the iso string representation of time
257
+ Returns:
258
+ The datetime representation of the iso string representation of time
259
+ """
260
+ if "Z" in datetime_isostring:
261
+ return datetime.datetime.strptime(datetime_isostring, "%Y-%m-%dT%H:%M:%S.%fZ")
262
+ if "+" in datetime_isostring:
263
+ return datetime.datetime.strptime(datetime_isostring[0:datetime_isostring.index("+")],
264
+ "%Y-%m-%dT%H:%M:%S.%f")
265
+
266
+
267
+ def validate_webhook_payload(payload: dict, signature_header: str, secret: str,
268
+ max_age_ms: int = DEFAULT_MAX_MESSAGE_AGE_MS) -> None:
269
+ """ Verifies that the webhook payload came from
270
+
271
+ Args:
272
+ payload: the JSON body of the webhooks req
273
+ signature_header: the value of the signature header
274
+ secret: the configured secret value for this webhook
275
+ max_age_ms: the maximum age of the message before it's considered invalid (default is 5 minutes)
276
+ Raises:
277
+ bosdyn.orbit.exceptions.WebhookSignatureVerificationError: thrown if the webhook signature is invalid
278
+ """
279
+ if not signature_header:
280
+ raise WebhookSignatureVerificationError("Signature header cannot be empty")
281
+
282
+ header_components = dict(entry.split('=') for entry in signature_header.split(','))
283
+ send_time = header_components.get('t')
284
+ send_time_ms = int(send_time) if send_time is not None and send_time.isdigit() else None
285
+ received_hmac = header_components.get('v1')
286
+ if not send_time_ms or not received_hmac:
287
+ raise WebhookSignatureVerificationError(
288
+ "Missing either send time or HMAC in signature header")
289
+
290
+ current_time_ns = time.time()
291
+ current_time_ms = round(current_time_ns) * 1000
292
+ time_diff_ms = current_time_ms - send_time_ms
293
+ if time_diff_ms > max_age_ms:
294
+ raise WebhookSignatureVerificationError(
295
+ f"The payload is {time_diff_ms}ms old, which is greater than the maximum age {max_age_ms}ms"
296
+ )
297
+
298
+ full_payload_string = f'{send_time}.{json.dumps(payload, separators=(",",":"))}'
299
+ calculated_hmac = hmac.new(bytes.fromhex(secret), full_payload_string.encode('utf-8'),
300
+ hashlib.sha256).hexdigest()
301
+
302
+ time_safe_equal = secrets.compare_digest(received_hmac, calculated_hmac)
303
+ if not time_safe_equal:
304
+ raise WebhookSignatureVerificationError(
305
+ "The received HMAC did not match the expected value")
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.1
2
+ Name: bosdyn-orbit
3
+ Version: 4.0.0
4
+ Summary: Boston Dynamics API Orbit Client
5
+ Home-page: https://dev.bostondynamics.com/docs/orbit/
6
+ Author: Boston Dynamics
7
+ Author-email: support@bostondynamics.com
8
+ Project-URL: Documentation, https://dev.bostondynamics.com/docs/orbit/
9
+ Project-URL: Source, https://github.com/boston-dynamics/spot-sdk/
10
+ Classifier: Programming Language :: Python :: 3.6
11
+ Classifier: Programming Language :: Python :: 3.7
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: License :: Other/Proprietary License
16
+ Classifier: Operating System :: OS Independent
17
+ Requires-Python: >=3.6
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: requests
20
+ Requires-Dist: Deprecated (~=1.2.10)
21
+
22
+ <!--
23
+ Copyright (c) 2023 Boston Dynamics, Inc. All rights reserved.
24
+
25
+ Downloading, reproducing, distributing or otherwise using the SDK Software
26
+ is subject to the terms and conditions of the Boston Dynamics Software
27
+ Development Kit License (20191101-BDSDK-SL).
28
+ -->
29
+
30
+ # bosdyn-orbit
31
+
32
+ The bosdyn-orbit wheel contains the Orbit client for the Boston Dynamics Orbit API. The Orbit API is hosted on your Orbit. The Orbit client interacts with the Orbit API by sending HTTPs requests to a number of resource endpoints. The client offers the following main functionalities:
33
+
34
+ - Retrieve Orbit software version info
35
+ - Get Orbit's current system time
36
+ - List robots
37
+ - List site walks, site elements, and site docks
38
+ - List posted calendar events
39
+ - Access resources such as runs, run events, and run captures
40
+ - Get images
41
+ - Export and import site walks
42
+ - Create a site walk, site element, and site dock
43
+ - Add robots to the specified Orbit instance
44
+ - Create a calendar event to play a mission
45
+ - Delete site walks, site elements, and site docks
46
+ - Remove robots
47
+ - Delete calendar event
@@ -0,0 +1,9 @@
1
+ bosdyn/__init__.py,sha256=CMQioQKK1NlMk3kZuY49b_Aw-JyvDeOtuqOCAul1I0s,330
2
+ bosdyn/orbit/__init__.py,sha256=L_VSEXjtWZNHVHs3Jdr_TF2pJ2ju2yysAi0-YZsbJoU,266
3
+ bosdyn/orbit/client.py,sha256=6aY_il9mPt8IjYdWwPFWBdyxrxaNv1z3KJDxeX-B9kw,40103
4
+ bosdyn/orbit/exceptions.py,sha256=eaeHSlGh27JlZUEjcpLKxR1CNdW6Twp4e685pUgEjyQ,711
5
+ bosdyn/orbit/utils.py,sha256=Ryz0oHIud5n433JDuiyLLUjOS89wWcMbOu45b4ISVMw,13636
6
+ bosdyn_orbit-4.0.0.dist-info/METADATA,sha256=5zpzRF8QV-OkBQWIoNI0-1Y2gV0p-wNk47rCxuBcVwY,1894
7
+ bosdyn_orbit-4.0.0.dist-info/WHEEL,sha256=AtBG6SXL3KF_v0NxLf0ehyVOh0cold-JbJYXNGorC6Q,92
8
+ bosdyn_orbit-4.0.0.dist-info/top_level.txt,sha256=an2OWgx1ej2jFjmBjPWNQ68ZglvUfKhmXWW-WhTtDmA,7
9
+ bosdyn_orbit-4.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.41.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ bosdyn