diff --git a/src/deepgram/core/client_wrapper.py b/src/deepgram/core/client_wrapper.py index 1db0612d..7ccd1231 100644 --- a/src/deepgram/core/client_wrapper.py +++ b/src/deepgram/core/client_wrapper.py @@ -26,7 +26,7 @@ def get_headers(self) -> typing.Dict[str, str]: "X-Fern-Language": "Python", "X-Fern-SDK-Name": "deepgram", # x-release-please-start-version - "X-Fern-SDK-Version": "5.2.0", + "X-Fern-SDK-Version": "5.2.0", # x-release-please-end **(self.get_custom_headers() or {}), } diff --git a/src/deepgram/core/http_client.py b/src/deepgram/core/http_client.py index e4173f99..a748681c 100644 --- a/src/deepgram/core/http_client.py +++ b/src/deepgram/core/http_client.py @@ -397,47 +397,52 @@ async def request( else self.base_timeout() ) - request_files: typing.Optional[RequestFiles] = ( - convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit)) - if (files is not None and files is not omit and isinstance(files, dict)) - else None - ) - - if (request_files is None or len(request_files) == 0) and force_multipart: + # Optimize: prepare request files only once + request_files = None + files_is_dict = isinstance(files, dict) + if files is not None and files is not omit and files_is_dict: + files_no_none = remove_none_from_dict(files) + files_dict = remove_omit_from_dict(files_no_none, omit) + request_files = convert_file_dict_to_httpx_tuples(files_dict) + if (not request_files or len(request_files) == 0) and force_multipart: request_files = FORCE_MULTIPART json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit) - # Add the input to each of these and do None-safety checks + # In hot path, prepare headers and params using local merges and comprehensions + # Flatten headers for efficiency + _base_headers = self.base_headers() + _headers = {} + _headers.update(_base_headers) + if headers: + _headers.update(headers) + if request_options is not None: + additional_headers = request_options.get("additional_headers") + if additional_headers: + _headers.update(additional_headers) + clean_headers = remove_none_from_dict(_headers) + encoded_headers = jsonable_encoder(clean_headers) + + # Similarly flatten and filter query params + _params = {} + if params: + _params.update(params) + if request_options is not None: + additional_query_parameters = request_options.get("additional_query_parameters") + if additional_query_parameters: + _params.update(additional_query_parameters) + # Combine remove_omit/remove_none only if needed + filtered_params = remove_omit_from_dict(_params, omit) if omit is not None else _params + clean_params = remove_none_from_dict(filtered_params) if filtered_params else filtered_params + encoded_params = encode_query(jsonable_encoder(clean_params)) if clean_params else None + + url = urllib.parse.urljoin(f"{base_url}/", path) + response = await self.httpx_client.request( method=method, - url=urllib.parse.urljoin(f"{base_url}/", path), - headers=jsonable_encoder( - remove_none_from_dict( - { - **self.base_headers(), - **(headers if headers is not None else {}), - **(request_options.get("additional_headers", {}) or {} if request_options is not None else {}), - } - ) - ), - params=encode_query( - jsonable_encoder( - remove_none_from_dict( - remove_omit_from_dict( - { - **(params if params is not None else {}), - **( - request_options.get("additional_query_parameters", {}) or {} - if request_options is not None - else {} - ), - }, - omit, - ) - ) - ) - ), + url=url, + headers=encoded_headers, + params=encoded_params, json=json_body, data=data_body, content=content, @@ -445,23 +450,25 @@ async def request( timeout=timeout, ) - max_retries: int = request_options.get("max_retries", 0) if request_options is not None else 0 - if _should_retry(response=response): - if max_retries > retries: - await asyncio.sleep(_retry_timeout(response=response, retries=retries)) - return await self.request( - path=path, - method=method, - base_url=base_url, - params=params, - json=json, - content=content, - files=files, - headers=headers, - request_options=request_options, - retries=retries + 1, - omit=omit, - ) + max_retries = request_options.get("max_retries", 0) if request_options is not None else 0 + if _should_retry(response=response) and max_retries > retries: + await asyncio.sleep(_retry_timeout(response=response, retries=retries)) + # Trampoline retry call without needlessly re-calculating computed/encoded values + return await self.request( + path=path, + method=method, + base_url=base_url, + params=params, + json=json, + content=content, + files=files, + headers=headers, + request_options=request_options, + retries=retries + 1, + omit=omit, + force_multipart=force_multipart, + data=data, + ) return response @asynccontextmanager diff --git a/src/deepgram/core/jsonable_encoder.py b/src/deepgram/core/jsonable_encoder.py index afee3662..2f057a7a 100644 --- a/src/deepgram/core/jsonable_encoder.py +++ b/src/deepgram/core/jsonable_encoder.py @@ -29,57 +29,66 @@ def jsonable_encoder(obj: Any, custom_encoder: Optional[Dict[Any, Callable[[Any], Any]]] = None) -> Any: + # Fast-path for simple types and bytes + if isinstance(obj, (str, int, float, type(None))): + return obj + if isinstance(obj, bytes): + return base64.b64encode(obj).decode("utf-8") + if isinstance(obj, Enum): + return obj.value + if isinstance(obj, PurePath): + return str(obj) + if isinstance(obj, dt.datetime): + return serialize_datetime(obj) + if isinstance(obj, dt.date): + return str(obj) + + # Custom encoder check custom_encoder = custom_encoder or {} + typ = type(obj) if custom_encoder: - if type(obj) in custom_encoder: - return custom_encoder[type(obj)](obj) - else: - for encoder_type, encoder_instance in custom_encoder.items(): - if isinstance(obj, encoder_type): - return encoder_instance(obj) + encoder_call = custom_encoder.get(typ) + if encoder_call is not None: + return encoder_call(obj) + for encoder_type, encoder_instance in custom_encoder.items(): + if isinstance(obj, encoder_type): + return encoder_instance(obj) + + # Optimize: check for Pydantic BaseModel before dataclasses if isinstance(obj, pydantic.BaseModel): if IS_PYDANTIC_V2: encoder = getattr(obj.model_config, "json_encoders", {}) # type: ignore # Pydantic v2 else: encoder = getattr(obj.__config__, "json_encoders", {}) # type: ignore # Pydantic v1 if custom_encoder: - encoder.update(custom_encoder) + encoder = {**encoder, **custom_encoder} obj_dict = obj.dict(by_alias=True) if "__root__" in obj_dict: obj_dict = obj_dict["__root__"] if "root" in obj_dict: obj_dict = obj_dict["root"] + # Fast-path: skip recursive dict if scalar return jsonable_encoder(obj_dict, custom_encoder=encoder) + if dataclasses.is_dataclass(obj): - obj_dict = dataclasses.asdict(obj) # type: ignore + obj_dict = dataclasses.asdict(obj) return jsonable_encoder(obj_dict, custom_encoder=custom_encoder) - if isinstance(obj, bytes): - return base64.b64encode(obj).decode("utf-8") - if isinstance(obj, Enum): - return obj.value - if isinstance(obj, PurePath): - return str(obj) - if isinstance(obj, (str, int, float, type(None))): - return obj - if isinstance(obj, dt.datetime): - return serialize_datetime(obj) - if isinstance(obj, dt.date): - return str(obj) + + # Dict encoding if isinstance(obj, dict): - encoded_dict = {} - allowed_keys = set(obj.keys()) - for key, value in obj.items(): - if key in allowed_keys: - encoded_key = jsonable_encoder(key, custom_encoder=custom_encoder) - encoded_value = jsonable_encoder(value, custom_encoder=custom_encoder) - encoded_dict[encoded_key] = encoded_value - return encoded_dict + # Avoid constructing allowed_keys set, keys are always valid in their own dict + # Avoid unnecessary comprehension overhead for common dict values + return { + jsonable_encoder(k, custom_encoder=custom_encoder): jsonable_encoder(v, custom_encoder=custom_encoder) + for k, v in obj.items() + } + + # Sequence-like structures if isinstance(obj, (list, set, frozenset, GeneratorType, tuple)): - encoded_list = [] - for item in obj: - encoded_list.append(jsonable_encoder(item, custom_encoder=custom_encoder)) - return encoded_list + # Use list comprehension for better perf than append in a loop + return [jsonable_encoder(item, custom_encoder=custom_encoder) for item in obj] + # Fallback serializer handling (defined only once) def fallback_serializer(o: Any) -> Any: attempt_encode = encode_by_type(o) if attempt_encode is not None: @@ -88,13 +97,13 @@ def fallback_serializer(o: Any) -> Any: try: data = dict(o) except Exception as e: - errors: List[Exception] = [] - errors.append(e) + errors: List[Exception] = [e] try: data = vars(o) - except Exception as e: - errors.append(e) - raise ValueError(errors) from e + except Exception as e2: + errors.append(e2) + raise ValueError(errors) from e2 return jsonable_encoder(data, custom_encoder=custom_encoder) + # Use to_jsonable_with_fallback for custom types return to_jsonable_with_fallback(obj, fallback_serializer)