|
| 1 | +import json |
1 | 2 | import logging
|
2 | 3 | import os
|
3 | 4 | import random
|
4 | 5 | import time
|
5 | 6 | import uuid
|
6 | 7 | from functools import wraps
|
7 |
| -from typing import Callable, Optional |
| 8 | +from typing import Callable, Dict, Optional, Union |
8 | 9 | from urllib.parse import urlsplit, urlunsplit
|
9 | 10 |
|
10 | 11 | import requests
|
11 | 12 | from model import Detector, ImageQuery
|
12 | 13 | from openapi_client.api_client import ApiClient, ApiException
|
13 | 14 |
|
| 15 | +from groundlight.images import ByteStreamWrapper |
14 | 16 | from groundlight.status_codes import is_ok
|
15 | 17 |
|
16 | 18 | logger = logging.getLogger("groundlight.sdk")
|
@@ -225,3 +227,182 @@ def _get_detector_by_name(self, name: str) -> Detector:
|
225 | 227 | f"We found multiple ({parsed['count']}) detectors with the same name. This shouldn't happen.",
|
226 | 228 | )
|
227 | 229 | return Detector.parse_obj(parsed["results"][0])
|
| 230 | + |
| 231 | + @RequestsRetryDecorator() |
| 232 | + def submit_image_query_with_inspection( # noqa: PLR0913 # pylint: disable=too-many-arguments |
| 233 | + self, |
| 234 | + detector_id: str, |
| 235 | + patience_time: float, |
| 236 | + body: ByteStreamWrapper, |
| 237 | + inspection_id: str, |
| 238 | + human_review: str = "DEFAULT", |
| 239 | + ) -> str: |
| 240 | + """Submits an image query to the API and returns the ID of the image query. |
| 241 | + The image query will be associated to the inspection_id provided. |
| 242 | + """ |
| 243 | + |
| 244 | + url = f"{self.configuration.host}/posichecks" |
| 245 | + |
| 246 | + params: Dict[str, Union[str, float, bool]] = { |
| 247 | + "inspection_id": inspection_id, |
| 248 | + "predictor_id": detector_id, |
| 249 | + "patience_time": patience_time, |
| 250 | + } |
| 251 | + |
| 252 | + # In the API, 'send_notification' is used to control human_review escalation. This will eventually |
| 253 | + # be deprecated, but for now we need to support it in the following manner: |
| 254 | + if human_review == "ALWAYS": |
| 255 | + params["send_notification"] = True |
| 256 | + elif human_review == "NEVER": |
| 257 | + params["send_notification"] = False |
| 258 | + else: |
| 259 | + pass # don't send the send_notifications param, allow "DEFAULT" behavior |
| 260 | + |
| 261 | + headers = self._headers() |
| 262 | + headers["Content-Type"] = "image/jpeg" |
| 263 | + |
| 264 | + response = requests.request("POST", url, headers=headers, params=params, data=body.read()) |
| 265 | + |
| 266 | + if not is_ok(response.status_code): |
| 267 | + logger.info(response) |
| 268 | + raise InternalApiError( |
| 269 | + status=response.status_code, |
| 270 | + reason=f"Error submitting image query with inspection ID {inspection_id} on detector {detector_id}", |
| 271 | + http_resp=response, |
| 272 | + ) |
| 273 | + |
| 274 | + return response.json()["id"] |
| 275 | + |
| 276 | + @RequestsRetryDecorator() |
| 277 | + def start_inspection(self) -> str: |
| 278 | + """Starts an inspection, returns the ID.""" |
| 279 | + url = f"{self.configuration.host}/inspections" |
| 280 | + |
| 281 | + headers = self._headers() |
| 282 | + |
| 283 | + response = requests.request("POST", url, headers=headers, json={}) |
| 284 | + |
| 285 | + if not is_ok(response.status_code): |
| 286 | + raise InternalApiError( |
| 287 | + status=response.status_code, |
| 288 | + reason="Error starting inspection.", |
| 289 | + http_resp=response, |
| 290 | + ) |
| 291 | + |
| 292 | + return response.json()["id"] |
| 293 | + |
| 294 | + @RequestsRetryDecorator() |
| 295 | + def update_inspection_metadata(self, inspection_id: str, user_provided_key: str, user_provided_value: str) -> None: |
| 296 | + """Add/update inspection metadata with the user_provided_key and user_provided_value. |
| 297 | +
|
| 298 | + The API stores inspections metadata in two ways: |
| 299 | + 1) At the top level of the inspection with user_provided_id_key and user_provided_id_value. This is a |
| 300 | + kind of "primary" piece of metadata for the inspection. Only one key/value pair is allowed at this level. |
| 301 | + 2) In the user_metadata field as a dictionary. Multiple key/value pairs are allowed at this level. |
| 302 | +
|
| 303 | + The first piece of metadata presented to an inspection will be assumed to be the user_provided_id_key and |
| 304 | + user_provided_id_value. All subsequent pieces metadata will be stored in the user_metadata field. |
| 305 | +
|
| 306 | + """ |
| 307 | + url = f"{self.configuration.host}/inspections/{inspection_id}" |
| 308 | + |
| 309 | + headers = self._headers() |
| 310 | + |
| 311 | + # Get inspection in order to find out: |
| 312 | + # 1) if user_provided_id_key has been set |
| 313 | + # 2) if the inspection is closed |
| 314 | + response = requests.request("GET", url, headers=headers) |
| 315 | + |
| 316 | + if not is_ok(response.status_code): |
| 317 | + raise InternalApiError( |
| 318 | + status=response.status_code, |
| 319 | + reason=f"Error getting inspection details for inspection {inspection_id}.", |
| 320 | + http_resp=response, |
| 321 | + ) |
| 322 | + if response.json()["status"] == "COMPLETE": |
| 323 | + raise ValueError(f"Inspection {inspection_id} is closed. Metadata cannot be added.") |
| 324 | + |
| 325 | + payload = {} |
| 326 | + |
| 327 | + # Set the user_provided_id_key and user_provided_id_value if they were not previously set. |
| 328 | + response_json = response.json() |
| 329 | + if not response_json.get("user_provided_id_key"): |
| 330 | + payload["user_provided_id_key"] = user_provided_key |
| 331 | + payload["user_provided_id_value"] = user_provided_value |
| 332 | + |
| 333 | + # Get the existing keys and values in user_metadata (if any) so that we don't overwrite them. |
| 334 | + metadata = response_json["user_metadata"] |
| 335 | + if not metadata: |
| 336 | + metadata = {} |
| 337 | + |
| 338 | + # Submit the new metadata |
| 339 | + metadata[user_provided_key] = user_provided_value |
| 340 | + payload["user_metadata_json"] = json.dumps(metadata) |
| 341 | + response = requests.request("PATCH", url, headers=headers, json=payload) |
| 342 | + |
| 343 | + if not is_ok(response.status_code): |
| 344 | + raise InternalApiError( |
| 345 | + status=response.status_code, |
| 346 | + reason=f"Error updating inspection metadata on inspection {inspection_id}.", |
| 347 | + http_resp=response, |
| 348 | + ) |
| 349 | + |
| 350 | + @RequestsRetryDecorator() |
| 351 | + def stop_inspection(self, inspection_id: str) -> str: |
| 352 | + """Stops an inspection and raises an exception if the response from the server does not indicate success. |
| 353 | + Returns a string that indicates the result (either PASS or FAIL). The URCap requires this. |
| 354 | + """ |
| 355 | + url = f"{self.configuration.host}/inspections/{inspection_id}" |
| 356 | + |
| 357 | + headers = self._headers() |
| 358 | + |
| 359 | + # Closing an inspection generates a new inspection PDF. Therefore, if the inspection |
| 360 | + # is already closed, just return "COMPLETE" to avoid unnecessarily generating a new PDF. |
| 361 | + response = requests.request("GET", url, headers=headers) |
| 362 | + |
| 363 | + if not is_ok(response.status_code): |
| 364 | + raise InternalApiError( |
| 365 | + status=response.status_code, |
| 366 | + reason=f"Error checking the status of {inspection_id}.", |
| 367 | + http_resp=response, |
| 368 | + ) |
| 369 | + |
| 370 | + if response.json().get("status") == "COMPLETE": |
| 371 | + return "COMPLETE" |
| 372 | + |
| 373 | + payload = {"status": "COMPLETE"} |
| 374 | + |
| 375 | + response = requests.request("PATCH", url, headers=headers, json=payload) |
| 376 | + |
| 377 | + if not is_ok(response.status_code): |
| 378 | + raise InternalApiError( |
| 379 | + status=response.status_code, |
| 380 | + reason=f"Error stopping inspection {inspection_id}.", |
| 381 | + http_resp=response, |
| 382 | + ) |
| 383 | + |
| 384 | + return response.json()["result"] |
| 385 | + |
| 386 | + @RequestsRetryDecorator() |
| 387 | + def update_detector_confidence_threshold(self, detector_id: str, confidence_threshold: float) -> None: |
| 388 | + """Updates the confidence threshold of a detector.""" |
| 389 | + |
| 390 | + # The API does not validate the confidence threshold, |
| 391 | + # so we will validate it here and raise an exception if necessary. |
| 392 | + if confidence_threshold < 0 or confidence_threshold > 1: |
| 393 | + raise ValueError(f"Confidence threshold must be between 0 and 1. Got {confidence_threshold}.") |
| 394 | + |
| 395 | + url = f"{self.configuration.host}/predictors/{detector_id}" |
| 396 | + |
| 397 | + headers = self._headers() |
| 398 | + |
| 399 | + payload = {"confidence_threshold": confidence_threshold} |
| 400 | + |
| 401 | + response = requests.request("PATCH", url, headers=headers, json=payload) |
| 402 | + |
| 403 | + if not is_ok(response.status_code): |
| 404 | + raise InternalApiError( |
| 405 | + status=response.status_code, |
| 406 | + reason=f"Error updating detector: {detector_id}.", |
| 407 | + http_resp=response, |
| 408 | + ) |
0 commit comments