hiddenlayer-sdk 1.2.1__py3-none-any.whl → 2.0.1__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- hiddenlayer/__init__.py +0 -10
- hiddenlayer/sdk/exceptions.py +1 -1
- hiddenlayer/sdk/models.py +2 -3
- hiddenlayer/sdk/rest/__init__.py +16 -11
- hiddenlayer/sdk/rest/api/__init__.py +0 -1
- hiddenlayer/sdk/rest/api/model_supply_chain_api.py +1706 -571
- hiddenlayer/sdk/rest/api/sensor_api.py +214 -1320
- hiddenlayer/sdk/rest/models/__init__.py +16 -10
- hiddenlayer/sdk/rest/models/{scan_model_request.py → begin_multi_file_upload200_response.py} +9 -9
- hiddenlayer/sdk/rest/models/{get_multipart_upload_response.py → begin_multipart_file_upload200_response.py} +9 -9
- hiddenlayer/sdk/rest/models/{multipart_upload_part.py → begin_multipart_file_upload200_response_parts_inner.py} +11 -10
- hiddenlayer/sdk/rest/models/errors_inner.py +91 -0
- hiddenlayer/sdk/rest/models/file_details_v3.py +8 -2
- hiddenlayer/sdk/rest/models/{scan_results_v2.py → file_result_v3.py} +21 -32
- hiddenlayer/sdk/rest/models/{model_scan_api_v3_scan_query200_response.py → get_condensed_model_scan_reports200_response.py} +4 -4
- hiddenlayer/sdk/rest/models/inventory_v3.py +97 -0
- hiddenlayer/sdk/rest/models/model_inventory_info.py +1 -1
- hiddenlayer/sdk/rest/models/{detections.py → multi_file_upload_request_v3.py} +14 -22
- hiddenlayer/sdk/rest/models/{model_scan_api_v3_scan_model_version_id_patch200_response.py → notify_model_scan_completed200_response.py} +4 -4
- hiddenlayer/sdk/rest/models/pagination_v3.py +95 -0
- hiddenlayer/sdk/rest/models/problem_details.py +103 -0
- hiddenlayer/sdk/rest/models/scan_detection_v31.py +155 -0
- hiddenlayer/sdk/rest/models/scan_model_details_v3.py +1 -1
- hiddenlayer/sdk/rest/models/scan_results_map_v3.py +105 -0
- hiddenlayer/sdk/rest/models/scan_results_v3.py +120 -0
- hiddenlayer/sdk/rest/models/{model.py → sensor.py} +4 -4
- hiddenlayer/sdk/rest/models/{model_query_response.py → sensor_query_response.py} +7 -7
- hiddenlayer/sdk/services/aidr_predictive.py +57 -3
- hiddenlayer/sdk/services/model_scan.py +98 -135
- hiddenlayer/sdk/version.py +1 -1
- {hiddenlayer_sdk-1.2.1.dist-info → hiddenlayer_sdk-2.0.1.dist-info}/METADATA +12 -2
- {hiddenlayer_sdk-1.2.1.dist-info → hiddenlayer_sdk-2.0.1.dist-info}/RECORD +35 -31
- hiddenlayer/sdk/rest/api/model_scan_api.py +0 -591
- hiddenlayer/sdk/rest/models/scan_results.py +0 -118
- hiddenlayer/sdk/services/model.py +0 -149
- {hiddenlayer_sdk-1.2.1.dist-info → hiddenlayer_sdk-2.0.1.dist-info}/LICENSE +0 -0
- {hiddenlayer_sdk-1.2.1.dist-info → hiddenlayer_sdk-2.0.1.dist-info}/WHEEL +0 -0
- {hiddenlayer_sdk-1.2.1.dist-info → hiddenlayer_sdk-2.0.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,120 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
"""
|
4
|
+
HiddenLayer ModelScan V2
|
5
|
+
|
6
|
+
HiddenLayer ModelScan API for scanning of models
|
7
|
+
|
8
|
+
The version of the OpenAPI document: 1
|
9
|
+
Generated by OpenAPI Generator (https://openapi-generator.tech)
|
10
|
+
|
11
|
+
Do not edit the class manually.
|
12
|
+
""" # noqa: E501
|
13
|
+
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
import pprint
|
17
|
+
import re # noqa: F401
|
18
|
+
import json
|
19
|
+
|
20
|
+
from pydantic import BaseModel, ConfigDict, StrictInt, StrictStr, field_validator
|
21
|
+
from typing import Any, ClassVar, Dict, List, Optional
|
22
|
+
from hiddenlayer.sdk.rest.models.file_result_v3 import FileResultV3
|
23
|
+
from typing import Optional, Set
|
24
|
+
from typing_extensions import Self
|
25
|
+
|
26
|
+
class ScanResultsV3(BaseModel):
|
27
|
+
"""
|
28
|
+
ScanResultsV3
|
29
|
+
""" # noqa: E501
|
30
|
+
scan_id: Optional[StrictStr] = None
|
31
|
+
start_time: Optional[StrictInt] = None
|
32
|
+
end_time: Optional[StrictInt] = None
|
33
|
+
status: Optional[StrictStr] = None
|
34
|
+
version: Optional[StrictStr] = None
|
35
|
+
inventory: Optional[Dict[str, Any]] = None
|
36
|
+
file_results: List[FileResultV3]
|
37
|
+
__properties: ClassVar[List[str]] = ["scan_id", "start_time", "end_time", "status", "version", "inventory", "file_results"]
|
38
|
+
|
39
|
+
@field_validator('status')
|
40
|
+
def status_validate_enum(cls, value):
|
41
|
+
"""Validates the enum"""
|
42
|
+
if value is None:
|
43
|
+
return value
|
44
|
+
|
45
|
+
if value not in set(['done', 'running', 'failed', 'pending', 'canceled']):
|
46
|
+
raise ValueError("must be one of enum values ('done', 'running', 'failed', 'pending', 'canceled')")
|
47
|
+
return value
|
48
|
+
|
49
|
+
model_config = ConfigDict(
|
50
|
+
populate_by_name=True,
|
51
|
+
validate_assignment=True,
|
52
|
+
protected_namespaces=(),
|
53
|
+
)
|
54
|
+
|
55
|
+
|
56
|
+
def to_str(self) -> str:
|
57
|
+
"""Returns the string representation of the model using alias"""
|
58
|
+
return pprint.pformat(self.model_dump(by_alias=True))
|
59
|
+
|
60
|
+
def to_json(self) -> str:
|
61
|
+
"""Returns the JSON representation of the model using alias"""
|
62
|
+
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
|
63
|
+
return json.dumps(self.to_dict())
|
64
|
+
|
65
|
+
@classmethod
|
66
|
+
def from_json(cls, json_str: str) -> Optional[Self]:
|
67
|
+
"""Create an instance of ScanResultsV3 from a JSON string"""
|
68
|
+
return cls.from_dict(json.loads(json_str))
|
69
|
+
|
70
|
+
def to_dict(self) -> Dict[str, Any]:
|
71
|
+
"""Return the dictionary representation of the model using alias.
|
72
|
+
|
73
|
+
This has the following differences from calling pydantic's
|
74
|
+
`self.model_dump(by_alias=True)`:
|
75
|
+
|
76
|
+
* `None` is only added to the output dict for nullable fields that
|
77
|
+
were set at model initialization. Other fields with value `None`
|
78
|
+
are ignored.
|
79
|
+
"""
|
80
|
+
excluded_fields: Set[str] = set([
|
81
|
+
])
|
82
|
+
|
83
|
+
_dict = self.model_dump(
|
84
|
+
by_alias=True,
|
85
|
+
exclude=excluded_fields,
|
86
|
+
exclude_none=True,
|
87
|
+
)
|
88
|
+
# override the default output from pydantic by calling `to_dict()` of inventory
|
89
|
+
if self.inventory:
|
90
|
+
_dict['inventory'] = self.inventory.to_dict()
|
91
|
+
# override the default output from pydantic by calling `to_dict()` of each item in file_results (list)
|
92
|
+
_items = []
|
93
|
+
if self.file_results:
|
94
|
+
for _item in self.file_results:
|
95
|
+
if _item:
|
96
|
+
_items.append(_item.to_dict())
|
97
|
+
_dict['file_results'] = _items
|
98
|
+
return _dict
|
99
|
+
|
100
|
+
@classmethod
|
101
|
+
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
102
|
+
"""Create an instance of ScanResultsV3 from a dict"""
|
103
|
+
if obj is None:
|
104
|
+
return None
|
105
|
+
|
106
|
+
if not isinstance(obj, dict):
|
107
|
+
return cls.model_validate(obj)
|
108
|
+
|
109
|
+
_obj = cls.model_validate({
|
110
|
+
"scan_id": obj.get("scan_id"),
|
111
|
+
"start_time": obj.get("start_time"),
|
112
|
+
"end_time": obj.get("end_time"),
|
113
|
+
"status": obj.get("status"),
|
114
|
+
"version": obj.get("version"),
|
115
|
+
"inventory": InventoryV3.from_dict(obj["inventory"]) if obj.get("inventory") is not None else None,
|
116
|
+
"file_results": [FileResultV3.from_dict(_item) for _item in obj["file_results"]] if obj.get("file_results") is not None else None
|
117
|
+
})
|
118
|
+
return _obj
|
119
|
+
|
120
|
+
|
@@ -23,9 +23,9 @@ from typing import Any, ClassVar, Dict, List, Optional
|
|
23
23
|
from typing import Optional, Set
|
24
24
|
from typing_extensions import Self
|
25
25
|
|
26
|
-
class
|
26
|
+
class Sensor(BaseModel):
|
27
27
|
"""
|
28
|
-
|
28
|
+
Sensor
|
29
29
|
""" # noqa: E501
|
30
30
|
sensor_id: StrictStr
|
31
31
|
created_at: datetime
|
@@ -54,7 +54,7 @@ class Model(BaseModel):
|
|
54
54
|
|
55
55
|
@classmethod
|
56
56
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
57
|
-
"""Create an instance of
|
57
|
+
"""Create an instance of Sensor from a JSON string"""
|
58
58
|
return cls.from_dict(json.loads(json_str))
|
59
59
|
|
60
60
|
def to_dict(self) -> Dict[str, Any]:
|
@@ -79,7 +79,7 @@ class Model(BaseModel):
|
|
79
79
|
|
80
80
|
@classmethod
|
81
81
|
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
82
|
-
"""Create an instance of
|
82
|
+
"""Create an instance of Sensor from a dict"""
|
83
83
|
if obj is None:
|
84
84
|
return None
|
85
85
|
|
@@ -19,18 +19,18 @@ import json
|
|
19
19
|
|
20
20
|
from pydantic import BaseModel, ConfigDict, StrictInt
|
21
21
|
from typing import Any, ClassVar, Dict, List
|
22
|
-
from hiddenlayer.sdk.rest.models.
|
22
|
+
from hiddenlayer.sdk.rest.models.sensor import Sensor
|
23
23
|
from typing import Optional, Set
|
24
24
|
from typing_extensions import Self
|
25
25
|
|
26
|
-
class
|
26
|
+
class SensorQueryResponse(BaseModel):
|
27
27
|
"""
|
28
|
-
|
28
|
+
SensorQueryResponse
|
29
29
|
""" # noqa: E501
|
30
30
|
total_count: StrictInt
|
31
31
|
page_size: StrictInt
|
32
32
|
page_number: StrictInt
|
33
|
-
results: List[
|
33
|
+
results: List[Sensor]
|
34
34
|
__properties: ClassVar[List[str]] = ["total_count", "page_size", "page_number", "results"]
|
35
35
|
|
36
36
|
model_config = ConfigDict(
|
@@ -51,7 +51,7 @@ class ModelQueryResponse(BaseModel):
|
|
51
51
|
|
52
52
|
@classmethod
|
53
53
|
def from_json(cls, json_str: str) -> Optional[Self]:
|
54
|
-
"""Create an instance of
|
54
|
+
"""Create an instance of SensorQueryResponse from a JSON string"""
|
55
55
|
return cls.from_dict(json.loads(json_str))
|
56
56
|
|
57
57
|
def to_dict(self) -> Dict[str, Any]:
|
@@ -83,7 +83,7 @@ class ModelQueryResponse(BaseModel):
|
|
83
83
|
|
84
84
|
@classmethod
|
85
85
|
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
|
86
|
-
"""Create an instance of
|
86
|
+
"""Create an instance of SensorQueryResponse from a dict"""
|
87
87
|
if obj is None:
|
88
88
|
return None
|
89
89
|
|
@@ -94,7 +94,7 @@ class ModelQueryResponse(BaseModel):
|
|
94
94
|
"total_count": obj.get("total_count"),
|
95
95
|
"page_size": obj.get("page_size"),
|
96
96
|
"page_number": obj.get("page_number"),
|
97
|
-
"results": [
|
97
|
+
"results": [Sensor.from_dict(_item) for _item in obj["results"]] if obj.get("results") is not None else None
|
98
98
|
})
|
99
99
|
return _obj
|
100
100
|
|
@@ -4,22 +4,29 @@ from typing import Any, Dict, List, Optional, Union
|
|
4
4
|
|
5
5
|
import numpy as np
|
6
6
|
|
7
|
+
from hiddenlayer.sdk.exceptions import SensorDoesNotExistError
|
7
8
|
from hiddenlayer.sdk.rest.api import AidrPredictiveApi
|
9
|
+
from hiddenlayer.sdk.rest.api.sensor_api import SensorApi
|
8
10
|
from hiddenlayer.sdk.rest.api_client import ApiClient
|
9
11
|
from hiddenlayer.sdk.rest.models import (
|
10
12
|
SubmissionResponse,
|
11
13
|
SubmissionV2,
|
12
14
|
)
|
15
|
+
from hiddenlayer.sdk.rest.models.create_sensor_request import CreateSensorRequest
|
16
|
+
from hiddenlayer.sdk.rest.models.sensor import Sensor
|
17
|
+
from hiddenlayer.sdk.rest.models.sensor_sor_query_filter import SensorSORQueryFilter
|
18
|
+
from hiddenlayer.sdk.rest.models.sensor_sor_query_request import SensorSORQueryRequest
|
13
19
|
|
14
20
|
|
15
21
|
class AIDRPredictive:
|
16
22
|
def __init__(self, api_client: ApiClient) -> None:
|
23
|
+
self._sensor_api = SensorApi(api_client=api_client)
|
17
24
|
self._aidr_predictive = AidrPredictiveApi(api_client=api_client)
|
18
25
|
|
19
26
|
def submit_vectors(
|
20
27
|
self,
|
21
28
|
*,
|
22
|
-
|
29
|
+
sensor_id: str,
|
23
30
|
requester_id: str,
|
24
31
|
input_vectors: Union[List[float], np.ndarray],
|
25
32
|
output: Union[List[float], np.ndarray],
|
@@ -31,7 +38,7 @@ class AIDRPredictive:
|
|
31
38
|
"""
|
32
39
|
Submit feature vectors and model outputs via the HiddenLayer API.
|
33
40
|
|
34
|
-
:param
|
41
|
+
:param sensor_id: Sensor id.
|
35
42
|
:param requester_id: Custom identifier for the inbound request. This should be a value that can be used to identify individual users interacting with the model.
|
36
43
|
:param input_vectors: Feature vectors for your model.
|
37
44
|
:param output: Output vectors directly from your model.
|
@@ -60,7 +67,7 @@ class AIDRPredictive:
|
|
60
67
|
SubmissionV2(
|
61
68
|
metadata=metadata if metadata else {},
|
62
69
|
tags=tags if tags else [],
|
63
|
-
sensor_id=
|
70
|
+
sensor_id=sensor_id,
|
64
71
|
requester_id=requester_id,
|
65
72
|
input_layer=input_layer,
|
66
73
|
input_layer_dtype=str(input_vectors.dtype),
|
@@ -74,3 +81,50 @@ class AIDRPredictive:
|
|
74
81
|
else str(datetime.now().isoformat()),
|
75
82
|
)
|
76
83
|
)
|
84
|
+
|
85
|
+
def create_sensor(self, *, sensor_name: str) -> Sensor:
|
86
|
+
"""
|
87
|
+
Creates a sensor in the HiddenLayer Platform.
|
88
|
+
|
89
|
+
:params sensor_name: Name of the sensor
|
90
|
+
|
91
|
+
:returns: HiddenLayer Sensor
|
92
|
+
"""
|
93
|
+
return self._sensor_api.create_sensor(
|
94
|
+
CreateSensorRequest(plaintext_name=sensor_name)
|
95
|
+
)
|
96
|
+
|
97
|
+
def get_sensor(self, *, sensor_name: str) -> Sensor:
|
98
|
+
"""
|
99
|
+
Gets a HiddenLayer sensor object.
|
100
|
+
|
101
|
+
:params sensor_name: Name of the sensor
|
102
|
+
|
103
|
+
:returns: HiddenLayer Sensor
|
104
|
+
"""
|
105
|
+
|
106
|
+
return self._get_sensor_by_name(sensor_name=sensor_name)
|
107
|
+
|
108
|
+
def _get_sensor_by_name(self, *, sensor_name: str) -> Sensor:
|
109
|
+
"""
|
110
|
+
Gets a model sensor by name.
|
111
|
+
|
112
|
+
:param sensor_name: Name of the model.
|
113
|
+
|
114
|
+
:returns: HiddenLayer Model object
|
115
|
+
"""
|
116
|
+
|
117
|
+
sensors = self._sensor_api.query_sensor(
|
118
|
+
sensor_sor_query_request=SensorSORQueryRequest(
|
119
|
+
filter=SensorSORQueryFilter(plaintext_name=sensor_name)
|
120
|
+
)
|
121
|
+
)
|
122
|
+
|
123
|
+
if not sensors.results or len(sensors.results) == 0:
|
124
|
+
msg = f"ModSensorel {sensor_name} does not exist"
|
125
|
+
|
126
|
+
raise SensorDoesNotExistError(msg)
|
127
|
+
|
128
|
+
sensors.results.sort(key=lambda x: x.version, reverse=True)
|
129
|
+
|
130
|
+
return sensors.results[0]
|
@@ -1,26 +1,18 @@
|
|
1
|
-
import json
|
2
1
|
import os
|
3
2
|
import random
|
4
3
|
import tempfile
|
5
4
|
import time
|
6
|
-
import warnings
|
7
5
|
import zipfile
|
8
|
-
from datetime import datetime
|
9
6
|
from pathlib import Path
|
10
7
|
from typing import List, Optional, Union
|
11
|
-
from uuid import uuid4
|
12
|
-
|
13
|
-
from pydantic_core import ValidationError
|
14
8
|
|
15
9
|
from hiddenlayer.sdk.constants import ScanStatus
|
16
|
-
from hiddenlayer.sdk.models import EmptyScanResults,
|
17
|
-
from hiddenlayer.sdk.rest.api import
|
10
|
+
from hiddenlayer.sdk.models import EmptyScanResults, ScanResults
|
11
|
+
from hiddenlayer.sdk.rest.api import ModelSupplyChainApi
|
18
12
|
from hiddenlayer.sdk.rest.api_client import ApiClient
|
19
|
-
from hiddenlayer.sdk.rest.
|
20
|
-
from hiddenlayer.sdk.rest.models
|
21
|
-
from hiddenlayer.sdk.
|
22
|
-
from hiddenlayer.sdk.services.model import ModelAPI
|
23
|
-
from hiddenlayer.sdk.utils import filter_path_objects, is_saas
|
13
|
+
from hiddenlayer.sdk.rest.exceptions import NotFoundException
|
14
|
+
from hiddenlayer.sdk.rest.models import MultiFileUploadRequestV3
|
15
|
+
from hiddenlayer.sdk.utils import filter_path_objects
|
24
16
|
|
25
17
|
EXCLUDE_FILE_TYPES = [
|
26
18
|
"*.txt",
|
@@ -37,22 +29,14 @@ EXCLUDE_FILE_TYPES = [
|
|
37
29
|
class ModelScanAPI:
|
38
30
|
def __init__(self, api_client: ApiClient) -> None:
|
39
31
|
self._api_client = api_client
|
40
|
-
|
41
32
|
self._model_supply_chain_api = ModelSupplyChainApi(api_client=api_client)
|
42
|
-
self._model_api = ModelAPI(api_client=api_client)
|
43
|
-
self._sensor_api = SensorApi(
|
44
|
-
api_client=api_client
|
45
|
-
) # lower level api of ModelAPI
|
46
|
-
|
47
|
-
self._model_scan_api = ModelScanApi(api_client=api_client)
|
48
33
|
|
49
34
|
def scan_file(
|
50
35
|
self,
|
51
36
|
*,
|
52
37
|
model_name: str,
|
53
38
|
model_path: Union[str, os.PathLike],
|
54
|
-
model_version:
|
55
|
-
chunk_size: int = 16,
|
39
|
+
model_version: str = "1",
|
56
40
|
wait_for_results: bool = True,
|
57
41
|
) -> ScanResults:
|
58
42
|
"""
|
@@ -69,52 +53,22 @@ class ModelScanAPI:
|
|
69
53
|
|
70
54
|
file_path = Path(model_path)
|
71
55
|
|
72
|
-
|
73
|
-
|
74
|
-
|
56
|
+
request = MultiFileUploadRequestV3(
|
57
|
+
model_name=model_name,
|
58
|
+
model_version=model_version,
|
59
|
+
requesting_entity="hiddenlayer-python-sdk",
|
75
60
|
)
|
76
|
-
|
77
|
-
|
78
|
-
with open(file_path, "rb") as f:
|
79
|
-
for i in range(0, len(upload.parts), chunk_size):
|
80
|
-
group: List[MultipartUploadPart] = upload.parts[i : i + chunk_size]
|
81
|
-
for part in group:
|
82
|
-
read_amount = part.end_offset - part.start_offset
|
83
|
-
f.seek(int(part.start_offset))
|
84
|
-
part_data = f.read(int(read_amount))
|
85
|
-
|
86
|
-
# The SaaS multipart upload returns a upload url for each part
|
87
|
-
# So there is no specified route
|
88
|
-
self._api_client.call_api(
|
89
|
-
"PUT",
|
90
|
-
part.upload_url,
|
91
|
-
body=part_data,
|
92
|
-
header_params={"Content-Type": "application/octet-binary"},
|
93
|
-
)
|
94
|
-
|
95
|
-
self._sensor_api.complete_multipart_upload(sensor.sensor_id, upload.upload_id)
|
96
|
-
|
97
|
-
self._model_scan_api.scan_model(sensor.sensor_id)
|
98
|
-
|
99
|
-
scan_results = self.get_scan_results(
|
100
|
-
model_name=model_name, model_version=model_version
|
61
|
+
response = self._model_supply_chain_api.begin_multi_file_upload(
|
62
|
+
multi_file_upload_request_v3=request
|
101
63
|
)
|
64
|
+
scan_id = response.scan_id
|
65
|
+
if scan_id is None:
|
66
|
+
raise Exception("scan_id must have a value")
|
102
67
|
|
103
|
-
|
104
|
-
retries = 0
|
105
|
-
if wait_for_results:
|
106
|
-
print(f"{file_path.name} scan status: {scan_results.status}")
|
107
|
-
while scan_results.status not in [ScanStatus.DONE, ScanStatus.FAILED]:
|
108
|
-
retries += 1
|
109
|
-
delay = base_delay * 2**retries + random.uniform(
|
110
|
-
0, 1
|
111
|
-
) # exponential back off retry
|
112
|
-
time.sleep(delay)
|
113
|
-
scan_results = self.get_scan_results(
|
114
|
-
model_name=model_name, model_version=model_version
|
115
|
-
)
|
116
|
-
print(f"{file_path.name} scan status: {scan_results.status}")
|
68
|
+
self._scan_file(scan_id=scan_id, file_path=file_path)
|
117
69
|
|
70
|
+
self._model_supply_chain_api.complete_multi_file_upload(scan_id=scan_id)
|
71
|
+
scan_results = self._wait_for_scan_results(scan_id=scan_id)
|
118
72
|
scan_results.file_name = file_path.name
|
119
73
|
scan_results.file_path = str(file_path)
|
120
74
|
|
@@ -126,9 +80,8 @@ class ModelScanAPI:
|
|
126
80
|
model_name: str,
|
127
81
|
bucket: str,
|
128
82
|
key: str,
|
129
|
-
model_version:
|
83
|
+
model_version: str = "1",
|
130
84
|
s3_client: Optional[object] = None,
|
131
|
-
chunk_size: int = 4,
|
132
85
|
wait_for_results: bool = True,
|
133
86
|
) -> ScanResults:
|
134
87
|
"""
|
@@ -173,7 +126,6 @@ class ModelScanAPI:
|
|
173
126
|
model_path=f"/tmp/{file_name}",
|
174
127
|
model_name=model_name,
|
175
128
|
model_version=model_version,
|
176
|
-
chunk_size=chunk_size,
|
177
129
|
wait_for_results=wait_for_results,
|
178
130
|
)
|
179
131
|
|
@@ -184,10 +136,9 @@ class ModelScanAPI:
|
|
184
136
|
account_url: str,
|
185
137
|
container: str,
|
186
138
|
blob: str,
|
187
|
-
model_version:
|
139
|
+
model_version: str = "1",
|
188
140
|
blob_service_client: Optional[object] = None,
|
189
141
|
credential: Optional[object] = None,
|
190
|
-
chunk_size: int = 4,
|
191
142
|
wait_for_results: bool = True,
|
192
143
|
) -> ScanResults:
|
193
144
|
"""
|
@@ -253,7 +204,6 @@ class ModelScanAPI:
|
|
253
204
|
model_path=f"/tmp/{file_name}",
|
254
205
|
model_name=model_name,
|
255
206
|
model_version=model_version,
|
256
|
-
chunk_size=chunk_size,
|
257
207
|
wait_for_results=wait_for_results,
|
258
208
|
)
|
259
209
|
|
@@ -270,8 +220,6 @@ class ModelScanAPI:
|
|
270
220
|
ignore_file_patterns: Optional[List[str]] = None,
|
271
221
|
force_download: bool = False,
|
272
222
|
hf_token: Optional[Union[str, bool]] = None,
|
273
|
-
# HL parameters
|
274
|
-
chunk_size: int = 4,
|
275
223
|
wait_for_results: bool = True,
|
276
224
|
) -> ScanResults:
|
277
225
|
"""
|
@@ -317,21 +265,19 @@ class ModelScanAPI:
|
|
317
265
|
token=hf_token,
|
318
266
|
)
|
319
267
|
|
268
|
+
if revision is None:
|
269
|
+
revision = "1"
|
270
|
+
|
320
271
|
return self.scan_folder(
|
321
272
|
model_name=model_name or repo_id,
|
273
|
+
model_version=revision,
|
322
274
|
path=local_dir,
|
323
275
|
allow_file_patterns=allow_file_patterns,
|
324
276
|
ignore_file_patterns=ignore_file_patterns,
|
325
|
-
chunk_size=chunk_size,
|
326
277
|
wait_for_results=wait_for_results,
|
327
278
|
)
|
328
279
|
|
329
|
-
def get_scan_results(
|
330
|
-
self,
|
331
|
-
*,
|
332
|
-
model_name: str,
|
333
|
-
model_version: Optional[int] = None,
|
334
|
-
) -> ScanResults:
|
280
|
+
def get_scan_results(self, *, scan_id: str) -> ScanResults:
|
335
281
|
"""
|
336
282
|
Get results from a model scan.
|
337
283
|
|
@@ -341,48 +287,17 @@ class ModelScanAPI:
|
|
341
287
|
:returns: Scan results.
|
342
288
|
"""
|
343
289
|
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
model_id = response.results[0].model_id
|
348
|
-
|
349
|
-
scans = self._model_supply_chain_api.model_scan_api_v3_scan_query(
|
350
|
-
model_ids=[model_id], latest_per_model_version_only=True
|
351
|
-
)
|
352
|
-
if scans.total == 0:
|
353
|
-
return EmptyScanResults()
|
354
|
-
|
355
|
-
if scans.items is None:
|
356
|
-
return EmptyScanResults()
|
357
|
-
|
358
|
-
scan = scans.items[0]
|
359
|
-
if model_version:
|
360
|
-
scan = next(
|
361
|
-
(
|
362
|
-
s
|
363
|
-
for s in scans.items
|
364
|
-
if s.inventory.model_version == str(model_version)
|
365
|
-
),
|
366
|
-
None,
|
367
|
-
)
|
368
|
-
if not scan:
|
290
|
+
try:
|
291
|
+
scan_report = self._model_supply_chain_api.get_scan_results(scan_id)
|
292
|
+
except NotFoundException:
|
369
293
|
return EmptyScanResults()
|
370
294
|
|
371
|
-
|
372
|
-
self._model_supply_chain_api.model_scan_api_v3_scan_model_version_id_get(
|
373
|
-
scan.scan_id
|
374
|
-
)
|
375
|
-
)
|
376
|
-
|
377
|
-
return ScanResults.from_scanreportv3(
|
378
|
-
scan_report_v3=scan_report, model_id=model_id
|
379
|
-
)
|
295
|
+
return ScanResults.from_scanreportv3(scan_report_v3=scan_report)
|
380
296
|
|
381
297
|
def get_sarif_results(
|
382
298
|
self,
|
383
299
|
*,
|
384
|
-
|
385
|
-
model_version: Optional[int] = None,
|
300
|
+
scan_id: str,
|
386
301
|
) -> Optional[str]:
|
387
302
|
"""
|
388
303
|
Get sarif results from a model scan.
|
@@ -392,34 +307,31 @@ class ModelScanAPI:
|
|
392
307
|
|
393
308
|
:returns: Scan results.
|
394
309
|
"""
|
395
|
-
scan = self.get_scan_results(model_name=model_name, model_version=model_version)
|
396
|
-
if scan.scan_id == "":
|
397
|
-
return None
|
398
310
|
|
399
311
|
# Unfortunately, the generated code for the API doesn't directly support modifying the Accept header
|
400
312
|
# in order to enable us to get the Sarif results
|
401
313
|
# Here we will reach in to the request serialization process. The 2nd element in the tuple is the headers
|
402
314
|
# where we will modify the Accept header to application/sarif+json
|
403
|
-
request = self._model_supply_chain_api.
|
404
|
-
|
315
|
+
request = self._model_supply_chain_api._get_scan_results_serialize(
|
316
|
+
scan_id, None, None, None, None, 0
|
405
317
|
)
|
406
318
|
request[2]["Accept"] = "application/sarif+json"
|
407
319
|
response = self._api_client.call_api(*request)
|
408
320
|
response.read()
|
409
321
|
|
410
|
-
|
411
|
-
|
412
|
-
|
322
|
+
if response.data is None:
|
323
|
+
return None
|
324
|
+
|
325
|
+
return response.data.decode()
|
413
326
|
|
414
327
|
def scan_folder(
|
415
328
|
self,
|
416
329
|
*,
|
417
330
|
model_name: str,
|
418
331
|
path: Union[str, os.PathLike],
|
419
|
-
model_version:
|
332
|
+
model_version: str = "1",
|
420
333
|
allow_file_patterns: Optional[List[str]] = None,
|
421
334
|
ignore_file_patterns: Optional[List[str]] = None,
|
422
|
-
chunk_size: int = 4,
|
423
335
|
wait_for_results: bool = True,
|
424
336
|
) -> ScanResults:
|
425
337
|
"""
|
@@ -437,7 +349,18 @@ class ModelScanAPI:
|
|
437
349
|
"""
|
438
350
|
|
439
351
|
model_path = Path(path)
|
440
|
-
|
352
|
+
|
353
|
+
request = MultiFileUploadRequestV3(
|
354
|
+
model_name=model_name,
|
355
|
+
model_version=model_version,
|
356
|
+
requesting_entity="hiddenlayer-python-sdk",
|
357
|
+
)
|
358
|
+
response = self._model_supply_chain_api.begin_multi_file_upload(
|
359
|
+
multi_file_upload_request_v3=request
|
360
|
+
)
|
361
|
+
scan_id = response.scan_id
|
362
|
+
if scan_id is None:
|
363
|
+
raise Exception("scan_id must have a value")
|
441
364
|
|
442
365
|
ignore_file_patterns = (
|
443
366
|
EXCLUDE_FILE_TYPES + ignore_file_patterns
|
@@ -451,14 +374,54 @@ class ModelScanAPI:
|
|
451
374
|
ignore_patterns=ignore_file_patterns,
|
452
375
|
)
|
453
376
|
|
454
|
-
|
455
|
-
|
456
|
-
zipf.write(file, os.path.relpath(file, model_path))
|
377
|
+
for file in files:
|
378
|
+
self._scan_file(scan_id=scan_id, file_path=Path(file))
|
457
379
|
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
380
|
+
self._model_supply_chain_api.complete_multi_file_upload(scan_id=scan_id)
|
381
|
+
scan_results = self._wait_for_scan_results(scan_id=scan_id)
|
382
|
+
|
383
|
+
return scan_results
|
384
|
+
|
385
|
+
def _scan_file(self, *, scan_id: str, file_path: Path):
|
386
|
+
filesize = file_path.stat().st_size
|
387
|
+
upload = self._model_supply_chain_api.begin_multipart_file_upload(
|
388
|
+
scan_id=str(scan_id), file_name=str(file_path), file_content_length=filesize
|
464
389
|
)
|
390
|
+
|
391
|
+
with open(file_path, "rb") as f:
|
392
|
+
for part in upload.parts:
|
393
|
+
if part.start_offset is None:
|
394
|
+
raise Exception("part must have a start_offset")
|
395
|
+
if part.stop_offset is not None:
|
396
|
+
read_amount = part.stop_offset - part.start_offset
|
397
|
+
else:
|
398
|
+
read_amount = None
|
399
|
+
f.seek(part.start_offset)
|
400
|
+
part_data = f.read(read_amount)
|
401
|
+
self._api_client.call_api(
|
402
|
+
"PUT",
|
403
|
+
part.upload_url,
|
404
|
+
body=part_data,
|
405
|
+
header_params={"Content-Type": "application/octet-binary"},
|
406
|
+
)
|
407
|
+
|
408
|
+
self._model_supply_chain_api.complete_multipart_file_upload(
|
409
|
+
scan_id=scan_id, file_id=upload.upload_id
|
410
|
+
)
|
411
|
+
|
412
|
+
def _wait_for_scan_results(self, *, scan_id: str):
|
413
|
+
scan_results = self.get_scan_results(scan_id=scan_id)
|
414
|
+
|
415
|
+
base_delay = 0.1 # seconds
|
416
|
+
retries = 0
|
417
|
+
print(f"scan status: {scan_results.status}")
|
418
|
+
while scan_results.status not in [ScanStatus.DONE, ScanStatus.FAILED]:
|
419
|
+
retries += 1
|
420
|
+
delay = base_delay * 2**retries + random.uniform(
|
421
|
+
0, 1
|
422
|
+
) # exponential back off retry
|
423
|
+
time.sleep(delay)
|
424
|
+
scan_results = self.get_scan_results(scan_id=scan_id)
|
425
|
+
print(f"scan status: {scan_results.status}")
|
426
|
+
|
427
|
+
return scan_results
|
hiddenlayer/sdk/version.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
VERSION = "
|
1
|
+
VERSION = "2.0.1"
|