# pyright: strict import json from copy import deepcopy from typing_extensions import TYPE_CHECKING, Type, Literal, Self, deprecated from typing import ( Any, Dict, Generic, List, Optional, Mapping, Set, Tuple, ClassVar, TypeVar, Union, cast, overload, ) # Used to break circular imports import stripe # noqa: IMP101 from stripe import _util from stripe._stripe_response import ( StripeResponse, StripeStreamResponse, StripeStreamResponseAsync, ) from stripe._encode import ( _coerce_int64_string, # pyright: ignore[reportPrivateUsage] _coerce_decimal_string, # pyright: ignore[reportPrivateUsage] _make_suitable_for_json, # pyright: ignore[reportPrivateUsage] ) from stripe._request_options import ( PERSISTENT_OPTIONS_KEYS, extract_options_from_dict, ) from stripe._api_mode import ApiMode from stripe._base_address import BaseAddress if TYPE_CHECKING: from stripe._api_requestor import _APIRequestor # pyright: ignore[reportPrivateUsage] @overload def _compute_diff( current: Dict[str, Any], previous: Optional[Dict[str, Any]] ) -> Dict[str, Any]: ... @overload def _compute_diff( current: object, previous: Optional[Dict[str, Any]] ) -> object: ... def _compute_diff( current: object, previous: Optional[Dict[str, Any]] ) -> object: if isinstance(current, dict): current = cast(Dict[str, Any], current) previous = previous or {} diff = current.copy() for key in set(previous.keys()) - set(diff.keys()): diff[key] = "" return diff return current if current is not None else "" def _serialize_list( array: Optional[List[Any]], previous: List[Any] ) -> Dict[str, Any]: array = array or [] previous = previous or [] params: Dict[str, Any] = {} for i, v in enumerate(array): previous_item = previous[i] if len(previous) > i else None if hasattr(v, "serialize"): params[str(i)] = v.serialize(previous_item) else: params[str(i)] = _compute_diff(v, previous_item) return params class StripeObject: _retrieve_params: Mapping[str, Any] _previous: Optional[Mapping[str, Any]] def __init__( self, id: Optional[str] = None, api_key: Optional[str] = None, stripe_version: Optional[str] = None, stripe_account: Optional[str] = None, last_response: Optional[StripeResponse] = None, *, _requestor: Optional["_APIRequestor"] = None, # TODO: is a more specific type possible here? **params: Any, ): self._data: Dict[str, Any] = {} self._unsaved_values: Set[str] = set() self._transient_values: Set[str] = set() self._last_response = last_response self._retrieve_params = params self._previous = None from stripe._api_requestor import _APIRequestor # pyright: ignore[reportPrivateUsage] self._requestor = ( _APIRequestor._global_with_options( # pyright: ignore[reportPrivateUsage] api_key=api_key, stripe_version=stripe_version, stripe_account=stripe_account, ) if _requestor is None else _requestor ) if id: self["id"] = id @property def api_key(self): return self._requestor.api_key @property def stripe_account(self): return self._requestor.stripe_account @property def stripe_version(self): return self._requestor.stripe_version @property def last_response(self) -> Optional[StripeResponse]: return self._last_response def update(self, update_dict: Mapping[str, Any]) -> None: for k in update_dict: self._unsaved_values.add(k) self._data.update(update_dict) if not TYPE_CHECKING: def __setattr__(self, k, v): if k in PERSISTENT_OPTIONS_KEYS: self._requestor = self._requestor._new_requestor_with_options( {k: v} ) return None if k[0] == "_" or k in self.__dict__: return super(StripeObject, self).__setattr__(k, v) self[k] = v return None def __getattr__(self, k): if k[0] == "_": raise AttributeError(k) try: if k in self._field_remappings: k = self._field_remappings[k] return self[k] except KeyError as err: raise AttributeError(*err.args) from err def __delattr__(self, k): if k[0] == "_" or k in self.__dict__: return super(StripeObject, self).__delattr__(k) else: del self[k] def __setitem__(self, k: str, v: Any) -> None: if v == "": raise ValueError( "You cannot set %s to an empty string on this object. " "The empty string is treated specially in our requests. " "If you'd like to delete the property using the save() method on this object, you may set %s.%s=None. " "Alternatively, you can pass %s='' to delete the property when using a resource method such as modify()." % (k, str(self), k, k) ) # Allows for unpickling in Python 3.x if not hasattr(self, "_unsaved_values"): self._unsaved_values = set() if not hasattr(self, "_data"): self._data = {} self._unsaved_values.add(k) self._data[k] = v def __getitem__(self, k: str) -> Any: try: return self._data[k] except KeyError as err: if k in self._transient_values: raise KeyError( "%r. HINT: The %r attribute was set in the past." "It was then wiped when refreshing the object with " "the result returned by Stripe's API, probably as a " "result of a save(). The attributes currently " "available on this object are: %s" % (k, k, ", ".join(list(self._data.keys()))) ) else: from stripe._invoice import Invoice # super specific one-off case to help users debug this property disappearing # see also: https://go/j/DEVSDK-2835 if isinstance(self, Invoice) and k == "payment_intent": raise KeyError( "The 'payment_intent' attribute is no longer available on Invoice objects. See the docs for more details: https://docs.stripe.com/changelog/basil/2025-03-31/add-support-for-multiple-partial-payments-on-invoices#why-is-this-a-breaking-change" ) raise err def __delitem__(self, k: str) -> None: del self._data[k] # Allows for unpickling in Python 3.x if hasattr(self, "_unsaved_values") and k in self._unsaved_values: self._unsaved_values.remove(k) def __contains__(self, k: object) -> bool: return k in self._data def __eq__(self, other: object) -> bool: if isinstance(other, StripeObject): return type(self) is type(other) and self._data == other._data return NotImplemented # Custom unpickling method that updates _data directly # without calling __setitem__, which would fail if any value is an empty # string def __setstate__(self, state: Dict[str, Any]) -> None: self._data.update(state) self._unsaved_values.update(state.keys()) # Custom pickling method to ensure the instance is pickled as a custom # class and not as a dict, otherwise __setstate__ would not be called when # unpickling. def __reduce__(self) -> Tuple[Any, ...]: reduce_value = ( type(self), # callable ( # args getattr(self, "id", None), self.api_key, self.stripe_version, self.stripe_account, ), self._data.copy(), # state ) return reduce_value @classmethod def construct_from( cls, values: Dict[str, Any], key: Optional[str], stripe_version: Optional[str] = None, stripe_account: Optional[str] = None, last_response: Optional[StripeResponse] = None, *, api_mode: ApiMode = "V1", ) -> Self: from stripe._api_requestor import _APIRequestor # pyright: ignore[reportPrivateUsage] return cls._construct_from( values=values, requestor=_APIRequestor._global_with_options( # pyright: ignore[reportPrivateUsage] api_key=key, stripe_version=stripe_version, stripe_account=stripe_account, ), api_mode=api_mode, last_response=last_response, ) @classmethod def _construct_from( cls, *, values: "Union[Dict[str, Any], StripeObject]", last_response: Optional[StripeResponse] = None, requestor: "_APIRequestor", api_mode: ApiMode, ) -> Self: raw = values._data if isinstance(values, StripeObject) else values instance = cls( raw.get("id"), last_response=last_response, _requestor=requestor, ) instance._refresh_from( values=values, last_response=last_response, requestor=requestor, api_mode=api_mode, ) return instance def refresh_from( self, values: Dict[str, Any], api_key: Optional[str] = None, partial: Optional[bool] = False, stripe_version: Optional[str] = None, stripe_account: Optional[str] = None, last_response: Optional[StripeResponse] = None, *, api_mode: ApiMode = "V1", ) -> None: self._refresh_from( values=values, partial=partial, last_response=last_response, requestor=self._requestor._new_requestor_with_options( # pyright: ignore[reportPrivateUsage] { "api_key": api_key, "stripe_version": stripe_version, "stripe_account": stripe_account, } ), api_mode=api_mode, ) def _refresh_from( self, *, values: "Union[Dict[str, Any], StripeObject]", partial: Optional[bool] = False, last_response: Optional[StripeResponse] = None, requestor: Optional["_APIRequestor"] = None, api_mode: ApiMode, ) -> None: self._requestor = requestor or self._requestor self._last_response = last_response or getattr( values, "_last_response", None ) # When called from APIResource._request, values may be a StripeObject if isinstance(values, StripeObject): values = values._data # Wipe old state before setting new. This is useful for e.g. # updating a customer, where there is no persistent card # parameter. Mark those values which don't persist as transient if partial: self._unsaved_values = self._unsaved_values - set(values) else: removed = set(self._data.keys()) - set(values) self._transient_values = self._transient_values | removed self._unsaved_values = set() self._data.clear() self._transient_values = self._transient_values - set(values) for k, v in values.items(): # Apply field encoding coercion (e.g. int64_string: str → int) v = self._coerce_field_value(k, v) inner_class = self._get_inner_class_type(k) is_dict = self._get_inner_class_is_beneath_dict(k) if is_dict: obj = { k: None if v is None else cast( StripeObject, _util._convert_to_stripe_object( # pyright: ignore[reportPrivateUsage] resp=v, params=None, klass_=inner_class, requestor=self._requestor, api_mode=api_mode, ), ) for k, v in v.items() } else: obj = cast( Union[StripeObject, List[StripeObject]], _util._convert_to_stripe_object( # pyright: ignore[reportPrivateUsage] resp=v, params=None, klass_=inner_class, requestor=self._requestor, api_mode=api_mode, ), ) self._data[k] = obj self._previous = values @deprecated("This will be removed in a future version of stripe-python.") def request( self, method: Literal["get", "post", "delete"], url: str, params: Optional[Dict[str, Any]] = None, *, base_address: BaseAddress = "api", ) -> "StripeObject": return StripeObject._request( self, method, url, params=params, base_address=base_address, ) def _request( self, method: Literal["get", "post", "delete"], url: str, params: Optional[Mapping[str, Any]] = None, usage: Optional[List[str]] = None, *, base_address: BaseAddress, ) -> "StripeObject": if params is None: params = self._retrieve_params request_options, request_params = extract_options_from_dict(params) return self._requestor.request( method, url, params=request_params, options=request_options, base_address=base_address, usage=usage, ) async def _request_async( self, method: Literal["get", "post", "delete"], url: str, params: Optional[Mapping[str, Any]] = None, usage: Optional[List[str]] = None, *, base_address: BaseAddress, ) -> "StripeObject": if params is None: params = self._retrieve_params request_options, request_params = extract_options_from_dict(params) return await self._requestor.request_async( method, url, params=request_params, options=request_options, base_address=base_address, usage=usage, ) def _request_stream( self, method: str, url: str, params: Optional[Mapping[str, Any]] = None, *, base_address: BaseAddress = "api", ) -> StripeStreamResponse: if params is None: params = self._retrieve_params request_options, request_params = extract_options_from_dict(params) return self._requestor.request_stream( method, url, params=request_params, options=request_options, base_address=base_address, ) async def _request_stream_async( self, method: str, url: str, params: Optional[Mapping[str, Any]] = None, *, base_address: BaseAddress = "api", ) -> StripeStreamResponseAsync: if params is None: params = self._retrieve_params request_options, request_params = extract_options_from_dict(params) return await self._requestor.request_stream_async( method, url, params=request_params, options=request_options, base_address=base_address, ) def __repr__(self) -> str: ident_parts = [type(self).__name__] obj_str = getattr(self, "object", None) if isinstance(obj_str, str): ident_parts.append(obj_str) obj_id = getattr(self, "id", None) if isinstance(obj_id, str): ident_parts.append("id=%s" % (obj_id,)) unicode_repr = "<%s at %s> JSON: %s" % ( " ".join(ident_parts), hex(id(self)), str(self), ) return unicode_repr def __str__(self) -> str: return json.dumps( self._to_dict_recursive(), sort_keys=True, indent=2, default=_make_suitable_for_json, ) def to_dict( self, recursive: bool = True, for_json: bool = False ) -> Dict[str, Any]: """ Dump the object's backing data. Recurses by default, but you can opt-out of that behavior by passing `recursive=False`. Pass `for_json=True` to convert non-JSON-serializable values (e.g. Decimal -> str) """ if recursive: return self._to_dict_recursive(for_json=for_json) # shallow copy, so nested objects will be shared return self._data.copy() def _to_dict_recursive(self, for_json: bool = False) -> Dict[str, Any]: """ used by __str__ to serialize the whole object """ def maybe_to_dict_recursive( value: Optional[Union[StripeObject, Dict[str, Any]]], ) -> Optional[Dict[str, Any]]: if value is None: return None elif isinstance(value, StripeObject): return value._to_dict_recursive(for_json=for_json) elif for_json: return _make_suitable_for_json(value) else: return value return { key: list(map(maybe_to_dict_recursive, cast(List[Any], value))) if isinstance(value, list) else maybe_to_dict_recursive(value) for key, value in self._data.items() } def serialize( self, previous: Optional[Mapping[str, Any]] ) -> Dict[str, Any]: params: Dict[str, Any] = {} unsaved_keys = self._unsaved_values or set() previous_raw = previous or self._previous or {} if isinstance(previous_raw, StripeObject): previous_raw = previous_raw._data prev: Dict[str, Any] = dict(previous_raw) for k, v in self._data.items(): if k == "id" or k.startswith("_"): continue elif isinstance(v, stripe.APIResource): continue elif hasattr(v, "serialize"): child = v.serialize(prev.get(k, None)) if child != {}: params[k] = child elif k in unsaved_keys: params[k] = _compute_diff(v, prev.get(k, None)) elif k == "additional_owners" and v is not None: params[k] = _serialize_list(v, prev.get(k, None)) return params # This class overrides __setitem__ to throw exceptions on inputs that it # doesn't like. This can cause problems when we try to copy an object # wholesale because some data that's returned from the API may not be valid # if it was set to be set manually. Here we override the class' copy # arguments so that we can bypass these possible exceptions on __setitem__. def __copy__(self) -> "StripeObject": copied = StripeObject( getattr(self, "id", None), self.api_key, stripe_version=self.stripe_version, stripe_account=self.stripe_account, ) copied._retrieve_params = self._retrieve_params for k, v in self._data.items(): # Write to _data directly to avoid checks that we've added in the # overridden __setitem__ that can throw exceptions. copied._data[k] = v return copied # This class overrides __setitem__ to throw exceptions on inputs that it # doesn't like. This can cause problems when we try to copy an object # wholesale because some data that's returned from the API may not be valid # if it was set to be set manually. Here we override the class' copy # arguments so that we can bypass these possible exceptions on __setitem__. def __deepcopy__(self, memo: Dict[int, Any]) -> "StripeObject": copied = self.__copy__() memo[id(self)] = copied for k, v in self._data.items(): # Write to _data directly to avoid checks that we've added in the # overridden __setitem__ that can throw exceptions. copied._data[k] = deepcopy(v, memo) return copied _field_remappings: ClassVar[Dict[str, str]] = {} _inner_class_types: ClassVar[Dict[str, Type["StripeObject"]]] = {} _inner_class_dicts: ClassVar[List[str]] = [] _field_encodings: ClassVar[Dict[str, str]] = {} def _get_inner_class_type( self, field_name: str ) -> Optional[Type["StripeObject"]]: return self._inner_class_types.get(field_name) def _get_inner_class_is_beneath_dict(self, field_name: str): return field_name in self._inner_class_dicts def _coerce_field_value(self, field_name: str, value: Any) -> Any: """ Convert JSON types to more applicable Python types, if able. For example, "int64_string"s become `int`s. """ # WARNING: if you edit this function to produce a type that's not json-serializable, you need to update `_make_suitable_for_json` as well. # By default, Python will only correctly dump a few standard types, so we have to handle the rest encoding = self._field_encodings.get(field_name) if encoding is None or value is None: return value if encoding == "int64_string": return _coerce_int64_string(value, encode=False) if encoding == "decimal_string": return _coerce_decimal_string(value, encode=False) return value T = TypeVar("T") class UntypedStripeObject(StripeObject, Generic[T]): """ A normal StripeObject, but it exposes `__getattr__`/`__setattr__` instead of hiding them, effectively removing type information. Because metadata & similar are supposed to be an untyped `dict`, we don't want to show type errors for arbitrary key access. Is generic on its value type """ def __init__(*args, **kwargs: Any): raise ValueError("this is not for runtime use, just typing") # This class is never actually used at runtime, it's just here for typechecking reasons def __setattr__(self, k: str, v: T): ... def __getattr__(self, k: str) -> T: ... def __delattr__(self, k: str): ...