Skip to content

Commit 189e0aa

Browse files
vdusekclaude
andcommitted
refactor: share parsed error payload between ApifyApiError __new__ and __init__
`__new__` used to parse `response.json()` for dispatch and `__init__` parsed it again for attribute assignment. Now `__new__` stashes the parsed `error` dict on the instance and `__init__` reads it, so each raised error parses the body once. Also trims the stale star-import comment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent dd7f08f commit 189e0aa

1 file changed

Lines changed: 28 additions & 38 deletions

File tree

src/apify_client/errors.py

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import TYPE_CHECKING
3+
from typing import TYPE_CHECKING, Any
44

55
from apify_client._docs import docs_group
66

@@ -43,19 +43,22 @@ class ApifyApiError(ApifyClientError):
4343
data: Additional error data from the API response.
4444
"""
4545

46+
_apify_error_payload: dict[str, Any] | None
47+
4648
def __new__(cls, response: HttpResponse, attempt: int, method: str = 'GET') -> Self: # noqa: ARG004
4749
"""Dispatch to the subclass matching the response's error `type`, if any."""
50+
payload = _extract_error_payload(response)
4851
target_cls: type[ApifyApiError] = cls
49-
if cls is ApifyApiError:
50-
# Local import to avoid the circular dependency (`_generated_errors` imports us).
51-
from apify_client._generated_errors import API_ERROR_CLASS_BY_TYPE # noqa: PLC0415
52+
if cls is ApifyApiError and payload is not None:
53+
error_type = payload.get('type')
54+
if isinstance(error_type, str):
55+
# avoid circular import with _generated_errors
56+
from apify_client._generated_errors import API_ERROR_CLASS_BY_TYPE # noqa: PLC0415
5257

53-
error_type = _extract_error_type(response)
54-
if error_type is not None:
55-
subclass = API_ERROR_CLASS_BY_TYPE.get(error_type)
56-
if subclass is not None:
57-
target_cls = subclass
58-
return super().__new__(target_cls)
58+
target_cls = API_ERROR_CLASS_BY_TYPE.get(error_type, cls)
59+
instance = super().__new__(target_cls)
60+
instance._apify_error_payload = payload
61+
return instance
5962

6063
def __init__(self, response: HttpResponse, attempt: int, method: str = 'GET') -> None:
6164
"""Initialize the API error from a failed response.
@@ -65,27 +68,21 @@ def __init__(self, response: HttpResponse, attempt: int, method: str = 'GET') ->
6568
attempt: The attempt number when the request failed (1-indexed).
6669
method: The HTTP method of the failed request.
6770
"""
68-
self.message: str | None = None
71+
# Prefer the payload stashed by __new__; fall back to re-parsing for direct subclass
72+
# instantiation (e.g. if a user constructs a subclass without going through the base class).
73+
payload = getattr(self, '_apify_error_payload', None)
74+
if payload is None:
75+
payload = _extract_error_payload(response)
76+
77+
self.message: str | None = f'Unexpected error: {response.text}'
6978
self.type: str | None = None
7079
self.data = dict[str, str]()
71-
self.message = f'Unexpected error: {response.text}'
72-
73-
try:
74-
response_data = response.json()
7580

76-
if (
77-
isinstance(response_data, dict)
78-
and 'error' in response_data
79-
and isinstance(response_data['error'], dict)
80-
):
81-
self.message = response_data['error']['message']
82-
self.type = response_data['error']['type']
83-
84-
if 'data' in response_data['error']:
85-
self.data = response_data['error']['data']
86-
87-
except ValueError:
88-
pass
81+
if payload is not None:
82+
self.message = payload['message']
83+
self.type = payload['type']
84+
if 'data' in payload:
85+
self.data = payload['data']
8986

9087
super().__init__(self.message)
9188

@@ -95,19 +92,16 @@ def __init__(self, response: HttpResponse, attempt: int, method: str = 'GET') ->
9592
self.http_method = method
9693

9794

98-
def _extract_error_type(response: HttpResponse) -> str | None:
99-
"""Return the `error.type` field from the response body, or None if absent or unparsable."""
95+
def _extract_error_payload(response: HttpResponse) -> dict[str, Any] | None:
96+
"""Return the `error` dict from the response body, or None if absent or unparsable."""
10097
try:
10198
data = response.json()
10299
except ValueError:
103100
return None
104101
if not isinstance(data, dict):
105102
return None
106103
error = data.get('error')
107-
if not isinstance(error, dict):
108-
return None
109-
error_type = error.get('type')
110-
return error_type if isinstance(error_type, str) else None
104+
return error if isinstance(error, dict) else None
111105

112106

113107
@docs_group('Errors')
@@ -132,8 +126,4 @@ def __init__(self, response: HttpResponse) -> None:
132126
self.response = response
133127

134128

135-
# Re-export the generated per-type Exception subclasses so users can do e.g.
136-
# `from apify_client.errors import RecordNotFoundError`. The star import is safe because
137-
# `_generated_errors` defines an `__all__` and `errors.py` is fully initialized at the point
138-
# the re-import triggers (the module-level classes above are already bound).
139129
from apify_client._generated_errors import * # noqa: E402, F403

0 commit comments

Comments
 (0)