• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stripe / stripe-python / 10567939360

26 Aug 2024 10:02PM UTC coverage: 96.388% (+0.001%) from 96.387%
10567939360

Pull #1380

github

web-flow
Merge 1a7a18201 into 4b36e4111
Pull Request #1380: Update generated code for beta

1343 of 2037 branches covered (65.93%)

Branch coverage included in aggregate %.

42 of 42 new or added lines in 10 files covered. (100.0%)

180 existing lines in 7 files now uncovered.

84818 of 87353 relevant lines covered (97.1%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.39
/stripe/_api_requestor.py
1
from io import BytesIO, IOBase
1✔
2
import json
1✔
3
import platform
1✔
4
from typing import (
1✔
5
    Any,
6
    AsyncIterable,
7
    Dict,
8
    List,
9
    Mapping,
10
    Optional,
11
    Tuple,
12
    Union,
13
    cast,
14
    ClassVar,
15
)
16
from typing_extensions import (
1✔
17
    TYPE_CHECKING,
18
    Literal,
19
    NoReturn,
20
    Unpack,
21
)
22
import uuid
1✔
23
from urllib.parse import urlsplit, urlunsplit
1✔
24

25
# breaking circular dependency
26
import stripe  # noqa: IMP101
1✔
27
from stripe._util import (
1✔
28
    log_debug,
29
    log_info,
30
    dashboard_link,
31
    _convert_to_stripe_object,
32
)
33
from stripe._version import VERSION
1✔
34
import stripe._error as error
1✔
35
import stripe.oauth_error as oauth_error
1✔
36
from stripe._multipart_data_generator import MultipartDataGenerator
1✔
37
from urllib.parse import urlencode
1✔
38
from stripe._encode import (
1✔
39
    _api_encode,
40
    _json_encode_date_callback,
41
)
42
from stripe._stripe_response import (
1✔
43
    StripeResponse,
44
    StripeStreamResponse,
45
    StripeStreamResponseAsync,
46
)
47
from stripe._request_options import RequestOptions, merge_options
1✔
48
from stripe._requestor_options import (
1✔
49
    RequestorOptions,
50
    _GlobalRequestorOptions,
51
)
52
from stripe._http_client import (
1✔
53
    HTTPClient,
54
    new_default_http_client,
55
    new_http_client_async_fallback,
56
)
57
from stripe._app_info import AppInfo
1✔
58

59
from stripe._base_address import BaseAddress
1✔
60
from stripe._api_mode import ApiMode
1✔
61

62
if TYPE_CHECKING:
1!
63
    from stripe._stripe_object import StripeObject
×
64

65
HttpVerb = Literal["get", "post", "delete"]
1✔
66

67
# Lazily initialized
68
_default_proxy: Optional[str] = None
1✔
69

70

71
class _APIRequestor(object):
1✔
72
    _instance: ClassVar["_APIRequestor|None"] = None
1✔
73

74
    def __init__(
1✔
75
        self,
76
        options: RequestorOptions = RequestorOptions(),
77
        client: Optional[HTTPClient] = None,
78
    ):
79
        self._options = options
1✔
80
        self._client = client
1✔
81

82
    # In the case of client=None, we should use the current value of stripe.default_http_client
83
    # or lazily initialize it. Since stripe.default_http_client can change throughout the lifetime of
84
    # an _APIRequestor, we shouldn't set it as stripe._client and should access it only through this
85
    # getter.
86
    def _get_http_client(self) -> HTTPClient:
1✔
87
        client = self._client
1✔
88
        if client is None:
1✔
89
            global _default_proxy
90

91
            if not stripe.default_http_client:
1✔
92
                kwargs = {
1✔
93
                    "verify_ssl_certs": stripe.verify_ssl_certs,
94
                    "proxy": stripe.proxy,
95
                }
96
                # If the stripe.default_http_client has not been set by the user
97
                # yet, we'll set it here. This way, we aren't creating a new
98
                # HttpClient for every request.
99
                stripe.default_http_client = new_default_http_client(
1✔
100
                    async_fallback_client=new_http_client_async_fallback(
101
                        **kwargs
102
                    ),
103
                    **kwargs,
104
                )
105
                _default_proxy = stripe.proxy
1✔
106
            elif stripe.proxy != _default_proxy:
1✔
107
                import warnings
1✔
108

109
                warnings.warn(
1✔
110
                    "stripe.proxy was updated after sending a "
111
                    "request - this is a no-op. To use a different proxy, "
112
                    "set stripe.default_http_client to a new client "
113
                    "configured with the proxy."
114
                )
115

116
            assert stripe.default_http_client is not None
1✔
117
            return stripe.default_http_client
1✔
118
        return client
1✔
119

120
    def _replace_options(
1✔
121
        self, options: Optional[RequestOptions]
122
    ) -> "_APIRequestor":
123
        options = options or {}
1✔
124
        new_options = self._options.to_dict()
1✔
125
        for key in ["api_key", "stripe_account", "stripe_version"]:
1✔
126
            if key in options and options[key] is not None:
1✔
127
                new_options[key] = options[key]
1✔
128
        return _APIRequestor(
1✔
129
            options=RequestorOptions(**new_options), client=self._client
130
        )
131

132
    @property
1✔
133
    def api_key(self):
1✔
134
        return self._options.api_key
1✔
135

136
    @property
1✔
137
    def stripe_account(self):
1✔
138
        return self._options.stripe_account
1✔
139

140
    @property
1✔
141
    def stripe_version(self):
1✔
142
        return self._options.stripe_version
1✔
143

144
    @property
1✔
145
    def base_addresses(self):
1✔
146
        return self._options.base_addresses
1✔
147

148
    @classmethod
1✔
149
    def _global_instance(cls):
1✔
150
        """
151
        Returns the singleton instance of _APIRequestor, to be used when
152
        calling a static method such as stripe.Customer.create(...)
153
        """
154

155
        # Lazily initialize.
156
        if cls._instance is None:
1✔
157
            cls._instance = cls(options=_GlobalRequestorOptions(), client=None)
1✔
158
        return cls._instance
1✔
159

160
    @staticmethod
1✔
161
    def _global_with_options(
1✔
162
        **params: Unpack[RequestOptions],
163
    ) -> "_APIRequestor":
164
        return _APIRequestor._global_instance()._replace_options(params)
1✔
165

166
    @classmethod
1✔
167
    def _format_app_info(cls, info):
1✔
168
        str = info["name"]
1✔
169
        if info["version"]:
1!
170
            str += "/%s" % (info["version"],)
1✔
171
        if info["url"]:
1!
172
            str += " (%s)" % (info["url"],)
1✔
173
        return str
1✔
174

175
    def request(
1✔
176
        self,
177
        method: str,
178
        url: str,
179
        params: Optional[Mapping[str, Any]] = None,
180
        options: Optional[RequestOptions] = None,
181
        *,
182
        base_address: BaseAddress,
183
        api_mode: ApiMode,
184
        usage: Optional[List[str]] = None,
185
    ) -> "StripeObject":
186
        requestor = self._replace_options(options)
1✔
187
        rbody, rcode, rheaders = requestor.request_raw(
1✔
188
            method.lower(),
189
            url,
190
            params,
191
            is_streaming=False,
192
            api_mode=api_mode,
193
            base_address=base_address,
194
            options=options,
195
            usage=usage,
196
        )
197
        resp = requestor._interpret_response(rbody, rcode, rheaders)
1✔
198

199
        return _convert_to_stripe_object(
1✔
200
            resp=resp,
201
            params=params,
202
            requestor=requestor,
203
            api_mode=api_mode,
204
        )
205

206
    async def request_async(
1✔
207
        self,
208
        method: str,
209
        url: str,
210
        params: Optional[Mapping[str, Any]] = None,
211
        options: Optional[RequestOptions] = None,
212
        *,
213
        base_address: BaseAddress,
214
        api_mode: ApiMode,
215
        usage: Optional[List[str]] = None,
216
    ) -> "StripeObject":
217
        requestor = self._replace_options(options)
1✔
218
        rbody, rcode, rheaders = await requestor.request_raw_async(
1✔
219
            method.lower(),
220
            url,
221
            params,
222
            is_streaming=False,
223
            api_mode=api_mode,
224
            base_address=base_address,
225
            options=options,
226
            usage=usage,
227
        )
228
        resp = requestor._interpret_response(rbody, rcode, rheaders)
1✔
229

230
        return _convert_to_stripe_object(
1✔
231
            resp=resp,
232
            params=params,
233
            requestor=requestor,
234
            api_mode=api_mode,
235
        )
236

237
    def request_stream(
1✔
238
        self,
239
        method: str,
240
        url: str,
241
        params: Optional[Mapping[str, Any]] = None,
242
        options: Optional[RequestOptions] = None,
243
        *,
244
        base_address: BaseAddress,
245
        api_mode: ApiMode,
246
        usage: Optional[List[str]] = None,
247
    ) -> StripeStreamResponse:
248
        stream, rcode, rheaders = self.request_raw(
1✔
249
            method.lower(),
250
            url,
251
            params,
252
            is_streaming=True,
253
            api_mode=api_mode,
254
            base_address=base_address,
255
            options=options,
256
            usage=usage,
257
        )
258
        resp = self._interpret_streaming_response(
1✔
259
            # TODO: should be able to remove this cast once self._client.request_stream_with_retries
260
            # returns a more specific type.
261
            cast(IOBase, stream),
262
            rcode,
263
            rheaders,
264
        )
265
        return resp
1✔
266

267
    async def request_stream_async(
1✔
268
        self,
269
        method: str,
270
        url: str,
271
        params: Optional[Mapping[str, Any]] = None,
272
        options: Optional[RequestOptions] = None,
273
        *,
274
        base_address: BaseAddress,
275
        api_mode: ApiMode,
276
        usage: Optional[List[str]] = None,
277
    ) -> StripeStreamResponseAsync:
278
        stream, rcode, rheaders = await self.request_raw_async(
1✔
279
            method.lower(),
280
            url,
281
            params,
282
            is_streaming=True,
283
            api_mode=api_mode,
284
            base_address=base_address,
285
            options=options,
286
            usage=usage,
287
        )
288
        resp = await self._interpret_streaming_response_async(
1✔
289
            stream,
290
            rcode,
291
            rheaders,
292
        )
293
        return resp
1✔
294

295
    def handle_error_response(self, rbody, rcode, resp, rheaders) -> NoReturn:
1✔
296
        try:
1✔
297
            error_data = resp["error"]
1✔
UNCOV
298
        except (KeyError, TypeError):
×
UNCOV
299
            raise error.APIError(
×
300
                "Invalid response object from API: %r (HTTP response code "
301
                "was %d)" % (rbody, rcode),
302
                rbody,
303
                rcode,
304
                resp,
305
            )
306

307
        err = None
1✔
308

309
        # OAuth errors are a JSON object where `error` is a string. In
310
        # contrast, in API errors, `error` is a hash with sub-keys. We use
311
        # this property to distinguish between OAuth and API errors.
312
        if isinstance(error_data, str):
1✔
313
            err = self.specific_oauth_error(
1✔
314
                rbody, rcode, resp, rheaders, error_data
315
            )
316

317
        if err is None:
1✔
318
            err = self.specific_api_error(
1✔
319
                rbody, rcode, resp, rheaders, error_data
320
            )
321

322
        raise err
1✔
323

324
    def specific_api_error(self, rbody, rcode, resp, rheaders, error_data):
1✔
325
        log_info(
1✔
326
            "Stripe API error received",
327
            error_code=error_data.get("code"),
328
            error_type=error_data.get("type"),
329
            error_message=error_data.get("message"),
330
            error_param=error_data.get("param"),
331
        )
332

333
        # Rate limits were previously coded as 400's with code 'rate_limit'
334
        if rcode == 429 or (
1✔
335
            rcode == 400 and error_data.get("code") == "rate_limit"
336
        ):
337
            return error.RateLimitError(
1✔
338
                error_data.get("message"), rbody, rcode, resp, rheaders
339
            )
340
        elif rcode in [400, 404]:
1✔
341
            if error_data.get("type") == "idempotency_error":
1✔
342
                return error.IdempotencyError(
1✔
343
                    error_data.get("message"), rbody, rcode, resp, rheaders
344
                )
345
            else:
346
                return error.InvalidRequestError(
1✔
347
                    error_data.get("message"),
348
                    error_data.get("param"),
349
                    error_data.get("code"),
350
                    rbody,
351
                    rcode,
352
                    resp,
353
                    rheaders,
354
                )
355
        elif rcode == 401:
1✔
356
            return error.AuthenticationError(
1✔
357
                error_data.get("message"), rbody, rcode, resp, rheaders
358
            )
359
        elif rcode == 402:
1✔
360
            return error.CardError(
1✔
361
                error_data.get("message"),
362
                error_data.get("param"),
363
                error_data.get("code"),
364
                rbody,
365
                rcode,
366
                resp,
367
                rheaders,
368
            )
369
        elif rcode == 403:
1✔
370
            return error.PermissionError(
1✔
371
                error_data.get("message"), rbody, rcode, resp, rheaders
372
            )
373
        else:
374
            return error.APIError(
1✔
375
                error_data.get("message"), rbody, rcode, resp, rheaders
376
            )
377

378
    def specific_oauth_error(self, rbody, rcode, resp, rheaders, error_code):
1✔
379
        description = resp.get("error_description", error_code)
1✔
380

381
        log_info(
1✔
382
            "Stripe OAuth error received",
383
            error_code=error_code,
384
            error_description=description,
385
        )
386

387
        args = [error_code, description, rbody, rcode, resp, rheaders]
1✔
388

389
        if error_code == "invalid_client":
1✔
390
            return oauth_error.InvalidClientError(*args)
1✔
391
        elif error_code == "invalid_grant":
1✔
392
            return oauth_error.InvalidGrantError(*args)
1✔
393
        elif error_code == "invalid_request":
1!
394
            return oauth_error.InvalidRequestError(*args)
1✔
UNCOV
395
        elif error_code == "invalid_scope":
×
UNCOV
396
            return oauth_error.InvalidScopeError(*args)
×
397
        elif error_code == "unsupported_grant_type":
×
398
            return oauth_error.UnsupportedGrantTypeError(*args)
×
399
        elif error_code == "unsupported_response_type":
×
400
            return oauth_error.UnsupportedResponseTypeError(*args)
×
401

402
        return None
×
403

404
    def request_headers(
1✔
405
        self, method, options: RequestOptions, api_mode: ApiMode
406
    ):
407
        user_agent = "Stripe/v1 PythonBindings/%s" % (VERSION,)
1✔
408
        if stripe.app_info:
1✔
409
            user_agent += " " + self._format_app_info(stripe.app_info)
1✔
410

411
        ua: Dict[str, Union[str, AppInfo]] = {
1✔
412
            "bindings_version": VERSION,
413
            "lang": "python",
414
            "publisher": "stripe",
415
            "httplib": self._get_http_client().name,
416
        }
417
        for attr, func in [
1✔
418
            ["lang_version", platform.python_version],
419
            ["platform", platform.platform],
420
            ["uname", lambda: " ".join(platform.uname())],
421
        ]:
422
            try:
1✔
423
                val = func()
1✔
424
            except Exception:
1✔
425
                val = "(disabled)"
1✔
426
            ua[attr] = val
1✔
427
        if stripe.app_info:
1✔
428
            ua["application"] = stripe.app_info
1✔
429

430
        headers: Dict[str, str] = {
1✔
431
            "X-Stripe-Client-User-Agent": json.dumps(ua),
432
            "User-Agent": user_agent,
433
            "Authorization": "Bearer %s" % (options.get("api_key"),),
434
        }
435

436
        stripe_account = options.get("stripe_account")
1✔
437
        if stripe_account:
1✔
438
            headers["Stripe-Account"] = stripe_account
1✔
439

440
        idempotency_key = options.get("idempotency_key")
1✔
441
        if idempotency_key:
1✔
442
            headers["Idempotency-Key"] = idempotency_key
1✔
443

444
        if method == "post":
1✔
445
            headers.setdefault("Idempotency-Key", str(uuid.uuid4()))
1✔
446
            if api_mode == "preview":
1✔
447
                headers["Content-Type"] = "application/json"
1✔
448
            else:
449
                headers["Content-Type"] = "application/x-www-form-urlencoded"
1✔
450

451
        stripe_version = options.get("stripe_version")
1✔
452
        if stripe_version:
1!
453
            headers["Stripe-Version"] = stripe_version
1✔
454

455
        return headers
1✔
456

457
    def _args_for_request_with_retries(
1✔
458
        self,
459
        method: str,
460
        url: str,
461
        params: Optional[Mapping[str, Any]] = None,
462
        options: Optional[RequestOptions] = None,
463
        *,
464
        base_address: BaseAddress,
465
        api_mode: ApiMode,
466
        usage: Optional[List[str]] = None,
467
    ):
468
        """
469
        Mechanism for issuing an API call
470
        """
471
        request_options = merge_options(self._options, options)
1✔
472

473
        # Special stripe_version handling for preview requests:
474
        if (
1✔
475
            options
476
            and "stripe_version" in options
477
            and (options["stripe_version"] is not None)
478
        ):
479
            # If user specified an API version, honor it
480
            request_options["stripe_version"] = options["stripe_version"]
1✔
481
        elif api_mode == "preview":
1✔
482
            request_options["stripe_version"] = stripe.preview_api_version
1✔
483

484
        if request_options.get("api_key") is None:
1✔
485
            raise error.AuthenticationError(
1✔
486
                "No API key provided. (HINT: set your API key using "
487
                '"stripe.api_key = <API-KEY>"). You can generate API keys '
488
                "from the Stripe web interface.  See https://stripe.com/api "
489
                "for details, or email support@stripe.com if you have any "
490
                "questions."
491
            )
492

493
        abs_url = "%s%s" % (
1✔
494
            self._options.base_addresses.get(base_address),
495
            url,
496
        )
497

498
        encoded_params = urlencode(list(_api_encode(params or {})))
1✔
499

500
        # Don't use strict form encoding by changing the square bracket control
501
        # characters back to their literals. This is fine by the server, and
502
        # makes these parameter strings easier to read.
503
        encoded_params = encoded_params.replace("%5B", "[").replace("%5D", "]")
1✔
504

505
        if api_mode == "preview":
1✔
506
            encoded_body = json.dumps(
1✔
507
                params or {}, default=_json_encode_date_callback
508
            )
509
        else:
510
            encoded_body = encoded_params
1✔
511

512
        supplied_headers = None
1✔
513
        if (
1✔
514
            "headers" in request_options
515
            and request_options["headers"] is not None
516
        ):
517
            supplied_headers = dict(request_options["headers"])
1✔
518

519
        headers = self.request_headers(method, request_options, api_mode)
1✔
520

521
        if method == "get" or method == "delete":
1✔
522
            if params:
1✔
523
                query = encoded_params
1✔
524
                scheme, netloc, path, base_query, fragment = urlsplit(abs_url)
1✔
525

526
                if base_query:
1✔
527
                    query = "%s&%s" % (base_query, query)
1✔
528

529
                abs_url = urlunsplit((scheme, netloc, path, query, fragment))
1✔
530
            post_data = None
1✔
531
        elif method == "post":
1✔
532
            if api_mode == "V1FILES":
1✔
533
                generator = MultipartDataGenerator()
1✔
534
                generator.add_params(params or {})
1✔
535
                post_data = generator.get_post_data()
1✔
536
                headers["Content-Type"] = (
1✔
537
                    "multipart/form-data; boundary=%s" % (generator.boundary,)
538
                )
539
            else:
540
                post_data = encoded_body
1✔
541
        else:
542
            raise error.APIConnectionError(
1✔
543
                "Unrecognized HTTP method %r.  This may indicate a bug in the "
544
                "Stripe bindings.  Please contact support@stripe.com for "
545
                "assistance." % (method,)
546
            )
547

548
        if supplied_headers is not None:
1✔
549
            for key, value in supplied_headers.items():
1✔
550
                headers[key] = value
1✔
551

552
        max_network_retries = request_options.get("max_network_retries")
1✔
553

554
        return (
1✔
555
            # Actual args
556
            method,
557
            abs_url,
558
            headers,
559
            post_data,
560
            max_network_retries,
561
            usage,
562
            # For logging
563
            encoded_params,
564
            request_options.get("stripe_version"),
565
        )
566

567
    def request_raw(
1✔
568
        self,
569
        method: str,
570
        url: str,
571
        params: Optional[Mapping[str, Any]] = None,
572
        options: Optional[RequestOptions] = None,
573
        is_streaming: bool = False,
574
        *,
575
        base_address: BaseAddress,
576
        api_mode: ApiMode,
577
        usage: Optional[List[str]] = None,
578
    ) -> Tuple[object, int, Mapping[str, str]]:
579
        (
1✔
580
            method,
581
            abs_url,
582
            headers,
583
            post_data,
584
            max_network_retries,
585
            usage,
586
            encoded_params,
587
            api_version,
588
        ) = self._args_for_request_with_retries(
589
            method,
590
            url,
591
            params,
592
            options,
593
            base_address=base_address,
594
            api_mode=api_mode,
595
            usage=usage,
596
        )
597

598
        log_info("Request to Stripe api", method=method, url=abs_url)
1✔
599
        log_debug(
1✔
600
            "Post details", post_data=encoded_params, api_version=api_version
601
        )
602

603
        if is_streaming:
1✔
604
            (
1✔
605
                rcontent,
606
                rcode,
607
                rheaders,
608
            ) = self._get_http_client().request_stream_with_retries(
609
                method,
610
                abs_url,
611
                headers,
612
                post_data,
613
                max_network_retries=max_network_retries,
614
                _usage=usage,
615
            )
616
        else:
617
            (
1✔
618
                rcontent,
619
                rcode,
620
                rheaders,
621
            ) = self._get_http_client().request_with_retries(
622
                method,
623
                abs_url,
624
                headers,
625
                post_data,
626
                max_network_retries=max_network_retries,
627
                _usage=usage,
628
            )
629

630
        log_info("Stripe API response", path=abs_url, response_code=rcode)
1✔
631
        log_debug("API response body", body=rcontent)
1✔
632

633
        if "Request-Id" in rheaders:
1✔
634
            request_id = rheaders["Request-Id"]
1✔
635
            log_debug(
1✔
636
                "Dashboard link for request",
637
                link=dashboard_link(request_id),
638
            )
639

640
        return rcontent, rcode, rheaders
1✔
641

642
    async def request_raw_async(
1✔
643
        self,
644
        method: str,
645
        url: str,
646
        params: Optional[Mapping[str, Any]] = None,
647
        options: Optional[RequestOptions] = None,
648
        is_streaming: bool = False,
649
        *,
650
        base_address: BaseAddress,
651
        api_mode: ApiMode,
652
        usage: Optional[List[str]] = None,
653
    ) -> Tuple[AsyncIterable[bytes], int, Mapping[str, str]]:
654
        """
655
        Mechanism for issuing an API call
656
        """
657

658
        usage = usage or []
1✔
659
        usage = usage + ["async"]
1✔
660

661
        (
1✔
662
            method,
663
            abs_url,
664
            headers,
665
            post_data,
666
            max_network_retries,
667
            usage,
668
            encoded_params,
669
            api_version,
670
        ) = self._args_for_request_with_retries(
671
            method,
672
            url,
673
            params,
674
            options,
675
            base_address=base_address,
676
            api_mode=api_mode,
677
            usage=usage,
678
        )
679

680
        log_info("Request to Stripe api", method=method, url=abs_url)
1✔
681
        log_debug(
1✔
682
            "Post details",
683
            post_data=encoded_params,
684
            api_version=api_version,
685
        )
686

687
        if is_streaming:
1✔
688
            (
1✔
689
                rcontent,
690
                rcode,
691
                rheaders,
692
            ) = await self._get_http_client().request_stream_with_retries_async(
693
                method,
694
                abs_url,
695
                headers,
696
                post_data,
697
                max_network_retries=max_network_retries,
698
                _usage=usage,
699
            )
700
        else:
701
            (
1✔
702
                rcontent,
703
                rcode,
704
                rheaders,
705
            ) = await self._get_http_client().request_with_retries_async(
706
                method,
707
                abs_url,
708
                headers,
709
                post_data,
710
                max_network_retries=max_network_retries,
711
                _usage=usage,
712
            )
713

714
        log_info("Stripe API response", path=abs_url, response_code=rcode)
1✔
715
        log_debug("API response body", body=rcontent)
1✔
716

717
        if "Request-Id" in rheaders:
1✔
718
            request_id = rheaders["Request-Id"]
1✔
719
            log_debug(
1✔
720
                "Dashboard link for request",
721
                link=dashboard_link(request_id),
722
            )
723

724
        return rcontent, rcode, rheaders
1✔
725

726
    def _should_handle_code_as_error(self, rcode):
1✔
727
        return not 200 <= rcode < 300
1✔
728

729
    def _interpret_response(
1✔
730
        self,
731
        rbody: object,
732
        rcode: int,
733
        rheaders: Mapping[str, str],
734
    ) -> StripeResponse:
735
        try:
1✔
736
            if hasattr(rbody, "decode"):
1✔
737
                # TODO: should be able to remove this cast once self._client.request_with_retries
738
                # returns a more specific type.
739
                rbody = cast(bytes, rbody).decode("utf-8")
1✔
740
            resp = StripeResponse(
1✔
741
                cast(str, rbody),
742
                rcode,
743
                rheaders,
744
            )
745
        except Exception:
1✔
746
            raise error.APIError(
1✔
747
                "Invalid response body from API: %s "
748
                "(HTTP response code was %d)" % (rbody, rcode),
749
                cast(bytes, rbody),
750
                rcode,
751
                rheaders,
752
            )
753
        if self._should_handle_code_as_error(rcode):
1✔
754
            self.handle_error_response(rbody, rcode, resp.data, rheaders)
1✔
755
        return resp
1✔
756

757
    async def _interpret_streaming_response_async(
1✔
758
        self,
759
        stream: AsyncIterable[bytes],
760
        rcode: int,
761
        rheaders: Mapping[str, str],
762
    ) -> StripeStreamResponseAsync:
763
        if self._should_handle_code_as_error(rcode):
1✔
764
            json_content = b"".join([chunk async for chunk in stream])
1✔
765
            self._interpret_response(json_content, rcode, rheaders)
1✔
766
            # _interpret_response is guaranteed to throw since we've checked self._should_handle_code_as_error
UNCOV
767
            raise RuntimeError(
×
768
                "_interpret_response should have raised an error"
769
            )
770
        else:
771
            return StripeStreamResponseAsync(stream, rcode, rheaders)
1✔
772

773
    def _interpret_streaming_response(
1✔
774
        self,
775
        stream: IOBase,
776
        rcode: int,
777
        rheaders: Mapping[str, str],
778
    ) -> StripeStreamResponse:
779
        # Streaming response are handled with minimal processing for the success
780
        # case (ie. we don't want to read the content). When an error is
781
        # received, we need to read from the stream and parse the received JSON,
782
        # treating it like a standard JSON response.
783
        if self._should_handle_code_as_error(rcode):
1✔
784
            if hasattr(stream, "getvalue"):
1✔
785
                json_content = cast(BytesIO, stream).getvalue()
1✔
786
            elif hasattr(stream, "read"):
1!
787
                json_content = stream.read()
1✔
788
            else:
UNCOV
789
                raise NotImplementedError(
×
790
                    "HTTP client %s does not return an IOBase object which "
791
                    "can be consumed when streaming a response."
792
                    % self._get_http_client().name
793
                )
794

795
            self._interpret_response(json_content, rcode, rheaders)
1✔
796
            # _interpret_response is guaranteed to throw since we've checked self._should_handle_code_as_error
UNCOV
797
            raise RuntimeError(
×
798
                "_interpret_response should have raised an error"
799
            )
800
        else:
801
            return StripeStreamResponse(stream, rcode, rheaders)
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc