hiddenlayer-sdk 2.0.10__py3-none-any.whl → 3.0.1__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.1.dist-info/METADATA +521 -0
  79. hiddenlayer_sdk-3.0.1.dist-info/RECORD +82 -0
  80. {hiddenlayer_sdk-2.0.10.dist-info → hiddenlayer_sdk-3.0.1.dist-info}/WHEEL +1 -2
  81. {hiddenlayer_sdk-2.0.10.dist-info → hiddenlayer_sdk-3.0.1.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,457 @@
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import base64
5
+ import pathlib
6
+ from typing import Any, Mapping, TypeVar, cast
7
+ from datetime import date, datetime
8
+ from typing_extensions import Literal, get_args, override, get_type_hints as _get_type_hints
9
+
10
+ import anyio
11
+ import pydantic
12
+
13
+ from ._utils import (
14
+ is_list,
15
+ is_given,
16
+ lru_cache,
17
+ is_mapping,
18
+ is_iterable,
19
+ is_sequence,
20
+ )
21
+ from .._files import is_base64_file_input
22
+ from ._compat import get_origin, is_typeddict
23
+ from ._typing import (
24
+ is_list_type,
25
+ is_union_type,
26
+ extract_type_arg,
27
+ is_iterable_type,
28
+ is_required_type,
29
+ is_sequence_type,
30
+ is_annotated_type,
31
+ strip_annotated_type,
32
+ )
33
+
34
+ _T = TypeVar("_T")
35
+
36
+
37
+ # TODO: support for drilling globals() and locals()
38
+ # TODO: ensure works correctly with forward references in all cases
39
+
40
+
41
+ PropertyFormat = Literal["iso8601", "base64", "custom"]
42
+
43
+
44
+ class PropertyInfo:
45
+ """Metadata class to be used in Annotated types to provide information about a given type.
46
+
47
+ For example:
48
+
49
+ class MyParams(TypedDict):
50
+ account_holder_name: Annotated[str, PropertyInfo(alias='accountHolderName')]
51
+
52
+ This means that {'account_holder_name': 'Robert'} will be transformed to {'accountHolderName': 'Robert'} before being sent to the API.
53
+ """
54
+
55
+ alias: str | None
56
+ format: PropertyFormat | None
57
+ format_template: str | None
58
+ discriminator: str | None
59
+
60
+ def __init__(
61
+ self,
62
+ *,
63
+ alias: str | None = None,
64
+ format: PropertyFormat | None = None,
65
+ format_template: str | None = None,
66
+ discriminator: str | None = None,
67
+ ) -> None:
68
+ self.alias = alias
69
+ self.format = format
70
+ self.format_template = format_template
71
+ self.discriminator = discriminator
72
+
73
+ @override
74
+ def __repr__(self) -> str:
75
+ return f"{self.__class__.__name__}(alias='{self.alias}', format={self.format}, format_template='{self.format_template}', discriminator='{self.discriminator}')"
76
+
77
+
78
+ def maybe_transform(
79
+ data: object,
80
+ expected_type: object,
81
+ ) -> Any | None:
82
+ """Wrapper over `transform()` that allows `None` to be passed.
83
+
84
+ See `transform()` for more details.
85
+ """
86
+ if data is None:
87
+ return None
88
+ return transform(data, expected_type)
89
+
90
+
91
+ # Wrapper over _transform_recursive providing fake types
92
+ def transform(
93
+ data: _T,
94
+ expected_type: object,
95
+ ) -> _T:
96
+ """Transform dictionaries based off of type information from the given type, for example:
97
+
98
+ ```py
99
+ class Params(TypedDict, total=False):
100
+ card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]]
101
+
102
+
103
+ transformed = transform({"card_id": "<my card ID>"}, Params)
104
+ # {'cardID': '<my card ID>'}
105
+ ```
106
+
107
+ Any keys / data that does not have type information given will be included as is.
108
+
109
+ It should be noted that the transformations that this function does are not represented in the type system.
110
+ """
111
+ transformed = _transform_recursive(data, annotation=cast(type, expected_type))
112
+ return cast(_T, transformed)
113
+
114
+
115
+ @lru_cache(maxsize=8096)
116
+ def _get_annotated_type(type_: type) -> type | None:
117
+ """If the given type is an `Annotated` type then it is returned, if not `None` is returned.
118
+
119
+ This also unwraps the type when applicable, e.g. `Required[Annotated[T, ...]]`
120
+ """
121
+ if is_required_type(type_):
122
+ # Unwrap `Required[Annotated[T, ...]]` to `Annotated[T, ...]`
123
+ type_ = get_args(type_)[0]
124
+
125
+ if is_annotated_type(type_):
126
+ return type_
127
+
128
+ return None
129
+
130
+
131
+ def _maybe_transform_key(key: str, type_: type) -> str:
132
+ """Transform the given `data` based on the annotations provided in `type_`.
133
+
134
+ Note: this function only looks at `Annotated` types that contain `PropertyInfo` metadata.
135
+ """
136
+ annotated_type = _get_annotated_type(type_)
137
+ if annotated_type is None:
138
+ # no `Annotated` definition for this type, no transformation needed
139
+ return key
140
+
141
+ # ignore the first argument as it is the actual type
142
+ annotations = get_args(annotated_type)[1:]
143
+ for annotation in annotations:
144
+ if isinstance(annotation, PropertyInfo) and annotation.alias is not None:
145
+ return annotation.alias
146
+
147
+ return key
148
+
149
+
150
+ def _no_transform_needed(annotation: type) -> bool:
151
+ return annotation == float or annotation == int
152
+
153
+
154
+ def _transform_recursive(
155
+ data: object,
156
+ *,
157
+ annotation: type,
158
+ inner_type: type | None = None,
159
+ ) -> object:
160
+ """Transform the given data against the expected type.
161
+
162
+ Args:
163
+ annotation: The direct type annotation given to the particular piece of data.
164
+ This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc
165
+
166
+ inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type
167
+ is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in
168
+ the list can be transformed using the metadata from the container type.
169
+
170
+ Defaults to the same value as the `annotation` argument.
171
+ """
172
+ from .._compat import model_dump
173
+
174
+ if inner_type is None:
175
+ inner_type = annotation
176
+
177
+ stripped_type = strip_annotated_type(inner_type)
178
+ origin = get_origin(stripped_type) or stripped_type
179
+ if is_typeddict(stripped_type) and is_mapping(data):
180
+ return _transform_typeddict(data, stripped_type)
181
+
182
+ if origin == dict and is_mapping(data):
183
+ items_type = get_args(stripped_type)[1]
184
+ return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
185
+
186
+ if (
187
+ # List[T]
188
+ (is_list_type(stripped_type) and is_list(data))
189
+ # Iterable[T]
190
+ or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
191
+ # Sequence[T]
192
+ or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
193
+ ):
194
+ # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
195
+ # intended as an iterable, so we don't transform it.
196
+ if isinstance(data, dict):
197
+ return cast(object, data)
198
+
199
+ inner_type = extract_type_arg(stripped_type, 0)
200
+ if _no_transform_needed(inner_type):
201
+ # for some types there is no need to transform anything, so we can get a small
202
+ # perf boost from skipping that work.
203
+ #
204
+ # but we still need to convert to a list to ensure the data is json-serializable
205
+ if is_list(data):
206
+ return data
207
+ return list(data)
208
+
209
+ return [_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
210
+
211
+ if is_union_type(stripped_type):
212
+ # For union types we run the transformation against all subtypes to ensure that everything is transformed.
213
+ #
214
+ # TODO: there may be edge cases where the same normalized field name will transform to two different names
215
+ # in different subtypes.
216
+ for subtype in get_args(stripped_type):
217
+ data = _transform_recursive(data, annotation=annotation, inner_type=subtype)
218
+ return data
219
+
220
+ if isinstance(data, pydantic.BaseModel):
221
+ return model_dump(data, exclude_unset=True, mode="json")
222
+
223
+ annotated_type = _get_annotated_type(annotation)
224
+ if annotated_type is None:
225
+ return data
226
+
227
+ # ignore the first argument as it is the actual type
228
+ annotations = get_args(annotated_type)[1:]
229
+ for annotation in annotations:
230
+ if isinstance(annotation, PropertyInfo) and annotation.format is not None:
231
+ return _format_data(data, annotation.format, annotation.format_template)
232
+
233
+ return data
234
+
235
+
236
+ def _format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object:
237
+ if isinstance(data, (date, datetime)):
238
+ if format_ == "iso8601":
239
+ return data.isoformat()
240
+
241
+ if format_ == "custom" and format_template is not None:
242
+ return data.strftime(format_template)
243
+
244
+ if format_ == "base64" and is_base64_file_input(data):
245
+ binary: str | bytes | None = None
246
+
247
+ if isinstance(data, pathlib.Path):
248
+ binary = data.read_bytes()
249
+ elif isinstance(data, io.IOBase):
250
+ binary = data.read()
251
+
252
+ if isinstance(binary, str): # type: ignore[unreachable]
253
+ binary = binary.encode()
254
+
255
+ if not isinstance(binary, bytes):
256
+ raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}")
257
+
258
+ return base64.b64encode(binary).decode("ascii")
259
+
260
+ return data
261
+
262
+
263
+ def _transform_typeddict(
264
+ data: Mapping[str, object],
265
+ expected_type: type,
266
+ ) -> Mapping[str, object]:
267
+ result: dict[str, object] = {}
268
+ annotations = get_type_hints(expected_type, include_extras=True)
269
+ for key, value in data.items():
270
+ if not is_given(value):
271
+ # we don't need to include omitted values here as they'll
272
+ # be stripped out before the request is sent anyway
273
+ continue
274
+
275
+ type_ = annotations.get(key)
276
+ if type_ is None:
277
+ # we do not have a type annotation for this field, leave it as is
278
+ result[key] = value
279
+ else:
280
+ result[_maybe_transform_key(key, type_)] = _transform_recursive(value, annotation=type_)
281
+ return result
282
+
283
+
284
+ async def async_maybe_transform(
285
+ data: object,
286
+ expected_type: object,
287
+ ) -> Any | None:
288
+ """Wrapper over `async_transform()` that allows `None` to be passed.
289
+
290
+ See `async_transform()` for more details.
291
+ """
292
+ if data is None:
293
+ return None
294
+ return await async_transform(data, expected_type)
295
+
296
+
297
+ async def async_transform(
298
+ data: _T,
299
+ expected_type: object,
300
+ ) -> _T:
301
+ """Transform dictionaries based off of type information from the given type, for example:
302
+
303
+ ```py
304
+ class Params(TypedDict, total=False):
305
+ card_id: Required[Annotated[str, PropertyInfo(alias="cardID")]]
306
+
307
+
308
+ transformed = transform({"card_id": "<my card ID>"}, Params)
309
+ # {'cardID': '<my card ID>'}
310
+ ```
311
+
312
+ Any keys / data that does not have type information given will be included as is.
313
+
314
+ It should be noted that the transformations that this function does are not represented in the type system.
315
+ """
316
+ transformed = await _async_transform_recursive(data, annotation=cast(type, expected_type))
317
+ return cast(_T, transformed)
318
+
319
+
320
+ async def _async_transform_recursive(
321
+ data: object,
322
+ *,
323
+ annotation: type,
324
+ inner_type: type | None = None,
325
+ ) -> object:
326
+ """Transform the given data against the expected type.
327
+
328
+ Args:
329
+ annotation: The direct type annotation given to the particular piece of data.
330
+ This may or may not be wrapped in metadata types, e.g. `Required[T]`, `Annotated[T, ...]` etc
331
+
332
+ inner_type: If applicable, this is the "inside" type. This is useful in certain cases where the outside type
333
+ is a container type such as `List[T]`. In that case `inner_type` should be set to `T` so that each entry in
334
+ the list can be transformed using the metadata from the container type.
335
+
336
+ Defaults to the same value as the `annotation` argument.
337
+ """
338
+ from .._compat import model_dump
339
+
340
+ if inner_type is None:
341
+ inner_type = annotation
342
+
343
+ stripped_type = strip_annotated_type(inner_type)
344
+ origin = get_origin(stripped_type) or stripped_type
345
+ if is_typeddict(stripped_type) and is_mapping(data):
346
+ return await _async_transform_typeddict(data, stripped_type)
347
+
348
+ if origin == dict and is_mapping(data):
349
+ items_type = get_args(stripped_type)[1]
350
+ return {key: _transform_recursive(value, annotation=items_type) for key, value in data.items()}
351
+
352
+ if (
353
+ # List[T]
354
+ (is_list_type(stripped_type) and is_list(data))
355
+ # Iterable[T]
356
+ or (is_iterable_type(stripped_type) and is_iterable(data) and not isinstance(data, str))
357
+ # Sequence[T]
358
+ or (is_sequence_type(stripped_type) and is_sequence(data) and not isinstance(data, str))
359
+ ):
360
+ # dicts are technically iterable, but it is an iterable on the keys of the dict and is not usually
361
+ # intended as an iterable, so we don't transform it.
362
+ if isinstance(data, dict):
363
+ return cast(object, data)
364
+
365
+ inner_type = extract_type_arg(stripped_type, 0)
366
+ if _no_transform_needed(inner_type):
367
+ # for some types there is no need to transform anything, so we can get a small
368
+ # perf boost from skipping that work.
369
+ #
370
+ # but we still need to convert to a list to ensure the data is json-serializable
371
+ if is_list(data):
372
+ return data
373
+ return list(data)
374
+
375
+ return [await _async_transform_recursive(d, annotation=annotation, inner_type=inner_type) for d in data]
376
+
377
+ if is_union_type(stripped_type):
378
+ # For union types we run the transformation against all subtypes to ensure that everything is transformed.
379
+ #
380
+ # TODO: there may be edge cases where the same normalized field name will transform to two different names
381
+ # in different subtypes.
382
+ for subtype in get_args(stripped_type):
383
+ data = await _async_transform_recursive(data, annotation=annotation, inner_type=subtype)
384
+ return data
385
+
386
+ if isinstance(data, pydantic.BaseModel):
387
+ return model_dump(data, exclude_unset=True, mode="json")
388
+
389
+ annotated_type = _get_annotated_type(annotation)
390
+ if annotated_type is None:
391
+ return data
392
+
393
+ # ignore the first argument as it is the actual type
394
+ annotations = get_args(annotated_type)[1:]
395
+ for annotation in annotations:
396
+ if isinstance(annotation, PropertyInfo) and annotation.format is not None:
397
+ return await _async_format_data(data, annotation.format, annotation.format_template)
398
+
399
+ return data
400
+
401
+
402
+ async def _async_format_data(data: object, format_: PropertyFormat, format_template: str | None) -> object:
403
+ if isinstance(data, (date, datetime)):
404
+ if format_ == "iso8601":
405
+ return data.isoformat()
406
+
407
+ if format_ == "custom" and format_template is not None:
408
+ return data.strftime(format_template)
409
+
410
+ if format_ == "base64" and is_base64_file_input(data):
411
+ binary: str | bytes | None = None
412
+
413
+ if isinstance(data, pathlib.Path):
414
+ binary = await anyio.Path(data).read_bytes()
415
+ elif isinstance(data, io.IOBase):
416
+ binary = data.read()
417
+
418
+ if isinstance(binary, str): # type: ignore[unreachable]
419
+ binary = binary.encode()
420
+
421
+ if not isinstance(binary, bytes):
422
+ raise RuntimeError(f"Could not read bytes from {data}; Received {type(binary)}")
423
+
424
+ return base64.b64encode(binary).decode("ascii")
425
+
426
+ return data
427
+
428
+
429
+ async def _async_transform_typeddict(
430
+ data: Mapping[str, object],
431
+ expected_type: type,
432
+ ) -> Mapping[str, object]:
433
+ result: dict[str, object] = {}
434
+ annotations = get_type_hints(expected_type, include_extras=True)
435
+ for key, value in data.items():
436
+ if not is_given(value):
437
+ # we don't need to include omitted values here as they'll
438
+ # be stripped out before the request is sent anyway
439
+ continue
440
+
441
+ type_ = annotations.get(key)
442
+ if type_ is None:
443
+ # we do not have a type annotation for this field, leave it as is
444
+ result[key] = value
445
+ else:
446
+ result[_maybe_transform_key(key, type_)] = await _async_transform_recursive(value, annotation=type_)
447
+ return result
448
+
449
+
450
+ @lru_cache(maxsize=8096)
451
+ def get_type_hints(
452
+ obj: Any,
453
+ globalns: dict[str, Any] | None = None,
454
+ localns: Mapping[str, Any] | None = None,
455
+ include_extras: bool = False,
456
+ ) -> dict[str, Any]:
457
+ return _get_type_hints(obj, globalns=globalns, localns=localns, include_extras=include_extras)
@@ -0,0 +1,156 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ import typing
5
+ import typing_extensions
6
+ from typing import Any, TypeVar, Iterable, cast
7
+ from collections import abc as _c_abc
8
+ from typing_extensions import (
9
+ TypeIs,
10
+ Required,
11
+ Annotated,
12
+ get_args,
13
+ get_origin,
14
+ )
15
+
16
+ from ._utils import lru_cache
17
+ from .._types import InheritsGeneric
18
+ from ._compat import is_union as _is_union
19
+
20
+
21
+ def is_annotated_type(typ: type) -> bool:
22
+ return get_origin(typ) == Annotated
23
+
24
+
25
+ def is_list_type(typ: type) -> bool:
26
+ return (get_origin(typ) or typ) == list
27
+
28
+
29
+ def is_sequence_type(typ: type) -> bool:
30
+ origin = get_origin(typ) or typ
31
+ return origin == typing_extensions.Sequence or origin == typing.Sequence or origin == _c_abc.Sequence
32
+
33
+
34
+ def is_iterable_type(typ: type) -> bool:
35
+ """If the given type is `typing.Iterable[T]`"""
36
+ origin = get_origin(typ) or typ
37
+ return origin == Iterable or origin == _c_abc.Iterable
38
+
39
+
40
+ def is_union_type(typ: type) -> bool:
41
+ return _is_union(get_origin(typ))
42
+
43
+
44
+ def is_required_type(typ: type) -> bool:
45
+ return get_origin(typ) == Required
46
+
47
+
48
+ def is_typevar(typ: type) -> bool:
49
+ # type ignore is required because type checkers
50
+ # think this expression will always return False
51
+ return type(typ) == TypeVar # type: ignore
52
+
53
+
54
+ _TYPE_ALIAS_TYPES: tuple[type[typing_extensions.TypeAliasType], ...] = (typing_extensions.TypeAliasType,)
55
+ if sys.version_info >= (3, 12):
56
+ _TYPE_ALIAS_TYPES = (*_TYPE_ALIAS_TYPES, typing.TypeAliasType)
57
+
58
+
59
+ def is_type_alias_type(tp: Any, /) -> TypeIs[typing_extensions.TypeAliasType]:
60
+ """Return whether the provided argument is an instance of `TypeAliasType`.
61
+
62
+ ```python
63
+ type Int = int
64
+ is_type_alias_type(Int)
65
+ # > True
66
+ Str = TypeAliasType("Str", str)
67
+ is_type_alias_type(Str)
68
+ # > True
69
+ ```
70
+ """
71
+ return isinstance(tp, _TYPE_ALIAS_TYPES)
72
+
73
+
74
+ # Extracts T from Annotated[T, ...] or from Required[Annotated[T, ...]]
75
+ @lru_cache(maxsize=8096)
76
+ def strip_annotated_type(typ: type) -> type:
77
+ if is_required_type(typ) or is_annotated_type(typ):
78
+ return strip_annotated_type(cast(type, get_args(typ)[0]))
79
+
80
+ return typ
81
+
82
+
83
+ def extract_type_arg(typ: type, index: int) -> type:
84
+ args = get_args(typ)
85
+ try:
86
+ return cast(type, args[index])
87
+ except IndexError as err:
88
+ raise RuntimeError(f"Expected type {typ} to have a type argument at index {index} but it did not") from err
89
+
90
+
91
+ def extract_type_var_from_base(
92
+ typ: type,
93
+ *,
94
+ generic_bases: tuple[type, ...],
95
+ index: int,
96
+ failure_message: str | None = None,
97
+ ) -> type:
98
+ """Given a type like `Foo[T]`, returns the generic type variable `T`.
99
+
100
+ This also handles the case where a concrete subclass is given, e.g.
101
+ ```py
102
+ class MyResponse(Foo[bytes]):
103
+ ...
104
+
105
+ extract_type_var(MyResponse, bases=(Foo,), index=0) -> bytes
106
+ ```
107
+
108
+ And where a generic subclass is given:
109
+ ```py
110
+ _T = TypeVar('_T')
111
+ class MyResponse(Foo[_T]):
112
+ ...
113
+
114
+ extract_type_var(MyResponse[bytes], bases=(Foo,), index=0) -> bytes
115
+ ```
116
+ """
117
+ cls = cast(object, get_origin(typ) or typ)
118
+ if cls in generic_bases: # pyright: ignore[reportUnnecessaryContains]
119
+ # we're given the class directly
120
+ return extract_type_arg(typ, index)
121
+
122
+ # if a subclass is given
123
+ # ---
124
+ # this is needed as __orig_bases__ is not present in the typeshed stubs
125
+ # because it is intended to be for internal use only, however there does
126
+ # not seem to be a way to resolve generic TypeVars for inherited subclasses
127
+ # without using it.
128
+ if isinstance(cls, InheritsGeneric):
129
+ target_base_class: Any | None = None
130
+ for base in cls.__orig_bases__:
131
+ if base.__origin__ in generic_bases:
132
+ target_base_class = base
133
+ break
134
+
135
+ if target_base_class is None:
136
+ raise RuntimeError(
137
+ "Could not find the generic base class;\n"
138
+ "This should never happen;\n"
139
+ f"Does {cls} inherit from one of {generic_bases} ?"
140
+ )
141
+
142
+ extracted = extract_type_arg(target_base_class, index)
143
+ if is_typevar(extracted):
144
+ # If the extracted type argument is itself a type variable
145
+ # then that means the subclass itself is generic, so we have
146
+ # to resolve the type argument from the class itself, not
147
+ # the base class.
148
+ #
149
+ # Note: if there is more than 1 type argument, the subclass could
150
+ # change the ordering of the type arguments, this is not currently
151
+ # supported.
152
+ return extract_type_arg(typ, index)
153
+
154
+ return extracted
155
+
156
+ raise RuntimeError(failure_message or f"Could not resolve inner type variable at index {index} for {typ}")