openmeter 1.0.0b54__py3-none-any.whl → 2.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.

Potentially problematic release.


This version of openmeter might be problematic. Click here for more details.

Files changed (132) hide show
  1. openmeter/__init__.py +89 -15
  2. openmeter/_base_client.py +2041 -0
  3. openmeter/_client.py +518 -70
  4. openmeter/_compat.py +221 -0
  5. openmeter/_constants.py +14 -0
  6. openmeter/_exceptions.py +108 -0
  7. openmeter/_files.py +127 -0
  8. openmeter/_models.py +777 -0
  9. openmeter/_qs.py +150 -0
  10. openmeter/_resource.py +43 -0
  11. openmeter/_response.py +820 -0
  12. openmeter/_streaming.py +333 -0
  13. openmeter/_types.py +222 -0
  14. openmeter/_utils/__init__.py +56 -0
  15. openmeter/_utils/_logs.py +25 -0
  16. openmeter/_utils/_proxy.py +63 -0
  17. openmeter/_utils/_reflection.py +42 -0
  18. openmeter/_utils/_streams.py +12 -0
  19. openmeter/_utils/_sync.py +81 -0
  20. openmeter/_utils/_transform.py +387 -0
  21. openmeter/_utils/_typing.py +120 -0
  22. openmeter/_utils/_utils.py +419 -0
  23. openmeter/_version.py +4 -0
  24. openmeter/lib/.keep +4 -0
  25. openmeter/py.typed +0 -1
  26. openmeter/resources/__init__.py +103 -0
  27. openmeter/resources/debug/__init__.py +33 -0
  28. openmeter/resources/debug/debug.py +102 -0
  29. openmeter/resources/debug/metrics.py +146 -0
  30. openmeter/resources/entitlements/__init__.py +47 -0
  31. openmeter/resources/entitlements/entitlements.py +450 -0
  32. openmeter/resources/entitlements/features.py +578 -0
  33. openmeter/resources/entitlements/grants.py +389 -0
  34. openmeter/resources/events.py +442 -0
  35. openmeter/resources/meters/__init__.py +33 -0
  36. openmeter/resources/meters/meters.py +666 -0
  37. openmeter/resources/meters/subjects.py +163 -0
  38. openmeter/resources/notifications/__init__.py +75 -0
  39. openmeter/resources/notifications/channels.py +686 -0
  40. openmeter/resources/notifications/events.py +365 -0
  41. openmeter/resources/notifications/notifications.py +198 -0
  42. openmeter/resources/notifications/rules.py +781 -0
  43. openmeter/resources/notifications/webhook.py +208 -0
  44. openmeter/resources/portal/__init__.py +47 -0
  45. openmeter/resources/portal/meters.py +230 -0
  46. openmeter/resources/portal/portal.py +112 -0
  47. openmeter/resources/portal/tokens.py +359 -0
  48. openmeter/resources/subjects/entitlements/__init__.py +33 -0
  49. openmeter/resources/subjects/entitlements/entitlements.py +1881 -0
  50. openmeter/resources/subjects/entitlements/grants.py +453 -0
  51. openmeter/resources/subjects.py +419 -0
  52. openmeter/types/__init__.py +21 -0
  53. openmeter/types/debug/__init__.py +5 -0
  54. openmeter/types/debug/metric_list_response.py +7 -0
  55. openmeter/types/entitlement.py +238 -0
  56. openmeter/types/entitlements/__init__.py +11 -0
  57. openmeter/types/entitlements/feature.py +61 -0
  58. openmeter/types/entitlements/feature_create_params.py +43 -0
  59. openmeter/types/entitlements/feature_list_params.py +23 -0
  60. openmeter/types/entitlements/grant_list_params.py +57 -0
  61. openmeter/types/entitlements/grant_list_response.py +11 -0
  62. openmeter/types/entitlements/grant_paginated_response.py +24 -0
  63. openmeter/types/entitlements/list_features_result.py +28 -0
  64. openmeter/types/event_ingest_params.py +46 -0
  65. openmeter/types/event_ingest_response.py +43 -0
  66. openmeter/types/event_list_params.py +22 -0
  67. openmeter/types/event_list_response.py +9 -0
  68. openmeter/types/ingested_event.py +59 -0
  69. openmeter/types/list_entitlements_result.py +28 -0
  70. openmeter/types/meter.py +53 -0
  71. openmeter/types/meter_create_params.py +50 -0
  72. openmeter/types/meter_list_response.py +9 -0
  73. openmeter/types/meter_query_params.py +50 -0
  74. openmeter/types/meter_query_result.py +35 -0
  75. openmeter/types/meters/__init__.py +5 -0
  76. openmeter/types/meters/subject_list_response.py +8 -0
  77. openmeter/types/notifications/__init__.py +18 -0
  78. openmeter/types/notifications/channel_create_params.py +34 -0
  79. openmeter/types/notifications/channel_list_params.py +41 -0
  80. openmeter/types/notifications/channel_list_response.py +24 -0
  81. openmeter/types/notifications/channel_update_params.py +34 -0
  82. openmeter/types/notifications/event_list_params.py +61 -0
  83. openmeter/types/notifications/event_list_response.py +24 -0
  84. openmeter/types/notifications/notification_channel.py +47 -0
  85. openmeter/types/notifications/notification_event.py +215 -0
  86. openmeter/types/notifications/notification_rule.py +70 -0
  87. openmeter/types/notifications/rule_create_params.py +39 -0
  88. openmeter/types/notifications/rule_list_params.py +54 -0
  89. openmeter/types/notifications/rule_list_response.py +24 -0
  90. openmeter/types/notifications/rule_update_params.py +39 -0
  91. openmeter/types/notifications/webhook_svix_params.py +26 -0
  92. openmeter/types/portal/__init__.py +10 -0
  93. openmeter/types/portal/meter_query_params.py +44 -0
  94. openmeter/types/portal/portal_token.py +28 -0
  95. openmeter/types/portal/token_create_params.py +17 -0
  96. openmeter/types/portal/token_invalidate_params.py +15 -0
  97. openmeter/types/portal/token_list_params.py +12 -0
  98. openmeter/types/portal/token_list_response.py +9 -0
  99. openmeter/types/shared/__init__.py +3 -0
  100. openmeter/types/subject.py +37 -0
  101. openmeter/types/subject_list_response.py +9 -0
  102. openmeter/types/subject_param.py +27 -0
  103. openmeter/types/subject_upsert_params.py +39 -0
  104. openmeter/types/subject_upsert_response.py +10 -0
  105. openmeter/types/subjects/__init__.py +13 -0
  106. openmeter/types/subjects/entitlement_history_params.py +35 -0
  107. openmeter/types/subjects/entitlement_history_response.py +98 -0
  108. openmeter/types/subjects/entitlement_list_response.py +10 -0
  109. openmeter/types/subjects/entitlements/__init__.py +8 -0
  110. openmeter/types/subjects/entitlements/entitlement_grant.py +103 -0
  111. openmeter/types/subjects/entitlements/grant_list_response.py +10 -0
  112. openmeter-2.0.0.dist-info/METADATA +396 -0
  113. openmeter-2.0.0.dist-info/RECORD +115 -0
  114. {openmeter-1.0.0b54.dist-info → openmeter-2.0.0.dist-info}/WHEEL +1 -1
  115. openmeter-2.0.0.dist-info/licenses/LICENSE +201 -0
  116. openmeter/_configuration.py +0 -36
  117. openmeter/_operations/__init__.py +0 -17
  118. openmeter/_operations/_operations.py +0 -2105
  119. openmeter/_operations/_patch.py +0 -20
  120. openmeter/_patch.py +0 -20
  121. openmeter/_serialization.py +0 -2008
  122. openmeter/_vendor.py +0 -24
  123. openmeter/aio/__init__.py +0 -21
  124. openmeter/aio/_client.py +0 -83
  125. openmeter/aio/_configuration.py +0 -36
  126. openmeter/aio/_operations/__init__.py +0 -17
  127. openmeter/aio/_operations/_operations.py +0 -1778
  128. openmeter/aio/_operations/_patch.py +0 -20
  129. openmeter/aio/_patch.py +0 -20
  130. openmeter/aio/_vendor.py +0 -24
  131. openmeter-1.0.0b54.dist-info/METADATA +0 -92
  132. openmeter-1.0.0b54.dist-info/RECORD +0 -21
openmeter/_response.py ADDED
@@ -0,0 +1,820 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import inspect
5
+ import logging
6
+ import datetime
7
+ import functools
8
+ from types import TracebackType
9
+ from typing import (
10
+ TYPE_CHECKING,
11
+ Any,
12
+ Union,
13
+ Generic,
14
+ TypeVar,
15
+ Callable,
16
+ Iterator,
17
+ AsyncIterator,
18
+ cast,
19
+ overload,
20
+ )
21
+ from typing_extensions import Awaitable, ParamSpec, override, get_origin
22
+
23
+ import anyio
24
+ import httpx
25
+ import pydantic
26
+
27
+ from ._types import NoneType
28
+ from ._utils import is_given, extract_type_arg, is_annotated_type, extract_type_var_from_base
29
+ from ._models import BaseModel, is_basemodel
30
+ from ._constants import RAW_RESPONSE_HEADER, OVERRIDE_CAST_TO_HEADER
31
+ from ._streaming import Stream, AsyncStream, is_stream_class_type, extract_stream_chunk_type
32
+ from ._exceptions import OpenMeterError, APIResponseValidationError
33
+
34
+ if TYPE_CHECKING:
35
+ from ._models import FinalRequestOptions
36
+ from ._base_client import BaseClient
37
+
38
+
39
+ P = ParamSpec("P")
40
+ R = TypeVar("R")
41
+ _T = TypeVar("_T")
42
+ _APIResponseT = TypeVar("_APIResponseT", bound="APIResponse[Any]")
43
+ _AsyncAPIResponseT = TypeVar("_AsyncAPIResponseT", bound="AsyncAPIResponse[Any]")
44
+
45
+ log: logging.Logger = logging.getLogger(__name__)
46
+
47
+
48
+ class BaseAPIResponse(Generic[R]):
49
+ _cast_to: type[R]
50
+ _client: BaseClient[Any, Any]
51
+ _parsed_by_type: dict[type[Any], Any]
52
+ _is_sse_stream: bool
53
+ _stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None
54
+ _options: FinalRequestOptions
55
+
56
+ http_response: httpx.Response
57
+
58
+ def __init__(
59
+ self,
60
+ *,
61
+ raw: httpx.Response,
62
+ cast_to: type[R],
63
+ client: BaseClient[Any, Any],
64
+ stream: bool,
65
+ stream_cls: type[Stream[Any]] | type[AsyncStream[Any]] | None,
66
+ options: FinalRequestOptions,
67
+ ) -> None:
68
+ self._cast_to = cast_to
69
+ self._client = client
70
+ self._parsed_by_type = {}
71
+ self._is_sse_stream = stream
72
+ self._stream_cls = stream_cls
73
+ self._options = options
74
+ self.http_response = raw
75
+
76
+ @property
77
+ def headers(self) -> httpx.Headers:
78
+ return self.http_response.headers
79
+
80
+ @property
81
+ def http_request(self) -> httpx.Request:
82
+ """Returns the httpx Request instance associated with the current response."""
83
+ return self.http_response.request
84
+
85
+ @property
86
+ def status_code(self) -> int:
87
+ return self.http_response.status_code
88
+
89
+ @property
90
+ def url(self) -> httpx.URL:
91
+ """Returns the URL for which the request was made."""
92
+ return self.http_response.url
93
+
94
+ @property
95
+ def method(self) -> str:
96
+ return self.http_request.method
97
+
98
+ @property
99
+ def http_version(self) -> str:
100
+ return self.http_response.http_version
101
+
102
+ @property
103
+ def elapsed(self) -> datetime.timedelta:
104
+ """The time taken for the complete request/response cycle to complete."""
105
+ return self.http_response.elapsed
106
+
107
+ @property
108
+ def is_closed(self) -> bool:
109
+ """Whether or not the response body has been closed.
110
+
111
+ If this is False then there is response data that has not been read yet.
112
+ You must either fully consume the response body or call `.close()`
113
+ before discarding the response to prevent resource leaks.
114
+ """
115
+ return self.http_response.is_closed
116
+
117
+ @override
118
+ def __repr__(self) -> str:
119
+ return (
120
+ f"<{self.__class__.__name__} [{self.status_code} {self.http_response.reason_phrase}] type={self._cast_to}>"
121
+ )
122
+
123
+ def _parse(self, *, to: type[_T] | None = None) -> R | _T:
124
+ # unwrap `Annotated[T, ...]` -> `T`
125
+ if to and is_annotated_type(to):
126
+ to = extract_type_arg(to, 0)
127
+
128
+ if self._is_sse_stream:
129
+ if to:
130
+ if not is_stream_class_type(to):
131
+ raise TypeError(f"Expected custom parse type to be a subclass of {Stream} or {AsyncStream}")
132
+
133
+ return cast(
134
+ _T,
135
+ to(
136
+ cast_to=extract_stream_chunk_type(
137
+ to,
138
+ failure_message="Expected custom stream type to be passed with a type argument, e.g. Stream[ChunkType]",
139
+ ),
140
+ response=self.http_response,
141
+ client=cast(Any, self._client),
142
+ ),
143
+ )
144
+
145
+ if self._stream_cls:
146
+ return cast(
147
+ R,
148
+ self._stream_cls(
149
+ cast_to=extract_stream_chunk_type(self._stream_cls),
150
+ response=self.http_response,
151
+ client=cast(Any, self._client),
152
+ ),
153
+ )
154
+
155
+ stream_cls = cast("type[Stream[Any]] | type[AsyncStream[Any]] | None", self._client._default_stream_cls)
156
+ if stream_cls is None:
157
+ raise MissingStreamClassError()
158
+
159
+ return cast(
160
+ R,
161
+ stream_cls(
162
+ cast_to=self._cast_to,
163
+ response=self.http_response,
164
+ client=cast(Any, self._client),
165
+ ),
166
+ )
167
+
168
+ cast_to = to if to is not None else self._cast_to
169
+
170
+ # unwrap `Annotated[T, ...]` -> `T`
171
+ if is_annotated_type(cast_to):
172
+ cast_to = extract_type_arg(cast_to, 0)
173
+
174
+ if cast_to is NoneType:
175
+ return cast(R, None)
176
+
177
+ response = self.http_response
178
+ if cast_to == str:
179
+ return cast(R, response.text)
180
+
181
+ if cast_to == bytes:
182
+ return cast(R, response.content)
183
+
184
+ if cast_to == int:
185
+ return cast(R, int(response.text))
186
+
187
+ if cast_to == float:
188
+ return cast(R, float(response.text))
189
+
190
+ origin = get_origin(cast_to) or cast_to
191
+
192
+ if origin == APIResponse:
193
+ raise RuntimeError("Unexpected state - cast_to is `APIResponse`")
194
+
195
+ if inspect.isclass(origin) and issubclass(origin, httpx.Response):
196
+ # Because of the invariance of our ResponseT TypeVar, users can subclass httpx.Response
197
+ # and pass that class to our request functions. We cannot change the variance to be either
198
+ # covariant or contravariant as that makes our usage of ResponseT illegal. We could construct
199
+ # the response class ourselves but that is something that should be supported directly in httpx
200
+ # as it would be easy to incorrectly construct the Response object due to the multitude of arguments.
201
+ if cast_to != httpx.Response:
202
+ raise ValueError(f"Subclasses of httpx.Response cannot be passed to `cast_to`")
203
+ return cast(R, response)
204
+
205
+ if inspect.isclass(origin) and not issubclass(origin, BaseModel) and issubclass(origin, pydantic.BaseModel):
206
+ raise TypeError("Pydantic models must subclass our base model type, e.g. `from openmeter import BaseModel`")
207
+
208
+ if (
209
+ cast_to is not object
210
+ and not origin is list
211
+ and not origin is dict
212
+ and not origin is Union
213
+ and not issubclass(origin, BaseModel)
214
+ ):
215
+ raise RuntimeError(
216
+ f"Unsupported type, expected {cast_to} to be a subclass of {BaseModel}, {dict}, {list}, {Union}, {NoneType}, {str} or {httpx.Response}."
217
+ )
218
+
219
+ # split is required to handle cases where additional information is included
220
+ # in the response, e.g. application/json; charset=utf-8
221
+ content_type, *_ = response.headers.get("content-type", "*").split(";")
222
+ if content_type != "application/json":
223
+ if is_basemodel(cast_to):
224
+ try:
225
+ data = response.json()
226
+ except Exception as exc:
227
+ log.debug("Could not read JSON from response data due to %s - %s", type(exc), exc)
228
+ else:
229
+ return self._client._process_response_data(
230
+ data=data,
231
+ cast_to=cast_to, # type: ignore
232
+ response=response,
233
+ )
234
+
235
+ if self._client._strict_response_validation:
236
+ raise APIResponseValidationError(
237
+ response=response,
238
+ message=f"Expected Content-Type response header to be `application/json` but received `{content_type}` instead.",
239
+ body=response.text,
240
+ )
241
+
242
+ # If the API responds with content that isn't JSON then we just return
243
+ # the (decoded) text without performing any parsing so that you can still
244
+ # handle the response however you need to.
245
+ return response.text # type: ignore
246
+
247
+ data = response.json()
248
+
249
+ return self._client._process_response_data(
250
+ data=data,
251
+ cast_to=cast_to, # type: ignore
252
+ response=response,
253
+ )
254
+
255
+
256
+ class APIResponse(BaseAPIResponse[R]):
257
+ @overload
258
+ def parse(self, *, to: type[_T]) -> _T:
259
+ ...
260
+
261
+ @overload
262
+ def parse(self) -> R:
263
+ ...
264
+
265
+ def parse(self, *, to: type[_T] | None = None) -> R | _T:
266
+ """Returns the rich python representation of this response's data.
267
+
268
+ For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`.
269
+
270
+ You can customise the type that the response is parsed into through
271
+ the `to` argument, e.g.
272
+
273
+ ```py
274
+ from openmeter import BaseModel
275
+
276
+
277
+ class MyModel(BaseModel):
278
+ foo: str
279
+
280
+
281
+ obj = response.parse(to=MyModel)
282
+ print(obj.foo)
283
+ ```
284
+
285
+ We support parsing:
286
+ - `BaseModel`
287
+ - `dict`
288
+ - `list`
289
+ - `Union`
290
+ - `str`
291
+ - `int`
292
+ - `float`
293
+ - `httpx.Response`
294
+ """
295
+ cache_key = to if to is not None else self._cast_to
296
+ cached = self._parsed_by_type.get(cache_key)
297
+ if cached is not None:
298
+ return cached # type: ignore[no-any-return]
299
+
300
+ if not self._is_sse_stream:
301
+ self.read()
302
+
303
+ parsed = self._parse(to=to)
304
+ if is_given(self._options.post_parser):
305
+ parsed = self._options.post_parser(parsed)
306
+
307
+ self._parsed_by_type[cache_key] = parsed
308
+ return parsed
309
+
310
+ def read(self) -> bytes:
311
+ """Read and return the binary response content."""
312
+ try:
313
+ return self.http_response.read()
314
+ except httpx.StreamConsumed as exc:
315
+ # The default error raised by httpx isn't very
316
+ # helpful in our case so we re-raise it with
317
+ # a different error message.
318
+ raise StreamAlreadyConsumed() from exc
319
+
320
+ def text(self) -> str:
321
+ """Read and decode the response content into a string."""
322
+ self.read()
323
+ return self.http_response.text
324
+
325
+ def json(self) -> object:
326
+ """Read and decode the JSON response content."""
327
+ self.read()
328
+ return self.http_response.json()
329
+
330
+ def close(self) -> None:
331
+ """Close the response and release the connection.
332
+
333
+ Automatically called if the response body is read to completion.
334
+ """
335
+ self.http_response.close()
336
+
337
+ def iter_bytes(self, chunk_size: int | None = None) -> Iterator[bytes]:
338
+ """
339
+ A byte-iterator over the decoded response content.
340
+
341
+ This automatically handles gzip, deflate and brotli encoded responses.
342
+ """
343
+ for chunk in self.http_response.iter_bytes(chunk_size):
344
+ yield chunk
345
+
346
+ def iter_text(self, chunk_size: int | None = None) -> Iterator[str]:
347
+ """A str-iterator over the decoded response content
348
+ that handles both gzip, deflate, etc but also detects the content's
349
+ string encoding.
350
+ """
351
+ for chunk in self.http_response.iter_text(chunk_size):
352
+ yield chunk
353
+
354
+ def iter_lines(self) -> Iterator[str]:
355
+ """Like `iter_text()` but will only yield chunks for each line"""
356
+ for chunk in self.http_response.iter_lines():
357
+ yield chunk
358
+
359
+
360
+ class AsyncAPIResponse(BaseAPIResponse[R]):
361
+ @overload
362
+ async def parse(self, *, to: type[_T]) -> _T:
363
+ ...
364
+
365
+ @overload
366
+ async def parse(self) -> R:
367
+ ...
368
+
369
+ async def parse(self, *, to: type[_T] | None = None) -> R | _T:
370
+ """Returns the rich python representation of this response's data.
371
+
372
+ For lower-level control, see `.read()`, `.json()`, `.iter_bytes()`.
373
+
374
+ You can customise the type that the response is parsed into through
375
+ the `to` argument, e.g.
376
+
377
+ ```py
378
+ from openmeter import BaseModel
379
+
380
+
381
+ class MyModel(BaseModel):
382
+ foo: str
383
+
384
+
385
+ obj = response.parse(to=MyModel)
386
+ print(obj.foo)
387
+ ```
388
+
389
+ We support parsing:
390
+ - `BaseModel`
391
+ - `dict`
392
+ - `list`
393
+ - `Union`
394
+ - `str`
395
+ - `httpx.Response`
396
+ """
397
+ cache_key = to if to is not None else self._cast_to
398
+ cached = self._parsed_by_type.get(cache_key)
399
+ if cached is not None:
400
+ return cached # type: ignore[no-any-return]
401
+
402
+ if not self._is_sse_stream:
403
+ await self.read()
404
+
405
+ parsed = self._parse(to=to)
406
+ if is_given(self._options.post_parser):
407
+ parsed = self._options.post_parser(parsed)
408
+
409
+ self._parsed_by_type[cache_key] = parsed
410
+ return parsed
411
+
412
+ async def read(self) -> bytes:
413
+ """Read and return the binary response content."""
414
+ try:
415
+ return await self.http_response.aread()
416
+ except httpx.StreamConsumed as exc:
417
+ # the default error raised by httpx isn't very
418
+ # helpful in our case so we re-raise it with
419
+ # a different error message
420
+ raise StreamAlreadyConsumed() from exc
421
+
422
+ async def text(self) -> str:
423
+ """Read and decode the response content into a string."""
424
+ await self.read()
425
+ return self.http_response.text
426
+
427
+ async def json(self) -> object:
428
+ """Read and decode the JSON response content."""
429
+ await self.read()
430
+ return self.http_response.json()
431
+
432
+ async def close(self) -> None:
433
+ """Close the response and release the connection.
434
+
435
+ Automatically called if the response body is read to completion.
436
+ """
437
+ await self.http_response.aclose()
438
+
439
+ async def iter_bytes(self, chunk_size: int | None = None) -> AsyncIterator[bytes]:
440
+ """
441
+ A byte-iterator over the decoded response content.
442
+
443
+ This automatically handles gzip, deflate and brotli encoded responses.
444
+ """
445
+ async for chunk in self.http_response.aiter_bytes(chunk_size):
446
+ yield chunk
447
+
448
+ async def iter_text(self, chunk_size: int | None = None) -> AsyncIterator[str]:
449
+ """A str-iterator over the decoded response content
450
+ that handles both gzip, deflate, etc but also detects the content's
451
+ string encoding.
452
+ """
453
+ async for chunk in self.http_response.aiter_text(chunk_size):
454
+ yield chunk
455
+
456
+ async def iter_lines(self) -> AsyncIterator[str]:
457
+ """Like `iter_text()` but will only yield chunks for each line"""
458
+ async for chunk in self.http_response.aiter_lines():
459
+ yield chunk
460
+
461
+
462
+ class BinaryAPIResponse(APIResponse[bytes]):
463
+ """Subclass of APIResponse providing helpers for dealing with binary data.
464
+
465
+ Note: If you want to stream the response data instead of eagerly reading it
466
+ all at once then you should use `.with_streaming_response` when making
467
+ the API request, e.g. `.with_streaming_response.get_binary_response()`
468
+ """
469
+
470
+ def write_to_file(
471
+ self,
472
+ file: str | os.PathLike[str],
473
+ ) -> None:
474
+ """Write the output to the given file.
475
+
476
+ Accepts a filename or any path-like object, e.g. pathlib.Path
477
+
478
+ Note: if you want to stream the data to the file instead of writing
479
+ all at once then you should use `.with_streaming_response` when making
480
+ the API request, e.g. `.with_streaming_response.get_binary_response()`
481
+ """
482
+ with open(file, mode="wb") as f:
483
+ for data in self.iter_bytes():
484
+ f.write(data)
485
+
486
+
487
+ class AsyncBinaryAPIResponse(AsyncAPIResponse[bytes]):
488
+ """Subclass of APIResponse providing helpers for dealing with binary data.
489
+
490
+ Note: If you want to stream the response data instead of eagerly reading it
491
+ all at once then you should use `.with_streaming_response` when making
492
+ the API request, e.g. `.with_streaming_response.get_binary_response()`
493
+ """
494
+
495
+ async def write_to_file(
496
+ self,
497
+ file: str | os.PathLike[str],
498
+ ) -> None:
499
+ """Write the output to the given file.
500
+
501
+ Accepts a filename or any path-like object, e.g. pathlib.Path
502
+
503
+ Note: if you want to stream the data to the file instead of writing
504
+ all at once then you should use `.with_streaming_response` when making
505
+ the API request, e.g. `.with_streaming_response.get_binary_response()`
506
+ """
507
+ path = anyio.Path(file)
508
+ async with await path.open(mode="wb") as f:
509
+ async for data in self.iter_bytes():
510
+ await f.write(data)
511
+
512
+
513
+ class StreamedBinaryAPIResponse(APIResponse[bytes]):
514
+ def stream_to_file(
515
+ self,
516
+ file: str | os.PathLike[str],
517
+ *,
518
+ chunk_size: int | None = None,
519
+ ) -> None:
520
+ """Streams the output to the given file.
521
+
522
+ Accepts a filename or any path-like object, e.g. pathlib.Path
523
+ """
524
+ with open(file, mode="wb") as f:
525
+ for data in self.iter_bytes(chunk_size):
526
+ f.write(data)
527
+
528
+
529
+ class AsyncStreamedBinaryAPIResponse(AsyncAPIResponse[bytes]):
530
+ async def stream_to_file(
531
+ self,
532
+ file: str | os.PathLike[str],
533
+ *,
534
+ chunk_size: int | None = None,
535
+ ) -> None:
536
+ """Streams the output to the given file.
537
+
538
+ Accepts a filename or any path-like object, e.g. pathlib.Path
539
+ """
540
+ path = anyio.Path(file)
541
+ async with await path.open(mode="wb") as f:
542
+ async for data in self.iter_bytes(chunk_size):
543
+ await f.write(data)
544
+
545
+
546
+ class MissingStreamClassError(TypeError):
547
+ def __init__(self) -> None:
548
+ super().__init__(
549
+ "The `stream` argument was set to `True` but the `stream_cls` argument was not given. See `openmeter._streaming` for reference",
550
+ )
551
+
552
+
553
+ class StreamAlreadyConsumed(OpenMeterError):
554
+ """
555
+ Attempted to read or stream content, but the content has already
556
+ been streamed.
557
+
558
+ This can happen if you use a method like `.iter_lines()` and then attempt
559
+ to read th entire response body afterwards, e.g.
560
+
561
+ ```py
562
+ response = await client.post(...)
563
+ async for line in response.iter_lines():
564
+ ... # do something with `line`
565
+
566
+ content = await response.read()
567
+ # ^ error
568
+ ```
569
+
570
+ If you want this behaviour you'll need to either manually accumulate the response
571
+ content or call `await response.read()` before iterating over the stream.
572
+ """
573
+
574
+ def __init__(self) -> None:
575
+ message = (
576
+ "Attempted to read or stream some content, but the content has "
577
+ "already been streamed. "
578
+ "This could be due to attempting to stream the response "
579
+ "content more than once."
580
+ "\n\n"
581
+ "You can fix this by manually accumulating the response content while streaming "
582
+ "or by calling `.read()` before starting to stream."
583
+ )
584
+ super().__init__(message)
585
+
586
+
587
+ class ResponseContextManager(Generic[_APIResponseT]):
588
+ """Context manager for ensuring that a request is not made
589
+ until it is entered and that the response will always be closed
590
+ when the context manager exits
591
+ """
592
+
593
+ def __init__(self, request_func: Callable[[], _APIResponseT]) -> None:
594
+ self._request_func = request_func
595
+ self.__response: _APIResponseT | None = None
596
+
597
+ def __enter__(self) -> _APIResponseT:
598
+ self.__response = self._request_func()
599
+ return self.__response
600
+
601
+ def __exit__(
602
+ self,
603
+ exc_type: type[BaseException] | None,
604
+ exc: BaseException | None,
605
+ exc_tb: TracebackType | None,
606
+ ) -> None:
607
+ if self.__response is not None:
608
+ self.__response.close()
609
+
610
+
611
+ class AsyncResponseContextManager(Generic[_AsyncAPIResponseT]):
612
+ """Context manager for ensuring that a request is not made
613
+ until it is entered and that the response will always be closed
614
+ when the context manager exits
615
+ """
616
+
617
+ def __init__(self, api_request: Awaitable[_AsyncAPIResponseT]) -> None:
618
+ self._api_request = api_request
619
+ self.__response: _AsyncAPIResponseT | None = None
620
+
621
+ async def __aenter__(self) -> _AsyncAPIResponseT:
622
+ self.__response = await self._api_request
623
+ return self.__response
624
+
625
+ async def __aexit__(
626
+ self,
627
+ exc_type: type[BaseException] | None,
628
+ exc: BaseException | None,
629
+ exc_tb: TracebackType | None,
630
+ ) -> None:
631
+ if self.__response is not None:
632
+ await self.__response.close()
633
+
634
+
635
+ def to_streamed_response_wrapper(func: Callable[P, R]) -> Callable[P, ResponseContextManager[APIResponse[R]]]:
636
+ """Higher order function that takes one of our bound API methods and wraps it
637
+ to support streaming and returning the raw `APIResponse` object directly.
638
+ """
639
+
640
+ @functools.wraps(func)
641
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[APIResponse[R]]:
642
+ extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
643
+ extra_headers[RAW_RESPONSE_HEADER] = "stream"
644
+
645
+ kwargs["extra_headers"] = extra_headers
646
+
647
+ make_request = functools.partial(func, *args, **kwargs)
648
+
649
+ return ResponseContextManager(cast(Callable[[], APIResponse[R]], make_request))
650
+
651
+ return wrapped
652
+
653
+
654
+ def async_to_streamed_response_wrapper(
655
+ func: Callable[P, Awaitable[R]],
656
+ ) -> Callable[P, AsyncResponseContextManager[AsyncAPIResponse[R]]]:
657
+ """Higher order function that takes one of our bound API methods and wraps it
658
+ to support streaming and returning the raw `APIResponse` object directly.
659
+ """
660
+
661
+ @functools.wraps(func)
662
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[AsyncAPIResponse[R]]:
663
+ extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
664
+ extra_headers[RAW_RESPONSE_HEADER] = "stream"
665
+
666
+ kwargs["extra_headers"] = extra_headers
667
+
668
+ make_request = func(*args, **kwargs)
669
+
670
+ return AsyncResponseContextManager(cast(Awaitable[AsyncAPIResponse[R]], make_request))
671
+
672
+ return wrapped
673
+
674
+
675
+ def to_custom_streamed_response_wrapper(
676
+ func: Callable[P, object],
677
+ response_cls: type[_APIResponseT],
678
+ ) -> Callable[P, ResponseContextManager[_APIResponseT]]:
679
+ """Higher order function that takes one of our bound API methods and an `APIResponse` class
680
+ and wraps the method to support streaming and returning the given response class directly.
681
+
682
+ Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
683
+ """
684
+
685
+ @functools.wraps(func)
686
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> ResponseContextManager[_APIResponseT]:
687
+ extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
688
+ extra_headers[RAW_RESPONSE_HEADER] = "stream"
689
+ extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
690
+
691
+ kwargs["extra_headers"] = extra_headers
692
+
693
+ make_request = functools.partial(func, *args, **kwargs)
694
+
695
+ return ResponseContextManager(cast(Callable[[], _APIResponseT], make_request))
696
+
697
+ return wrapped
698
+
699
+
700
+ def async_to_custom_streamed_response_wrapper(
701
+ func: Callable[P, Awaitable[object]],
702
+ response_cls: type[_AsyncAPIResponseT],
703
+ ) -> Callable[P, AsyncResponseContextManager[_AsyncAPIResponseT]]:
704
+ """Higher order function that takes one of our bound API methods and an `APIResponse` class
705
+ and wraps the method to support streaming and returning the given response class directly.
706
+
707
+ Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
708
+ """
709
+
710
+ @functools.wraps(func)
711
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncResponseContextManager[_AsyncAPIResponseT]:
712
+ extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
713
+ extra_headers[RAW_RESPONSE_HEADER] = "stream"
714
+ extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
715
+
716
+ kwargs["extra_headers"] = extra_headers
717
+
718
+ make_request = func(*args, **kwargs)
719
+
720
+ return AsyncResponseContextManager(cast(Awaitable[_AsyncAPIResponseT], make_request))
721
+
722
+ return wrapped
723
+
724
+
725
+ def to_raw_response_wrapper(func: Callable[P, R]) -> Callable[P, APIResponse[R]]:
726
+ """Higher order function that takes one of our bound API methods and wraps it
727
+ to support returning the raw `APIResponse` object directly.
728
+ """
729
+
730
+ @functools.wraps(func)
731
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> APIResponse[R]:
732
+ extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
733
+ extra_headers[RAW_RESPONSE_HEADER] = "raw"
734
+
735
+ kwargs["extra_headers"] = extra_headers
736
+
737
+ return cast(APIResponse[R], func(*args, **kwargs))
738
+
739
+ return wrapped
740
+
741
+
742
+ def async_to_raw_response_wrapper(func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[AsyncAPIResponse[R]]]:
743
+ """Higher order function that takes one of our bound API methods and wraps it
744
+ to support returning the raw `APIResponse` object directly.
745
+ """
746
+
747
+ @functools.wraps(func)
748
+ async def wrapped(*args: P.args, **kwargs: P.kwargs) -> AsyncAPIResponse[R]:
749
+ extra_headers: dict[str, str] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
750
+ extra_headers[RAW_RESPONSE_HEADER] = "raw"
751
+
752
+ kwargs["extra_headers"] = extra_headers
753
+
754
+ return cast(AsyncAPIResponse[R], await func(*args, **kwargs))
755
+
756
+ return wrapped
757
+
758
+
759
+ def to_custom_raw_response_wrapper(
760
+ func: Callable[P, object],
761
+ response_cls: type[_APIResponseT],
762
+ ) -> Callable[P, _APIResponseT]:
763
+ """Higher order function that takes one of our bound API methods and an `APIResponse` class
764
+ and wraps the method to support returning the given response class directly.
765
+
766
+ Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
767
+ """
768
+
769
+ @functools.wraps(func)
770
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> _APIResponseT:
771
+ extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
772
+ extra_headers[RAW_RESPONSE_HEADER] = "raw"
773
+ extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
774
+
775
+ kwargs["extra_headers"] = extra_headers
776
+
777
+ return cast(_APIResponseT, func(*args, **kwargs))
778
+
779
+ return wrapped
780
+
781
+
782
+ def async_to_custom_raw_response_wrapper(
783
+ func: Callable[P, Awaitable[object]],
784
+ response_cls: type[_AsyncAPIResponseT],
785
+ ) -> Callable[P, Awaitable[_AsyncAPIResponseT]]:
786
+ """Higher order function that takes one of our bound API methods and an `APIResponse` class
787
+ and wraps the method to support returning the given response class directly.
788
+
789
+ Note: the given `response_cls` *must* be concrete, e.g. `class BinaryAPIResponse(APIResponse[bytes])`
790
+ """
791
+
792
+ @functools.wraps(func)
793
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> Awaitable[_AsyncAPIResponseT]:
794
+ extra_headers: dict[str, Any] = {**(cast(Any, kwargs.get("extra_headers")) or {})}
795
+ extra_headers[RAW_RESPONSE_HEADER] = "raw"
796
+ extra_headers[OVERRIDE_CAST_TO_HEADER] = response_cls
797
+
798
+ kwargs["extra_headers"] = extra_headers
799
+
800
+ return cast(Awaitable[_AsyncAPIResponseT], func(*args, **kwargs))
801
+
802
+ return wrapped
803
+
804
+
805
+ def extract_response_type(typ: type[BaseAPIResponse[Any]]) -> type:
806
+ """Given a type like `APIResponse[T]`, returns the generic type variable `T`.
807
+
808
+ This also handles the case where a concrete subclass is given, e.g.
809
+ ```py
810
+ class MyResponse(APIResponse[bytes]):
811
+ ...
812
+
813
+ extract_response_type(MyResponse) -> bytes
814
+ ```
815
+ """
816
+ return extract_type_var_from_base(
817
+ typ,
818
+ generic_bases=cast("tuple[type, ...]", (BaseAPIResponse, APIResponse, AsyncAPIResponse)),
819
+ index=0,
820
+ )