hiddenlayer-sdk 2.0.10__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.
Files changed (204) hide show
  1. hiddenlayer/__init__.py +109 -114
  2. hiddenlayer/_base_client.py +1995 -0
  3. hiddenlayer/_client.py +761 -0
  4. hiddenlayer/_compat.py +219 -0
  5. hiddenlayer/_constants.py +14 -0
  6. hiddenlayer/_exceptions.py +108 -0
  7. hiddenlayer/_files.py +123 -0
  8. hiddenlayer/_models.py +835 -0
  9. hiddenlayer/_oauth2.py +118 -0
  10. hiddenlayer/_qs.py +150 -0
  11. hiddenlayer/_resource.py +43 -0
  12. hiddenlayer/_response.py +832 -0
  13. hiddenlayer/_streaming.py +333 -0
  14. hiddenlayer/_types.py +260 -0
  15. hiddenlayer/_utils/__init__.py +64 -0
  16. hiddenlayer/_utils/_compat.py +45 -0
  17. hiddenlayer/_utils/_datetime_parse.py +136 -0
  18. hiddenlayer/_utils/_logs.py +25 -0
  19. hiddenlayer/_utils/_proxy.py +65 -0
  20. hiddenlayer/_utils/_reflection.py +42 -0
  21. hiddenlayer/_utils/_resources_proxy.py +24 -0
  22. hiddenlayer/_utils/_streams.py +12 -0
  23. hiddenlayer/_utils/_sync.py +86 -0
  24. hiddenlayer/_utils/_transform.py +457 -0
  25. hiddenlayer/_utils/_typing.py +156 -0
  26. hiddenlayer/_utils/_utils.py +421 -0
  27. hiddenlayer/_version.py +4 -0
  28. hiddenlayer/lib/.keep +4 -0
  29. hiddenlayer/lib/__init__.py +6 -0
  30. hiddenlayer/lib/community_scan.py +174 -0
  31. hiddenlayer/lib/model_scan.py +752 -0
  32. hiddenlayer/lib/scan_utils.py +142 -0
  33. hiddenlayer/pagination.py +127 -0
  34. hiddenlayer/resources/__init__.py +75 -0
  35. hiddenlayer/resources/interactions.py +205 -0
  36. hiddenlayer/resources/models/__init__.py +33 -0
  37. hiddenlayer/resources/models/cards.py +259 -0
  38. hiddenlayer/resources/models/models.py +284 -0
  39. hiddenlayer/resources/prompt_analyzer.py +207 -0
  40. hiddenlayer/resources/scans/__init__.py +61 -0
  41. hiddenlayer/resources/scans/jobs.py +499 -0
  42. hiddenlayer/resources/scans/results.py +169 -0
  43. hiddenlayer/resources/scans/scans.py +166 -0
  44. hiddenlayer/resources/scans/upload/__init__.py +33 -0
  45. hiddenlayer/resources/scans/upload/file.py +279 -0
  46. hiddenlayer/resources/scans/upload/upload.py +340 -0
  47. hiddenlayer/resources/sensors.py +575 -0
  48. hiddenlayer/types/__init__.py +16 -0
  49. hiddenlayer/types/interaction_analyze_params.py +62 -0
  50. hiddenlayer/types/interaction_analyze_response.py +199 -0
  51. hiddenlayer/types/model_retrieve_response.py +50 -0
  52. hiddenlayer/types/models/__init__.py +6 -0
  53. hiddenlayer/types/models/card_list_params.py +65 -0
  54. hiddenlayer/types/models/card_list_response.py +50 -0
  55. hiddenlayer/types/prompt_analyzer_create_params.py +23 -0
  56. hiddenlayer/types/prompt_analyzer_create_response.py +381 -0
  57. hiddenlayer/types/scans/__init__.py +14 -0
  58. hiddenlayer/types/scans/job_list_params.py +75 -0
  59. hiddenlayer/types/scans/job_list_response.py +22 -0
  60. hiddenlayer/types/scans/job_request_params.py +49 -0
  61. hiddenlayer/types/scans/job_retrieve_params.py +16 -0
  62. hiddenlayer/types/scans/result_sarif_response.py +7 -0
  63. hiddenlayer/types/scans/scan_job.py +46 -0
  64. hiddenlayer/types/scans/scan_report.py +367 -0
  65. hiddenlayer/types/scans/upload/__init__.py +6 -0
  66. hiddenlayer/types/scans/upload/file_add_response.py +24 -0
  67. hiddenlayer/types/scans/upload/file_complete_response.py +12 -0
  68. hiddenlayer/types/scans/upload_complete_all_response.py +12 -0
  69. hiddenlayer/types/scans/upload_start_params.py +34 -0
  70. hiddenlayer/types/scans/upload_start_response.py +12 -0
  71. hiddenlayer/types/sensor_create_params.py +24 -0
  72. hiddenlayer/types/sensor_create_response.py +33 -0
  73. hiddenlayer/types/sensor_query_params.py +39 -0
  74. hiddenlayer/types/sensor_query_response.py +43 -0
  75. hiddenlayer/types/sensor_retrieve_response.py +33 -0
  76. hiddenlayer/types/sensor_update_params.py +20 -0
  77. hiddenlayer/types/sensor_update_response.py +9 -0
  78. hiddenlayer_sdk-3.0.0.dist-info/METADATA +431 -0
  79. hiddenlayer_sdk-3.0.0.dist-info/RECORD +82 -0
  80. {hiddenlayer_sdk-2.0.10.dist-info → hiddenlayer_sdk-3.0.0.dist-info}/WHEEL +1 -2
  81. {hiddenlayer_sdk-2.0.10.dist-info → hiddenlayer_sdk-3.0.0.dist-info}/licenses/LICENSE +1 -1
  82. hiddenlayer/sdk/constants.py +0 -26
  83. hiddenlayer/sdk/exceptions.py +0 -12
  84. hiddenlayer/sdk/models.py +0 -58
  85. hiddenlayer/sdk/rest/__init__.py +0 -135
  86. hiddenlayer/sdk/rest/api/__init__.py +0 -10
  87. hiddenlayer/sdk/rest/api/aidr_predictive_api.py +0 -308
  88. hiddenlayer/sdk/rest/api/health_api.py +0 -272
  89. hiddenlayer/sdk/rest/api/model_api.py +0 -559
  90. hiddenlayer/sdk/rest/api/model_supply_chain_api.py +0 -4063
  91. hiddenlayer/sdk/rest/api/readiness_api.py +0 -272
  92. hiddenlayer/sdk/rest/api/sensor_api.py +0 -1432
  93. hiddenlayer/sdk/rest/api_client.py +0 -770
  94. hiddenlayer/sdk/rest/api_response.py +0 -21
  95. hiddenlayer/sdk/rest/configuration.py +0 -445
  96. hiddenlayer/sdk/rest/exceptions.py +0 -199
  97. hiddenlayer/sdk/rest/models/__init__.py +0 -113
  98. hiddenlayer/sdk/rest/models/address.py +0 -110
  99. hiddenlayer/sdk/rest/models/artifact.py +0 -155
  100. hiddenlayer/sdk/rest/models/artifact_change.py +0 -108
  101. hiddenlayer/sdk/rest/models/artifact_content.py +0 -101
  102. hiddenlayer/sdk/rest/models/artifact_location.py +0 -109
  103. hiddenlayer/sdk/rest/models/attachment.py +0 -129
  104. hiddenlayer/sdk/rest/models/begin_multi_file_upload200_response.py +0 -87
  105. hiddenlayer/sdk/rest/models/begin_multipart_file_upload200_response.py +0 -97
  106. hiddenlayer/sdk/rest/models/begin_multipart_file_upload200_response_parts_inner.py +0 -94
  107. hiddenlayer/sdk/rest/models/code_flow.py +0 -113
  108. hiddenlayer/sdk/rest/models/configuration_override.py +0 -108
  109. hiddenlayer/sdk/rest/models/conversion.py +0 -114
  110. hiddenlayer/sdk/rest/models/create_sensor_request.py +0 -95
  111. hiddenlayer/sdk/rest/models/edge.py +0 -108
  112. hiddenlayer/sdk/rest/models/edge_traversal.py +0 -122
  113. hiddenlayer/sdk/rest/models/errors_inner.py +0 -91
  114. hiddenlayer/sdk/rest/models/exception.py +0 -113
  115. hiddenlayer/sdk/rest/models/external_properties.py +0 -273
  116. hiddenlayer/sdk/rest/models/external_property_file_reference.py +0 -102
  117. hiddenlayer/sdk/rest/models/external_property_file_references.py +0 -240
  118. hiddenlayer/sdk/rest/models/file_details_v3.py +0 -139
  119. hiddenlayer/sdk/rest/models/file_result_v3.py +0 -117
  120. hiddenlayer/sdk/rest/models/file_scan_report_v3.py +0 -132
  121. hiddenlayer/sdk/rest/models/file_scan_reports_v3.py +0 -95
  122. hiddenlayer/sdk/rest/models/fix.py +0 -113
  123. hiddenlayer/sdk/rest/models/get_condensed_model_scan_reports200_response.py +0 -102
  124. hiddenlayer/sdk/rest/models/graph.py +0 -123
  125. hiddenlayer/sdk/rest/models/graph_traversal.py +0 -97
  126. hiddenlayer/sdk/rest/models/inventory_v3.py +0 -101
  127. hiddenlayer/sdk/rest/models/invocation.py +0 -199
  128. hiddenlayer/sdk/rest/models/location.py +0 -146
  129. hiddenlayer/sdk/rest/models/location_inner.py +0 -138
  130. hiddenlayer/sdk/rest/models/location_relationship.py +0 -107
  131. hiddenlayer/sdk/rest/models/logical_location.py +0 -104
  132. hiddenlayer/sdk/rest/models/message.py +0 -92
  133. hiddenlayer/sdk/rest/models/mitre_atlas_inner.py +0 -110
  134. hiddenlayer/sdk/rest/models/model.py +0 -103
  135. hiddenlayer/sdk/rest/models/model_inventory_info.py +0 -103
  136. hiddenlayer/sdk/rest/models/model_version.py +0 -97
  137. hiddenlayer/sdk/rest/models/multi_file_upload_request_v3.py +0 -97
  138. hiddenlayer/sdk/rest/models/multiformat_message_string.py +0 -95
  139. hiddenlayer/sdk/rest/models/node.py +0 -122
  140. hiddenlayer/sdk/rest/models/notification.py +0 -157
  141. hiddenlayer/sdk/rest/models/notify_model_scan_completed200_response.py +0 -87
  142. hiddenlayer/sdk/rest/models/paged_response_with_total.py +0 -94
  143. hiddenlayer/sdk/rest/models/pagination_v3.py +0 -95
  144. hiddenlayer/sdk/rest/models/physical_location.py +0 -94
  145. hiddenlayer/sdk/rest/models/problem_details.py +0 -103
  146. hiddenlayer/sdk/rest/models/property_bag.py +0 -101
  147. hiddenlayer/sdk/rest/models/rectangle.py +0 -110
  148. hiddenlayer/sdk/rest/models/region.py +0 -127
  149. hiddenlayer/sdk/rest/models/replacement.py +0 -103
  150. hiddenlayer/sdk/rest/models/reporting_configuration.py +0 -113
  151. hiddenlayer/sdk/rest/models/reporting_descriptor.py +0 -162
  152. hiddenlayer/sdk/rest/models/reporting_descriptor_reference.py +0 -103
  153. hiddenlayer/sdk/rest/models/reporting_descriptor_relationship.py +0 -115
  154. hiddenlayer/sdk/rest/models/result.py +0 -312
  155. hiddenlayer/sdk/rest/models/result_provenance.py +0 -133
  156. hiddenlayer/sdk/rest/models/rule_details_inner.py +0 -102
  157. hiddenlayer/sdk/rest/models/run.py +0 -318
  158. hiddenlayer/sdk/rest/models/run_automation_details.py +0 -129
  159. hiddenlayer/sdk/rest/models/sarif210.py +0 -123
  160. hiddenlayer/sdk/rest/models/scan_create_request.py +0 -87
  161. hiddenlayer/sdk/rest/models/scan_detection_v3.py +0 -159
  162. hiddenlayer/sdk/rest/models/scan_detection_v31.py +0 -158
  163. hiddenlayer/sdk/rest/models/scan_header_v3.py +0 -129
  164. hiddenlayer/sdk/rest/models/scan_job.py +0 -115
  165. hiddenlayer/sdk/rest/models/scan_job_access.py +0 -97
  166. hiddenlayer/sdk/rest/models/scan_model_details_v3.py +0 -99
  167. hiddenlayer/sdk/rest/models/scan_model_details_v31.py +0 -97
  168. hiddenlayer/sdk/rest/models/scan_model_ids_v3.py +0 -89
  169. hiddenlayer/sdk/rest/models/scan_report_v3.py +0 -139
  170. hiddenlayer/sdk/rest/models/scan_results_map_v3.py +0 -105
  171. hiddenlayer/sdk/rest/models/scan_results_v3.py +0 -120
  172. hiddenlayer/sdk/rest/models/security_posture.py +0 -89
  173. hiddenlayer/sdk/rest/models/sensor.py +0 -100
  174. hiddenlayer/sdk/rest/models/sensor_query_response.py +0 -101
  175. hiddenlayer/sdk/rest/models/sensor_sor_model_card_query_response.py +0 -101
  176. hiddenlayer/sdk/rest/models/sensor_sor_model_card_response.py +0 -127
  177. hiddenlayer/sdk/rest/models/sensor_sor_query_filter.py +0 -108
  178. hiddenlayer/sdk/rest/models/sensor_sor_query_request.py +0 -109
  179. hiddenlayer/sdk/rest/models/special_locations.py +0 -97
  180. hiddenlayer/sdk/rest/models/stack.py +0 -113
  181. hiddenlayer/sdk/rest/models/stack_frame.py +0 -104
  182. hiddenlayer/sdk/rest/models/submission_response.py +0 -95
  183. hiddenlayer/sdk/rest/models/submission_v2.py +0 -109
  184. hiddenlayer/sdk/rest/models/suppression.py +0 -133
  185. hiddenlayer/sdk/rest/models/thread_flow.py +0 -144
  186. hiddenlayer/sdk/rest/models/thread_flow_location.py +0 -166
  187. hiddenlayer/sdk/rest/models/tool.py +0 -107
  188. hiddenlayer/sdk/rest/models/tool_component.py +0 -251
  189. hiddenlayer/sdk/rest/models/tool_component_reference.py +0 -108
  190. hiddenlayer/sdk/rest/models/translation_metadata.py +0 -110
  191. hiddenlayer/sdk/rest/models/validation_error_model.py +0 -99
  192. hiddenlayer/sdk/rest/models/version_control_details.py +0 -108
  193. hiddenlayer/sdk/rest/models/web_request.py +0 -112
  194. hiddenlayer/sdk/rest/models/web_response.py +0 -112
  195. hiddenlayer/sdk/rest/rest.py +0 -257
  196. hiddenlayer/sdk/services/__init__.py +0 -0
  197. hiddenlayer/sdk/services/aidr_predictive.py +0 -130
  198. hiddenlayer/sdk/services/model_scan.py +0 -505
  199. hiddenlayer/sdk/utils.py +0 -92
  200. hiddenlayer/sdk/version.py +0 -1
  201. hiddenlayer_sdk-2.0.10.dist-info/METADATA +0 -368
  202. hiddenlayer_sdk-2.0.10.dist-info/RECORD +0 -126
  203. hiddenlayer_sdk-2.0.10.dist-info/top_level.txt +0 -1
  204. /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)