hiddenlayer-sdk 2.0.9__py3-none-any.whl → 3.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.
- hiddenlayer/__init__.py +109 -105
- hiddenlayer/_base_client.py +1995 -0
- hiddenlayer/_client.py +761 -0
- hiddenlayer/_compat.py +219 -0
- hiddenlayer/_constants.py +14 -0
- hiddenlayer/_exceptions.py +108 -0
- hiddenlayer/_files.py +123 -0
- hiddenlayer/_models.py +835 -0
- hiddenlayer/_oauth2.py +118 -0
- hiddenlayer/_qs.py +150 -0
- hiddenlayer/_resource.py +43 -0
- hiddenlayer/_response.py +832 -0
- hiddenlayer/_streaming.py +333 -0
- hiddenlayer/_types.py +260 -0
- hiddenlayer/_utils/__init__.py +64 -0
- hiddenlayer/_utils/_compat.py +45 -0
- hiddenlayer/_utils/_datetime_parse.py +136 -0
- hiddenlayer/_utils/_logs.py +25 -0
- hiddenlayer/_utils/_proxy.py +65 -0
- hiddenlayer/_utils/_reflection.py +42 -0
- hiddenlayer/_utils/_resources_proxy.py +24 -0
- hiddenlayer/_utils/_streams.py +12 -0
- hiddenlayer/_utils/_sync.py +86 -0
- hiddenlayer/_utils/_transform.py +457 -0
- hiddenlayer/_utils/_typing.py +156 -0
- hiddenlayer/_utils/_utils.py +421 -0
- hiddenlayer/_version.py +4 -0
- hiddenlayer/lib/.keep +4 -0
- hiddenlayer/lib/__init__.py +6 -0
- hiddenlayer/lib/community_scan.py +174 -0
- hiddenlayer/lib/model_scan.py +752 -0
- hiddenlayer/lib/scan_utils.py +142 -0
- hiddenlayer/pagination.py +127 -0
- hiddenlayer/resources/__init__.py +75 -0
- hiddenlayer/resources/interactions.py +205 -0
- hiddenlayer/resources/models/__init__.py +33 -0
- hiddenlayer/resources/models/cards.py +259 -0
- hiddenlayer/resources/models/models.py +284 -0
- hiddenlayer/resources/prompt_analyzer.py +207 -0
- hiddenlayer/resources/scans/__init__.py +61 -0
- hiddenlayer/resources/scans/jobs.py +499 -0
- hiddenlayer/resources/scans/results.py +169 -0
- hiddenlayer/resources/scans/scans.py +166 -0
- hiddenlayer/resources/scans/upload/__init__.py +33 -0
- hiddenlayer/resources/scans/upload/file.py +279 -0
- hiddenlayer/resources/scans/upload/upload.py +340 -0
- hiddenlayer/resources/sensors.py +575 -0
- hiddenlayer/types/__init__.py +16 -0
- hiddenlayer/types/interaction_analyze_params.py +62 -0
- hiddenlayer/types/interaction_analyze_response.py +199 -0
- hiddenlayer/types/model_retrieve_response.py +50 -0
- hiddenlayer/types/models/__init__.py +6 -0
- hiddenlayer/types/models/card_list_params.py +65 -0
- hiddenlayer/types/models/card_list_response.py +50 -0
- hiddenlayer/types/prompt_analyzer_create_params.py +23 -0
- hiddenlayer/types/prompt_analyzer_create_response.py +381 -0
- hiddenlayer/types/scans/__init__.py +14 -0
- hiddenlayer/types/scans/job_list_params.py +75 -0
- hiddenlayer/types/scans/job_list_response.py +22 -0
- hiddenlayer/types/scans/job_request_params.py +49 -0
- hiddenlayer/types/scans/job_retrieve_params.py +16 -0
- hiddenlayer/types/scans/result_sarif_response.py +7 -0
- hiddenlayer/types/scans/scan_job.py +46 -0
- hiddenlayer/types/scans/scan_report.py +367 -0
- hiddenlayer/types/scans/upload/__init__.py +6 -0
- hiddenlayer/types/scans/upload/file_add_response.py +24 -0
- hiddenlayer/types/scans/upload/file_complete_response.py +12 -0
- hiddenlayer/types/scans/upload_complete_all_response.py +12 -0
- hiddenlayer/types/scans/upload_start_params.py +34 -0
- hiddenlayer/types/scans/upload_start_response.py +12 -0
- hiddenlayer/types/sensor_create_params.py +24 -0
- hiddenlayer/types/sensor_create_response.py +33 -0
- hiddenlayer/types/sensor_query_params.py +39 -0
- hiddenlayer/types/sensor_query_response.py +43 -0
- hiddenlayer/types/sensor_retrieve_response.py +33 -0
- hiddenlayer/types/sensor_update_params.py +20 -0
- hiddenlayer/types/sensor_update_response.py +9 -0
- hiddenlayer_sdk-3.0.0.dist-info/METADATA +431 -0
- hiddenlayer_sdk-3.0.0.dist-info/RECORD +82 -0
- {hiddenlayer_sdk-2.0.9.dist-info → hiddenlayer_sdk-3.0.0.dist-info}/WHEEL +1 -2
- {hiddenlayer_sdk-2.0.9.dist-info → hiddenlayer_sdk-3.0.0.dist-info}/licenses/LICENSE +1 -1
- hiddenlayer/sdk/constants.py +0 -26
- hiddenlayer/sdk/exceptions.py +0 -12
- hiddenlayer/sdk/models.py +0 -58
- hiddenlayer/sdk/rest/__init__.py +0 -135
- hiddenlayer/sdk/rest/api/__init__.py +0 -10
- hiddenlayer/sdk/rest/api/aidr_predictive_api.py +0 -308
- hiddenlayer/sdk/rest/api/health_api.py +0 -272
- hiddenlayer/sdk/rest/api/model_api.py +0 -559
- hiddenlayer/sdk/rest/api/model_supply_chain_api.py +0 -4063
- hiddenlayer/sdk/rest/api/readiness_api.py +0 -272
- hiddenlayer/sdk/rest/api/sensor_api.py +0 -1432
- hiddenlayer/sdk/rest/api_client.py +0 -770
- hiddenlayer/sdk/rest/api_response.py +0 -21
- hiddenlayer/sdk/rest/configuration.py +0 -445
- hiddenlayer/sdk/rest/exceptions.py +0 -199
- hiddenlayer/sdk/rest/models/__init__.py +0 -113
- hiddenlayer/sdk/rest/models/address.py +0 -110
- hiddenlayer/sdk/rest/models/artifact.py +0 -155
- hiddenlayer/sdk/rest/models/artifact_change.py +0 -108
- hiddenlayer/sdk/rest/models/artifact_content.py +0 -101
- hiddenlayer/sdk/rest/models/artifact_location.py +0 -109
- hiddenlayer/sdk/rest/models/attachment.py +0 -129
- hiddenlayer/sdk/rest/models/begin_multi_file_upload200_response.py +0 -87
- hiddenlayer/sdk/rest/models/begin_multipart_file_upload200_response.py +0 -97
- hiddenlayer/sdk/rest/models/begin_multipart_file_upload200_response_parts_inner.py +0 -94
- hiddenlayer/sdk/rest/models/code_flow.py +0 -113
- hiddenlayer/sdk/rest/models/configuration_override.py +0 -108
- hiddenlayer/sdk/rest/models/conversion.py +0 -114
- hiddenlayer/sdk/rest/models/create_sensor_request.py +0 -95
- hiddenlayer/sdk/rest/models/edge.py +0 -108
- hiddenlayer/sdk/rest/models/edge_traversal.py +0 -122
- hiddenlayer/sdk/rest/models/errors_inner.py +0 -91
- hiddenlayer/sdk/rest/models/exception.py +0 -113
- hiddenlayer/sdk/rest/models/external_properties.py +0 -273
- hiddenlayer/sdk/rest/models/external_property_file_reference.py +0 -102
- hiddenlayer/sdk/rest/models/external_property_file_references.py +0 -240
- hiddenlayer/sdk/rest/models/file_details_v3.py +0 -139
- hiddenlayer/sdk/rest/models/file_result_v3.py +0 -117
- hiddenlayer/sdk/rest/models/file_scan_report_v3.py +0 -132
- hiddenlayer/sdk/rest/models/file_scan_reports_v3.py +0 -95
- hiddenlayer/sdk/rest/models/fix.py +0 -113
- hiddenlayer/sdk/rest/models/get_condensed_model_scan_reports200_response.py +0 -102
- hiddenlayer/sdk/rest/models/graph.py +0 -123
- hiddenlayer/sdk/rest/models/graph_traversal.py +0 -97
- hiddenlayer/sdk/rest/models/inventory_v3.py +0 -101
- hiddenlayer/sdk/rest/models/invocation.py +0 -199
- hiddenlayer/sdk/rest/models/location.py +0 -146
- hiddenlayer/sdk/rest/models/location_inner.py +0 -138
- hiddenlayer/sdk/rest/models/location_relationship.py +0 -107
- hiddenlayer/sdk/rest/models/logical_location.py +0 -104
- hiddenlayer/sdk/rest/models/message.py +0 -92
- hiddenlayer/sdk/rest/models/mitre_atlas_inner.py +0 -110
- hiddenlayer/sdk/rest/models/model.py +0 -103
- hiddenlayer/sdk/rest/models/model_inventory_info.py +0 -103
- hiddenlayer/sdk/rest/models/model_version.py +0 -97
- hiddenlayer/sdk/rest/models/multi_file_upload_request_v3.py +0 -97
- hiddenlayer/sdk/rest/models/multiformat_message_string.py +0 -95
- hiddenlayer/sdk/rest/models/node.py +0 -122
- hiddenlayer/sdk/rest/models/notification.py +0 -157
- hiddenlayer/sdk/rest/models/notify_model_scan_completed200_response.py +0 -87
- hiddenlayer/sdk/rest/models/paged_response_with_total.py +0 -94
- hiddenlayer/sdk/rest/models/pagination_v3.py +0 -95
- hiddenlayer/sdk/rest/models/physical_location.py +0 -94
- hiddenlayer/sdk/rest/models/problem_details.py +0 -103
- hiddenlayer/sdk/rest/models/property_bag.py +0 -101
- hiddenlayer/sdk/rest/models/rectangle.py +0 -110
- hiddenlayer/sdk/rest/models/region.py +0 -127
- hiddenlayer/sdk/rest/models/replacement.py +0 -103
- hiddenlayer/sdk/rest/models/reporting_configuration.py +0 -113
- hiddenlayer/sdk/rest/models/reporting_descriptor.py +0 -162
- hiddenlayer/sdk/rest/models/reporting_descriptor_reference.py +0 -103
- hiddenlayer/sdk/rest/models/reporting_descriptor_relationship.py +0 -115
- hiddenlayer/sdk/rest/models/result.py +0 -312
- hiddenlayer/sdk/rest/models/result_provenance.py +0 -133
- hiddenlayer/sdk/rest/models/rule_details_inner.py +0 -102
- hiddenlayer/sdk/rest/models/run.py +0 -318
- hiddenlayer/sdk/rest/models/run_automation_details.py +0 -129
- hiddenlayer/sdk/rest/models/sarif210.py +0 -123
- hiddenlayer/sdk/rest/models/scan_create_request.py +0 -87
- hiddenlayer/sdk/rest/models/scan_detection_v3.py +0 -159
- hiddenlayer/sdk/rest/models/scan_detection_v31.py +0 -158
- hiddenlayer/sdk/rest/models/scan_header_v3.py +0 -129
- hiddenlayer/sdk/rest/models/scan_job.py +0 -115
- hiddenlayer/sdk/rest/models/scan_job_access.py +0 -97
- hiddenlayer/sdk/rest/models/scan_model_details_v3.py +0 -99
- hiddenlayer/sdk/rest/models/scan_model_details_v31.py +0 -97
- hiddenlayer/sdk/rest/models/scan_model_ids_v3.py +0 -89
- hiddenlayer/sdk/rest/models/scan_report_v3.py +0 -139
- hiddenlayer/sdk/rest/models/scan_results_map_v3.py +0 -105
- hiddenlayer/sdk/rest/models/scan_results_v3.py +0 -120
- hiddenlayer/sdk/rest/models/security_posture.py +0 -89
- hiddenlayer/sdk/rest/models/sensor.py +0 -100
- hiddenlayer/sdk/rest/models/sensor_query_response.py +0 -101
- hiddenlayer/sdk/rest/models/sensor_sor_model_card_query_response.py +0 -101
- hiddenlayer/sdk/rest/models/sensor_sor_model_card_response.py +0 -127
- hiddenlayer/sdk/rest/models/sensor_sor_query_filter.py +0 -108
- hiddenlayer/sdk/rest/models/sensor_sor_query_request.py +0 -109
- hiddenlayer/sdk/rest/models/special_locations.py +0 -97
- hiddenlayer/sdk/rest/models/stack.py +0 -113
- hiddenlayer/sdk/rest/models/stack_frame.py +0 -104
- hiddenlayer/sdk/rest/models/submission_response.py +0 -95
- hiddenlayer/sdk/rest/models/submission_v2.py +0 -109
- hiddenlayer/sdk/rest/models/suppression.py +0 -133
- hiddenlayer/sdk/rest/models/thread_flow.py +0 -144
- hiddenlayer/sdk/rest/models/thread_flow_location.py +0 -166
- hiddenlayer/sdk/rest/models/tool.py +0 -107
- hiddenlayer/sdk/rest/models/tool_component.py +0 -251
- hiddenlayer/sdk/rest/models/tool_component_reference.py +0 -108
- hiddenlayer/sdk/rest/models/translation_metadata.py +0 -110
- hiddenlayer/sdk/rest/models/validation_error_model.py +0 -99
- hiddenlayer/sdk/rest/models/version_control_details.py +0 -108
- hiddenlayer/sdk/rest/models/web_request.py +0 -112
- hiddenlayer/sdk/rest/models/web_response.py +0 -112
- hiddenlayer/sdk/rest/rest.py +0 -257
- hiddenlayer/sdk/services/__init__.py +0 -0
- hiddenlayer/sdk/services/aidr_predictive.py +0 -130
- hiddenlayer/sdk/services/model_scan.py +0 -505
- hiddenlayer/sdk/utils.py +0 -92
- hiddenlayer/sdk/version.py +0 -1
- hiddenlayer_sdk-2.0.9.dist-info/METADATA +0 -368
- hiddenlayer_sdk-2.0.9.dist-info/RECORD +0 -126
- hiddenlayer_sdk-2.0.9.dist-info/top_level.txt +0 -1
- /hiddenlayer/{sdk/__init__.py → py.typed} +0 -0
@@ -0,0 +1,752 @@
|
|
1
|
+
"""
|
2
|
+
Model scanning functionality for Hidden Layer SDK.
|
3
|
+
|
4
|
+
This module provides the model scanning methods that were available in the old SDK,
|
5
|
+
including scan_file and scan_folder methods with multipart upload functionality.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import os
|
9
|
+
import logging
|
10
|
+
from typing import List, Union, Literal, Optional, Generator, cast
|
11
|
+
from fnmatch import fnmatch
|
12
|
+
from pathlib import Path
|
13
|
+
from typing_extensions import TYPE_CHECKING
|
14
|
+
|
15
|
+
import httpx
|
16
|
+
|
17
|
+
from .scan_utils import get_scan_results, wait_for_scan_results, get_scan_results_async, wait_for_scan_results_async
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
if TYPE_CHECKING:
|
22
|
+
from .. import HiddenLayer, AsyncHiddenLayer
|
23
|
+
from ..types.scans import ScanReport
|
24
|
+
|
25
|
+
# Exclude patterns matching the old SDK
|
26
|
+
EXCLUDE_FILE_TYPES = [
|
27
|
+
"*.txt",
|
28
|
+
"*.md",
|
29
|
+
"*.lock",
|
30
|
+
".gitattributes",
|
31
|
+
".git",
|
32
|
+
".git/*",
|
33
|
+
"*/.git",
|
34
|
+
"**/.git/**",
|
35
|
+
]
|
36
|
+
|
37
|
+
PathInputType = Union[str, os.PathLike[str]]
|
38
|
+
|
39
|
+
|
40
|
+
def filter_path_objects(
|
41
|
+
items: Union[List[PathInputType], Generator[PathInputType, None, None]],
|
42
|
+
*,
|
43
|
+
allow_patterns: Optional[Union[List[str], str]] = None,
|
44
|
+
ignore_patterns: Optional[Union[List[str], str]] = None,
|
45
|
+
) -> Generator[Union[str, os.PathLike[str]], None, None]:
|
46
|
+
"""Filter path objects based on an allowlist and a denylist.
|
47
|
+
|
48
|
+
Input must be a list of paths (`str` or `Path`) or a generator of paths.
|
49
|
+
|
50
|
+
Patterns are Unix shell-style wildcards which are NOT regular expressions. See
|
51
|
+
https://docs.python.org/3/library/fnmatch.html for more details.
|
52
|
+
|
53
|
+
:param items: List of paths to filter.
|
54
|
+
:param allow_patterns: Patterns constituting the allowlist. If provided, item paths must match at
|
55
|
+
least one pattern from the allowlist.
|
56
|
+
:param ignore_patterns: Patterns constituting the denylist. If provided, item paths must not match
|
57
|
+
any patterns from the denylist.
|
58
|
+
|
59
|
+
:returns: Filtered list of objects, as a generator.
|
60
|
+
"""
|
61
|
+
if isinstance(allow_patterns, str):
|
62
|
+
allow_patterns = [allow_patterns]
|
63
|
+
|
64
|
+
if isinstance(ignore_patterns, str):
|
65
|
+
ignore_patterns = [ignore_patterns]
|
66
|
+
|
67
|
+
def _identity(item: Union[str, os.PathLike[str]]) -> Path:
|
68
|
+
if isinstance(item, str):
|
69
|
+
return Path(item)
|
70
|
+
if isinstance(item, Path):
|
71
|
+
return item
|
72
|
+
raise ValueError("Objects must be string or Pathlike.")
|
73
|
+
|
74
|
+
key = _identity # Items must be `str` or `Path`, otherwise raise ValueError
|
75
|
+
|
76
|
+
for item in items:
|
77
|
+
path: Path = key(item)
|
78
|
+
|
79
|
+
if path.is_dir():
|
80
|
+
continue
|
81
|
+
|
82
|
+
# Skip if there's an allowlist and path doesn't match any
|
83
|
+
if allow_patterns is not None and not any(fnmatch(str(path), r) for r in allow_patterns):
|
84
|
+
continue
|
85
|
+
|
86
|
+
# Skip if there's a denylist and path matches any
|
87
|
+
if ignore_patterns is not None and any(fnmatch(str(path), r) for r in ignore_patterns):
|
88
|
+
continue
|
89
|
+
|
90
|
+
yield item
|
91
|
+
|
92
|
+
|
93
|
+
class ModelScanner:
|
94
|
+
"""
|
95
|
+
Model scanner that provides file and folder scanning functionality.
|
96
|
+
|
97
|
+
This class extends the generated SDK to provide the same functionality as the old SDK's
|
98
|
+
ModelScanAPI, including multipart upload functionality for files and folders.
|
99
|
+
"""
|
100
|
+
|
101
|
+
def __init__(self, client: "HiddenLayer") -> None:
|
102
|
+
self._client = client
|
103
|
+
|
104
|
+
def scan_file(
|
105
|
+
self,
|
106
|
+
*,
|
107
|
+
model_name: str,
|
108
|
+
model_path: Union[str, os.PathLike[str]],
|
109
|
+
model_version: str = "1",
|
110
|
+
wait_for_results: bool = True,
|
111
|
+
request_source: str = "API Upload",
|
112
|
+
origin: str = "",
|
113
|
+
) -> "ScanReport":
|
114
|
+
"""
|
115
|
+
Scan a local model file using the HiddenLayer Model Scanner.
|
116
|
+
|
117
|
+
:param model_name: Name of the model to be shown on the HiddenLayer UI
|
118
|
+
:param model_path: Local path to the model file.
|
119
|
+
:param model_version: Version of the model to be shown on the HiddenLayer UI.
|
120
|
+
:param wait_for_results: True whether to wait for the scan to finish, defaults to True.
|
121
|
+
:param request_source: Source that requested the scan.
|
122
|
+
:param origin: Origin platform where the model came from.
|
123
|
+
|
124
|
+
:returns: Scan Results
|
125
|
+
"""
|
126
|
+
file_path = Path(model_path)
|
127
|
+
|
128
|
+
# Start the upload
|
129
|
+
upload_response = self._client.scans.upload.start(
|
130
|
+
model_name=model_name,
|
131
|
+
model_version=model_version,
|
132
|
+
requesting_entity="hiddenlayer-python-sdk",
|
133
|
+
request_source=cast("Literal['Hybrid Upload', 'API Upload', 'Integration', 'UI Upload']", request_source),
|
134
|
+
origin=origin,
|
135
|
+
)
|
136
|
+
|
137
|
+
scan_id = upload_response.scan_id
|
138
|
+
if scan_id is None:
|
139
|
+
raise ValueError("scan_id must have a value")
|
140
|
+
|
141
|
+
# Upload the file
|
142
|
+
self._scan_file(scan_id=scan_id, file_path=file_path)
|
143
|
+
|
144
|
+
# Complete the upload
|
145
|
+
self._client.scans.upload.complete_all(scan_id=scan_id)
|
146
|
+
|
147
|
+
if wait_for_results:
|
148
|
+
scan_results = wait_for_scan_results(self._client, scan_id=scan_id)
|
149
|
+
else:
|
150
|
+
scan_results = get_scan_results(self._client, scan_id=scan_id)
|
151
|
+
|
152
|
+
return scan_results
|
153
|
+
|
154
|
+
def scan_folder(
|
155
|
+
self,
|
156
|
+
*,
|
157
|
+
model_name: str,
|
158
|
+
path: Union[str, os.PathLike[str]],
|
159
|
+
model_version: str = "1",
|
160
|
+
allow_file_patterns: Optional[List[str]] = None,
|
161
|
+
ignore_file_patterns: Optional[List[str]] = None,
|
162
|
+
wait_for_results: bool = True,
|
163
|
+
request_source: str = "API Upload",
|
164
|
+
origin: str = "",
|
165
|
+
) -> "ScanReport":
|
166
|
+
"""
|
167
|
+
Submits all files in a directory and its sub directories to be scanned.
|
168
|
+
|
169
|
+
:param model_name: Name of the model to be shown on the HiddenLayer UI.
|
170
|
+
:param path: Path to the folder on disk to be scanned.
|
171
|
+
:param model_version: Version of the model to be shown on the HiddenLayer UI.
|
172
|
+
:param allow_file_patterns: If provided, only files matching at least one pattern are scanned.
|
173
|
+
:param ignore_file_patterns: If provided, files matching any of the patterns are not scanned.
|
174
|
+
:param wait_for_results: True whether to wait for the scan to finish, defaults to True.
|
175
|
+
:param request_source: Source that requested the scan.
|
176
|
+
:param origin: Origin platform where the model came from.
|
177
|
+
|
178
|
+
:returns: Scan Results
|
179
|
+
"""
|
180
|
+
model_path = Path(path)
|
181
|
+
|
182
|
+
# Start the upload
|
183
|
+
upload_response = self._client.scans.upload.start(
|
184
|
+
model_name=model_name,
|
185
|
+
model_version=model_version,
|
186
|
+
requesting_entity="hiddenlayer-python-sdk",
|
187
|
+
request_source=cast("Literal['Hybrid Upload', 'API Upload', 'Integration', 'UI Upload']", request_source),
|
188
|
+
origin=origin,
|
189
|
+
)
|
190
|
+
|
191
|
+
scan_id = upload_response.scan_id
|
192
|
+
if scan_id is None:
|
193
|
+
raise ValueError("scan_id must have a value")
|
194
|
+
|
195
|
+
# Prepare file patterns
|
196
|
+
ignore_file_patterns = EXCLUDE_FILE_TYPES + ignore_file_patterns if ignore_file_patterns else EXCLUDE_FILE_TYPES
|
197
|
+
|
198
|
+
# Filter files
|
199
|
+
files = filter_path_objects(
|
200
|
+
model_path.rglob("*"),
|
201
|
+
allow_patterns=allow_file_patterns,
|
202
|
+
ignore_patterns=ignore_file_patterns,
|
203
|
+
)
|
204
|
+
|
205
|
+
# Upload each file
|
206
|
+
for file in files:
|
207
|
+
self._scan_file(scan_id=scan_id, file_path=Path(file))
|
208
|
+
|
209
|
+
# Complete the upload
|
210
|
+
self._client.scans.upload.complete_all(scan_id=scan_id)
|
211
|
+
|
212
|
+
if wait_for_results:
|
213
|
+
scan_results = wait_for_scan_results(self._client, scan_id=scan_id)
|
214
|
+
else:
|
215
|
+
scan_results = get_scan_results(self._client, scan_id=scan_id)
|
216
|
+
|
217
|
+
return scan_results
|
218
|
+
|
219
|
+
def scan_s3_model(
|
220
|
+
self,
|
221
|
+
*,
|
222
|
+
model_name: str,
|
223
|
+
bucket: str,
|
224
|
+
key: str,
|
225
|
+
model_version: str = "1",
|
226
|
+
s3_client: Optional[object] = None,
|
227
|
+
wait_for_results: bool = True,
|
228
|
+
request_source: str = "API Upload",
|
229
|
+
) -> "ScanReport":
|
230
|
+
"""
|
231
|
+
Scan a model file on S3.
|
232
|
+
|
233
|
+
:param model_name: Name of the model to be shown on the HiddenLayer UI.
|
234
|
+
:param bucket: Name of the s3 bucket where the model file is stored.
|
235
|
+
:param key: Path to the model file on s3.
|
236
|
+
:param model_version: Version of the model to be shown on the HiddenLayer UI.
|
237
|
+
:param s3_client: boto3 s3 client.
|
238
|
+
:param wait_for_results: True whether to wait for the scan to finish, defaults to True.
|
239
|
+
:param request_source: Source that requested the scan.
|
240
|
+
|
241
|
+
:returns: Scan Results
|
242
|
+
|
243
|
+
:examples:
|
244
|
+
.. code-block:: python
|
245
|
+
|
246
|
+
hl_client.model_scanner.scan_s3_model(
|
247
|
+
model_name="your-model-name", bucket="s3_bucket", key="path/to/file"
|
248
|
+
)
|
249
|
+
"""
|
250
|
+
try:
|
251
|
+
import boto3 # type: ignore
|
252
|
+
except ImportError as err:
|
253
|
+
raise ImportError("Python package boto3 is not installed.") from err
|
254
|
+
|
255
|
+
if not s3_client:
|
256
|
+
s3_client = boto3.client("s3") # type: ignore
|
257
|
+
|
258
|
+
file_name = key.split("/")[-1]
|
259
|
+
|
260
|
+
try:
|
261
|
+
s3_client.download_file(bucket, key, f"/tmp/{file_name}") # type: ignore
|
262
|
+
except Exception as e:
|
263
|
+
raise RuntimeError(f"Couldn't download model s3://{bucket}/{key}: {e}") from e
|
264
|
+
|
265
|
+
return self.scan_file(
|
266
|
+
model_path=f"/tmp/{file_name}",
|
267
|
+
model_name=model_name,
|
268
|
+
model_version=model_version,
|
269
|
+
wait_for_results=wait_for_results,
|
270
|
+
request_source=request_source,
|
271
|
+
origin="S3",
|
272
|
+
)
|
273
|
+
|
274
|
+
def scan_azure_blob_model(
|
275
|
+
self,
|
276
|
+
*,
|
277
|
+
model_name: str,
|
278
|
+
account_url: str,
|
279
|
+
container: str,
|
280
|
+
blob: str,
|
281
|
+
model_version: str = "1",
|
282
|
+
blob_service_client: Optional[object] = None,
|
283
|
+
credential: Optional[object] = None,
|
284
|
+
wait_for_results: bool = True,
|
285
|
+
request_source: str = "API Upload",
|
286
|
+
) -> "ScanReport":
|
287
|
+
"""
|
288
|
+
Scan a model file on Azure Blob Storage.
|
289
|
+
|
290
|
+
:param model_name: Name of the model to be shown on the HiddenLayer UI.
|
291
|
+
:param account_url: Azure Blob url of where the file is stored.
|
292
|
+
:param container: Azure Blob container containing the model file.
|
293
|
+
:param blob: Path to the model file inside the Azure blob container.
|
294
|
+
:param model_version: Version of the model to be shown on the HiddenLayer UI.
|
295
|
+
:param blob_service_client: BlobServiceClient object. Defaults to creating one using DefaultCredential().
|
296
|
+
:param credential: Credential to be passed to the BlobServiceClient object, can be a credential object, SAS key, etc.
|
297
|
+
Defaults to `DefaultCredential`
|
298
|
+
:param wait_for_results: True whether to wait for the scan to finish, defaults to True.
|
299
|
+
:param request_source: Source that requested the scan.
|
300
|
+
|
301
|
+
:returns: Scan Results
|
302
|
+
|
303
|
+
:examples:
|
304
|
+
.. code-block:: python
|
305
|
+
|
306
|
+
hl_client.model_scanner.scan_azure_blob_model(
|
307
|
+
model_name="your-model-name",
|
308
|
+
account_url="https://<storageaccountname>.blob.core.windows.net",
|
309
|
+
container="container_name",
|
310
|
+
blob="path/to/file.bin",
|
311
|
+
credential="?<sas_key>", # If using a SAS key and not DefaultCredentials
|
312
|
+
)
|
313
|
+
"""
|
314
|
+
try:
|
315
|
+
from azure.identity import DefaultAzureCredential # type: ignore
|
316
|
+
except ImportError as err:
|
317
|
+
raise ImportError("Python package azure-identity is not installed.") from err
|
318
|
+
|
319
|
+
try:
|
320
|
+
from azure.storage.blob import BlobServiceClient # type: ignore
|
321
|
+
except ImportError as err:
|
322
|
+
raise ImportError("Python package azure-storage-blob is not installed.") from err
|
323
|
+
|
324
|
+
if not credential:
|
325
|
+
credential = DefaultAzureCredential() # type: ignore
|
326
|
+
|
327
|
+
if not blob_service_client:
|
328
|
+
blob_service_client = BlobServiceClient(account_url, credential=credential) # type: ignore
|
329
|
+
|
330
|
+
file_name = blob.split("/")[-1]
|
331
|
+
|
332
|
+
blob_client = blob_service_client.get_blob_client( # type: ignore
|
333
|
+
container=container, blob=blob
|
334
|
+
)
|
335
|
+
|
336
|
+
try:
|
337
|
+
with open(os.path.join("/tmp", file_name), "wb") as f:
|
338
|
+
download_stream = blob_client.download_blob() # type: ignore
|
339
|
+
f.write(download_stream.readall()) # type: ignore
|
340
|
+
|
341
|
+
except Exception as e:
|
342
|
+
raise RuntimeError(f"Couldn't download model {account_url}, {container}, {blob}: {e}") from e
|
343
|
+
|
344
|
+
return self.scan_file(
|
345
|
+
model_path=f"/tmp/{file_name}",
|
346
|
+
model_name=model_name,
|
347
|
+
model_version=model_version,
|
348
|
+
wait_for_results=wait_for_results,
|
349
|
+
request_source=request_source,
|
350
|
+
origin="Azure Blob Storage",
|
351
|
+
)
|
352
|
+
|
353
|
+
def scan_huggingface_model(
|
354
|
+
self,
|
355
|
+
*,
|
356
|
+
repo_id: str,
|
357
|
+
model_name: Optional[str] = None,
|
358
|
+
revision: Optional[str] = None,
|
359
|
+
local_dir: str = "/tmp",
|
360
|
+
allow_file_patterns: Optional[List[str]] = None,
|
361
|
+
ignore_file_patterns: Optional[List[str]] = None,
|
362
|
+
force_download: bool = False,
|
363
|
+
hf_token: Optional[Union[str, bool]] = None,
|
364
|
+
wait_for_results: bool = True,
|
365
|
+
request_source: str = "API Upload",
|
366
|
+
) -> "ScanReport":
|
367
|
+
"""
|
368
|
+
Scans a model on HuggingFace.
|
369
|
+
|
370
|
+
Note: Requires the `huggingface_hub` pip package to be installed.
|
371
|
+
|
372
|
+
:param repo_id: The HuggingFace repository id.
|
373
|
+
:param model_name: Name of the model to be shown on the HiddenLayer UI. If not provided, uses repo_id.
|
374
|
+
:param revision: An optional Git revision id which can be a branch name, a tag, or a commit hash.
|
375
|
+
:param local_dir: If provided, the downloaded files will be placed under this directory.
|
376
|
+
:param allow_file_patterns: If provided, only files matching at least one pattern are scanned.
|
377
|
+
:param ignore_file_patterns: If provided, files matching any of the patterns are not scanned.
|
378
|
+
:param force_download: Whether the file should be downloaded even if it already exists in the local cache.
|
379
|
+
:param hf_token: A token to be used for the download.
|
380
|
+
If True, the token is read from the HuggingFace config folder.
|
381
|
+
If a string, it's used as the authentication token.
|
382
|
+
:param wait_for_results: True whether to wait for the scan to finish, defaults to True.
|
383
|
+
:param request_source: Source that requested the scan.
|
384
|
+
|
385
|
+
:returns: Scan Results
|
386
|
+
"""
|
387
|
+
try:
|
388
|
+
from huggingface_hub import snapshot_download # type: ignore
|
389
|
+
except ImportError as err:
|
390
|
+
raise ImportError("Python package huggingface_hub is not installed.") from err
|
391
|
+
|
392
|
+
local_dir = f"/tmp/{repo_id}" if local_dir == "/tmp" else local_dir
|
393
|
+
ignore_file_patterns = EXCLUDE_FILE_TYPES + ignore_file_patterns if ignore_file_patterns else EXCLUDE_FILE_TYPES
|
394
|
+
|
395
|
+
snapshot_download(
|
396
|
+
repo_id,
|
397
|
+
revision=revision,
|
398
|
+
allow_patterns=allow_file_patterns,
|
399
|
+
ignore_patterns=ignore_file_patterns,
|
400
|
+
local_dir=local_dir,
|
401
|
+
local_dir_use_symlinks=False,
|
402
|
+
cache_dir=local_dir,
|
403
|
+
force_download=force_download,
|
404
|
+
token=hf_token,
|
405
|
+
)
|
406
|
+
|
407
|
+
if revision is None:
|
408
|
+
revision = "1"
|
409
|
+
|
410
|
+
return self.scan_folder(
|
411
|
+
model_name=model_name or repo_id,
|
412
|
+
model_version=revision,
|
413
|
+
path=local_dir,
|
414
|
+
allow_file_patterns=allow_file_patterns,
|
415
|
+
ignore_file_patterns=ignore_file_patterns,
|
416
|
+
wait_for_results=wait_for_results,
|
417
|
+
request_source=request_source,
|
418
|
+
origin="Hugging Face",
|
419
|
+
)
|
420
|
+
|
421
|
+
def _scan_file(self, *, scan_id: str, file_path: Path) -> None:
|
422
|
+
"""Upload a single file using multipart upload."""
|
423
|
+
filesize = file_path.stat().st_size
|
424
|
+
|
425
|
+
# Initiate multipart upload for this file
|
426
|
+
upload = self._client.scans.upload.file.add(
|
427
|
+
scan_id=scan_id, file_name=str(file_path), file_content_length=filesize
|
428
|
+
)
|
429
|
+
|
430
|
+
# Upload each part
|
431
|
+
with open(file_path, "rb") as f:
|
432
|
+
for part in upload.parts:
|
433
|
+
if part.start_offset is None:
|
434
|
+
raise Exception("part must have a start_offset")
|
435
|
+
|
436
|
+
if part.end_offset is not None:
|
437
|
+
read_amount = part.end_offset - part.start_offset
|
438
|
+
else:
|
439
|
+
read_amount = None
|
440
|
+
|
441
|
+
f.seek(part.start_offset)
|
442
|
+
part_data = f.read(read_amount)
|
443
|
+
|
444
|
+
# Upload this part directly to the presigned URL
|
445
|
+
if part.upload_url is None:
|
446
|
+
raise Exception("part.upload_url must not be None")
|
447
|
+
|
448
|
+
response = httpx.put(
|
449
|
+
part.upload_url,
|
450
|
+
content=part_data,
|
451
|
+
headers={"Content-Type": "application/octet-stream"},
|
452
|
+
)
|
453
|
+
response.raise_for_status()
|
454
|
+
|
455
|
+
# Complete the file upload
|
456
|
+
self._client.scans.upload.file.complete(file_id=upload.upload_id, scan_id=scan_id)
|
457
|
+
|
458
|
+
|
459
|
+
class AsyncModelScanner:
|
460
|
+
"""
|
461
|
+
Async version of ModelScanner for use with AsyncHiddenLayer client.
|
462
|
+
"""
|
463
|
+
|
464
|
+
def __init__(self, client: "AsyncHiddenLayer") -> None:
|
465
|
+
self._client = client
|
466
|
+
|
467
|
+
async def scan_file(
|
468
|
+
self,
|
469
|
+
*,
|
470
|
+
model_name: str,
|
471
|
+
model_path: Union[str, os.PathLike[str]],
|
472
|
+
model_version: str = "1",
|
473
|
+
wait_for_results: bool = True,
|
474
|
+
request_source: str = "API Upload",
|
475
|
+
origin: str = "",
|
476
|
+
) -> "ScanReport":
|
477
|
+
"""
|
478
|
+
Async version of scan_file.
|
479
|
+
|
480
|
+
See ModelScanner.scan_file for parameter documentation.
|
481
|
+
"""
|
482
|
+
file_path = Path(model_path)
|
483
|
+
|
484
|
+
# Start the upload
|
485
|
+
upload_response = await self._client.scans.upload.start(
|
486
|
+
model_name=model_name,
|
487
|
+
model_version=model_version,
|
488
|
+
requesting_entity="hiddenlayer-python-sdk",
|
489
|
+
request_source=cast("Literal['Hybrid Upload', 'API Upload', 'Integration', 'UI Upload']", request_source),
|
490
|
+
origin=origin,
|
491
|
+
)
|
492
|
+
|
493
|
+
scan_id = upload_response.scan_id
|
494
|
+
if scan_id is None:
|
495
|
+
raise ValueError("scan_id must have a value")
|
496
|
+
|
497
|
+
# Upload the file
|
498
|
+
await self._scan_file(scan_id=scan_id, file_path=file_path)
|
499
|
+
|
500
|
+
# Complete the upload
|
501
|
+
await self._client.scans.upload.complete_all(scan_id=scan_id)
|
502
|
+
|
503
|
+
if wait_for_results:
|
504
|
+
scan_results = await wait_for_scan_results_async(self._client, scan_id=scan_id)
|
505
|
+
else:
|
506
|
+
scan_results = await get_scan_results_async(self._client, scan_id=scan_id)
|
507
|
+
|
508
|
+
return scan_results
|
509
|
+
|
510
|
+
async def scan_folder(
|
511
|
+
self,
|
512
|
+
*,
|
513
|
+
model_name: str,
|
514
|
+
path: Union[str, os.PathLike[str]],
|
515
|
+
model_version: str = "1",
|
516
|
+
allow_file_patterns: Optional[List[str]] = None,
|
517
|
+
ignore_file_patterns: Optional[List[str]] = None,
|
518
|
+
wait_for_results: bool = True,
|
519
|
+
request_source: str = "API Upload",
|
520
|
+
origin: str = "",
|
521
|
+
) -> "ScanReport":
|
522
|
+
"""
|
523
|
+
Async version of scan_folder.
|
524
|
+
|
525
|
+
See ModelScanner.scan_folder for parameter documentation.
|
526
|
+
"""
|
527
|
+
model_path = Path(path)
|
528
|
+
|
529
|
+
# Start the upload
|
530
|
+
upload_response = await self._client.scans.upload.start(
|
531
|
+
model_name=model_name,
|
532
|
+
model_version=model_version,
|
533
|
+
requesting_entity="hiddenlayer-python-sdk",
|
534
|
+
request_source=cast("Literal['Hybrid Upload', 'API Upload', 'Integration', 'UI Upload']", request_source),
|
535
|
+
origin=origin,
|
536
|
+
)
|
537
|
+
|
538
|
+
scan_id = upload_response.scan_id
|
539
|
+
if scan_id is None:
|
540
|
+
raise ValueError("scan_id must have a value")
|
541
|
+
|
542
|
+
# Prepare file patterns
|
543
|
+
ignore_file_patterns = EXCLUDE_FILE_TYPES + ignore_file_patterns if ignore_file_patterns else EXCLUDE_FILE_TYPES
|
544
|
+
|
545
|
+
# Filter files
|
546
|
+
files = filter_path_objects(
|
547
|
+
model_path.rglob("*"),
|
548
|
+
allow_patterns=allow_file_patterns,
|
549
|
+
ignore_patterns=ignore_file_patterns,
|
550
|
+
)
|
551
|
+
|
552
|
+
# Upload each file
|
553
|
+
for file in files:
|
554
|
+
await self._scan_file(scan_id=scan_id, file_path=Path(file))
|
555
|
+
|
556
|
+
# Complete the upload
|
557
|
+
await self._client.scans.upload.complete_all(scan_id=scan_id)
|
558
|
+
|
559
|
+
if wait_for_results:
|
560
|
+
scan_results = await wait_for_scan_results_async(self._client, scan_id=scan_id)
|
561
|
+
else:
|
562
|
+
scan_results = await get_scan_results_async(self._client, scan_id=scan_id)
|
563
|
+
|
564
|
+
return scan_results
|
565
|
+
|
566
|
+
async def scan_s3_model(
|
567
|
+
self,
|
568
|
+
*,
|
569
|
+
model_name: str,
|
570
|
+
bucket: str,
|
571
|
+
key: str,
|
572
|
+
model_version: str = "1",
|
573
|
+
s3_client: Optional[object] = None,
|
574
|
+
wait_for_results: bool = True,
|
575
|
+
request_source: str = "API Upload",
|
576
|
+
) -> "ScanReport":
|
577
|
+
"""
|
578
|
+
Async version of scan_s3_model.
|
579
|
+
|
580
|
+
See ModelScanner.scan_s3_model for parameter documentation.
|
581
|
+
"""
|
582
|
+
try:
|
583
|
+
import boto3 # type: ignore
|
584
|
+
except ImportError as err:
|
585
|
+
raise ImportError("Python package boto3 is not installed.") from err
|
586
|
+
|
587
|
+
if not s3_client:
|
588
|
+
s3_client = boto3.client("s3") # type: ignore
|
589
|
+
|
590
|
+
file_name = key.split("/")[-1]
|
591
|
+
|
592
|
+
try:
|
593
|
+
s3_client.download_file(bucket, key, f"/tmp/{file_name}") # type: ignore
|
594
|
+
except Exception as e:
|
595
|
+
raise RuntimeError(f"Couldn't download model s3://{bucket}/{key}: {e}") from e
|
596
|
+
|
597
|
+
return await self.scan_file(
|
598
|
+
model_path=f"/tmp/{file_name}",
|
599
|
+
model_name=model_name,
|
600
|
+
model_version=model_version,
|
601
|
+
wait_for_results=wait_for_results,
|
602
|
+
request_source=request_source,
|
603
|
+
origin="S3",
|
604
|
+
)
|
605
|
+
|
606
|
+
async def scan_azure_blob_model(
|
607
|
+
self,
|
608
|
+
*,
|
609
|
+
model_name: str,
|
610
|
+
account_url: str,
|
611
|
+
container: str,
|
612
|
+
blob: str,
|
613
|
+
model_version: str = "1",
|
614
|
+
blob_service_client: Optional[object] = None,
|
615
|
+
credential: Optional[object] = None,
|
616
|
+
wait_for_results: bool = True,
|
617
|
+
request_source: str = "API Upload",
|
618
|
+
) -> "ScanReport":
|
619
|
+
"""
|
620
|
+
Async version of scan_azure_blob_model.
|
621
|
+
|
622
|
+
See ModelScanner.scan_azure_blob_model for parameter documentation.
|
623
|
+
"""
|
624
|
+
try:
|
625
|
+
from azure.identity import DefaultAzureCredential # type: ignore
|
626
|
+
except ImportError as err:
|
627
|
+
raise ImportError("Python package azure-identity is not installed.") from err
|
628
|
+
|
629
|
+
try:
|
630
|
+
from azure.storage.blob import BlobServiceClient # type: ignore
|
631
|
+
except ImportError as err:
|
632
|
+
raise ImportError("Python package azure-storage-blob is not installed.") from err
|
633
|
+
|
634
|
+
if not credential:
|
635
|
+
credential = DefaultAzureCredential() # type: ignore
|
636
|
+
|
637
|
+
if not blob_service_client:
|
638
|
+
blob_service_client = BlobServiceClient(account_url, credential=credential) # type: ignore
|
639
|
+
|
640
|
+
file_name = blob.split("/")[-1]
|
641
|
+
|
642
|
+
blob_client = blob_service_client.get_blob_client( # type: ignore
|
643
|
+
container=container, blob=blob
|
644
|
+
)
|
645
|
+
|
646
|
+
try:
|
647
|
+
with open(os.path.join("/tmp", file_name), "wb") as f:
|
648
|
+
download_stream = blob_client.download_blob() # type: ignore
|
649
|
+
f.write(download_stream.readall()) # type: ignore
|
650
|
+
|
651
|
+
except Exception as e:
|
652
|
+
raise RuntimeError(f"Couldn't download model {account_url}, {container}, {blob}: {e}") from e
|
653
|
+
|
654
|
+
return await self.scan_file(
|
655
|
+
model_path=f"/tmp/{file_name}",
|
656
|
+
model_name=model_name,
|
657
|
+
model_version=model_version,
|
658
|
+
wait_for_results=wait_for_results,
|
659
|
+
request_source=request_source,
|
660
|
+
origin="Azure Blob Storage",
|
661
|
+
)
|
662
|
+
|
663
|
+
async def scan_huggingface_model(
|
664
|
+
self,
|
665
|
+
*,
|
666
|
+
repo_id: str,
|
667
|
+
model_name: Optional[str] = None,
|
668
|
+
revision: Optional[str] = None,
|
669
|
+
local_dir: str = "/tmp",
|
670
|
+
allow_file_patterns: Optional[List[str]] = None,
|
671
|
+
ignore_file_patterns: Optional[List[str]] = None,
|
672
|
+
force_download: bool = False,
|
673
|
+
hf_token: Optional[Union[str, bool]] = None,
|
674
|
+
wait_for_results: bool = True,
|
675
|
+
request_source: str = "API Upload",
|
676
|
+
) -> "ScanReport":
|
677
|
+
"""
|
678
|
+
Async version of scan_huggingface_model.
|
679
|
+
|
680
|
+
See ModelScanner.scan_huggingface_model for parameter documentation.
|
681
|
+
"""
|
682
|
+
try:
|
683
|
+
from huggingface_hub import snapshot_download # type: ignore
|
684
|
+
except ImportError as err:
|
685
|
+
raise ImportError("Python package huggingface_hub is not installed.") from err
|
686
|
+
|
687
|
+
local_dir = f"/tmp/{repo_id}" if local_dir == "/tmp" else local_dir
|
688
|
+
ignore_file_patterns = EXCLUDE_FILE_TYPES + ignore_file_patterns if ignore_file_patterns else EXCLUDE_FILE_TYPES
|
689
|
+
|
690
|
+
snapshot_download(
|
691
|
+
repo_id,
|
692
|
+
revision=revision,
|
693
|
+
allow_patterns=allow_file_patterns,
|
694
|
+
ignore_patterns=ignore_file_patterns,
|
695
|
+
local_dir=local_dir,
|
696
|
+
local_dir_use_symlinks=False,
|
697
|
+
cache_dir=local_dir,
|
698
|
+
force_download=force_download,
|
699
|
+
token=hf_token,
|
700
|
+
)
|
701
|
+
|
702
|
+
if revision is None:
|
703
|
+
revision = "1"
|
704
|
+
|
705
|
+
return await self.scan_folder(
|
706
|
+
model_name=model_name or repo_id,
|
707
|
+
model_version=revision,
|
708
|
+
path=local_dir,
|
709
|
+
allow_file_patterns=allow_file_patterns,
|
710
|
+
ignore_file_patterns=ignore_file_patterns,
|
711
|
+
wait_for_results=wait_for_results,
|
712
|
+
request_source=request_source,
|
713
|
+
origin="Hugging Face",
|
714
|
+
)
|
715
|
+
|
716
|
+
async def _scan_file(self, *, scan_id: str, file_path: Path) -> None:
|
717
|
+
"""Async version of _scan_file."""
|
718
|
+
filesize = file_path.stat().st_size
|
719
|
+
|
720
|
+
# Initiate multipart upload for this file
|
721
|
+
upload = await self._client.scans.upload.file.add(
|
722
|
+
scan_id=scan_id, file_name=str(file_path), file_content_length=filesize
|
723
|
+
)
|
724
|
+
|
725
|
+
# Upload each part
|
726
|
+
with open(file_path, "rb") as f:
|
727
|
+
for part in upload.parts:
|
728
|
+
if part.start_offset is None:
|
729
|
+
raise Exception("part must have a start_offset")
|
730
|
+
|
731
|
+
if part.end_offset is not None:
|
732
|
+
read_amount = part.end_offset - part.start_offset
|
733
|
+
else:
|
734
|
+
read_amount = None
|
735
|
+
|
736
|
+
f.seek(part.start_offset)
|
737
|
+
part_data = f.read(read_amount)
|
738
|
+
|
739
|
+
# Upload this part directly to the presigned URL
|
740
|
+
if part.upload_url is None:
|
741
|
+
raise Exception("part.upload_url must not be None")
|
742
|
+
|
743
|
+
async with httpx.AsyncClient() as client:
|
744
|
+
response = await client.put(
|
745
|
+
part.upload_url,
|
746
|
+
content=part_data,
|
747
|
+
headers={"Content-Type": "application/octet-stream"},
|
748
|
+
)
|
749
|
+
response.raise_for_status()
|
750
|
+
|
751
|
+
# Complete the file upload
|
752
|
+
await self._client.scans.upload.file.complete(file_id=upload.upload_id, scan_id=scan_id)
|