Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/deepgram/core/client_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}),
}
Expand Down
111 changes: 59 additions & 52 deletions src/deepgram/core/http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,71 +397,78 @@ 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,
files=request_files,
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
Expand Down
83 changes: 46 additions & 37 deletions src/deepgram/core/jsonable_encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)