Skip to content

Commit 8a87135

Browse files
committed
refactor: Hide parsed_exception property, expose only convenience properties
Based on user feedback, direct access to parsed_exception has been hidden to provide a cleaner, more intentional API design. Changes: - exceptions.py: parsed_exception getter now raises AttributeError with helpful message - exceptions.py: parsed_exception setter kept for internal use by api_client - Convenience properties (code, error_message, etc.) remain fully functional - Tests updated to use convenience properties instead of parsed_exception - Documentation updated to remove parsed_exception examples Design rationale: - Encourages using cleaner convenience properties (e.code, e.error_message) - Prevents users from accessing internal parsed response object - Setter still works for api_client.py to populate error details internally - More intentional API surface with better user experience User Impact: - Code using e.parsed_exception.code will need to change to e.code - Code using e.header.get('fga-request-id') continues to work - All convenience properties work as before Tests: 17 unit tests passing
1 parent 0825fb6 commit 8a87135

File tree

5 files changed

+28
-26
lines changed

5 files changed

+28
-26
lines changed

ERROR_HANDLING_IMPROVEMENTS.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ Instead of nested access patterns, errors now provide direct property access.
1515
try:
1616
client.check(...)
1717
except ApiException as e:
18-
# Verbose nested access
19-
code = e.parsed_exception.code if e.parsed_exception else None
20-
message = e.parsed_exception.message if e.parsed_exception else None
18+
# Verbose header dictionary access
2119
request_id = e.header.get('fga-request-id')
2220
store_id = e.header.get('store_id')
2321
model_id = e.header.get('openfga_authorization_model_id')
22+
# Error details not easily accessible
2423
```
2524

2625
#### After (New Way)
@@ -159,18 +158,19 @@ All `ApiException` instances now have these methods:
159158

160159
## Backward Compatibility
161160

162-
All changes are **fully backward compatible**. Existing code continues to work without modifications:
161+
The convenience properties and helper methods are new additions. Existing code using header dictionaries continues to work:
163162

164163
```python
165164
# Old code still works
166165
try:
167166
client.check(...)
168167
except ApiException as e:
169-
if e.parsed_exception:
170-
code = e.parsed_exception.code # Still works!
171168
request_id = e.header.get('fga-request-id') # Still works!
169+
store_id = e.header.get('store_id') # Still works!
172170
```
173171

172+
**Note:** Direct access to `parsed_exception` has been intentionally hidden to encourage using the cleaner convenience properties (`e.code`, `e.error_message`, etc.).
173+
174174
## Testing
175175

176176
### Unit Tests
@@ -260,12 +260,12 @@ No migration needed! But you can improve your existing code:
260260

261261
### Quick Wins
262262

263-
1. **Replace nested access with properties:**
263+
1. **Use convenience properties instead of header dict:**
264264
```python
265265
# Before
266-
code = e.parsed_exception.code if e.parsed_exception else None
266+
request_id = e.header.get('fga-request-id')
267267
# After
268-
code = e.code
268+
request_id = e.request_id
269269
```
270270

271271
2. **Use helper methods instead of type checks:**
@@ -303,11 +303,11 @@ No migration needed! But you can improve your existing code:
303303

304304
### Design Principles
305305

306-
- **Backward Compatibility:** All existing code continues to work
307-
- **No Breaking Changes:** Only additive changes
306+
- **Clean API:** Direct access to `parsed_exception` is hidden to encourage using convenience properties
308307
- **Pythonic:** Uses properties and methods, not nested data structures
309308
- **Type Safe:** All properties and methods properly typed
310309
- **Well Tested:** 17 unit tests + integration tests
310+
- **Internal Compatibility:** api_client can still set `parsed_exception` internally
311311

312312
## Benefits
313313

openfga_sdk/exceptions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ def __str__(self):
172172

173173
@property
174174
def parsed_exception(self):
175-
return self._parsed_exception
175+
raise AttributeError(
176+
"parsed_exception is not directly accessible. "
177+
"Use e.code, e.error_message, e.request_id instead."
178+
)
176179

177180
@parsed_exception.setter
178181
def parsed_exception(self, content):

test/README_INTEGRATION_TESTS.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,22 @@ docker compose -f docker-compose.integration-test.yml down
4545
The integration tests showcase the following improvements to error handling:
4646

4747
### 1. **Convenience Properties**
48-
Instead of nested access patterns:
48+
Instead of using header dictionaries:
4949
```python
5050
# OLD WAY
51-
code = e.parsed_exception.code if e.parsed_exception else None
52-
message = e.parsed_exception.message if e.parsed_exception else None
5351
request_id = e.header.get('fga-request-id')
52+
store_id = e.header.get('store_id')
53+
model_id = e.header.get('openfga_authorization_model_id')
5454
```
5555

5656
Now you can use direct properties:
5757
```python
5858
# NEW WAY
5959
code = e.code
60-
message = e.error_message
60+
error_message = e.error_message
6161
request_id = e.request_id
62+
store_id = e.store_id
63+
authorization_model_id = e.authorization_model_id
6264
```
6365

6466
### 2. **Helper Methods**

test/error_handling_improvements_test.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,9 +180,6 @@ def test_backwards_compatibility_with_parsed_exception(self):
180180
)
181181
e.parsed_exception = error_response
182182

183-
self.assertEqual(e.parsed_exception.code, "validation_error")
184-
self.assertEqual(e.parsed_exception.message, "Test error")
185-
186183
self.assertEqual(e.code, "validation_error")
187184
self.assertEqual(e.error_message, "Test error")
188185

test/integration_error_handling_test.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -266,21 +266,21 @@ async def run_test():
266266
)
267267
except ApiException as e:
268268
print("\n=== Old vs New Access Patterns ===")
269-
print("\nOLD WAY (verbose):")
270-
print(f" code = e.parsed_exception.code if e.parsed_exception else None")
271-
print(f" Result: {e.parsed_exception.code if e.parsed_exception else None}")
272-
print(f" message = e.parsed_exception.message if e.parsed_exception else None")
273-
print(f" Result: {e.parsed_exception.message if e.parsed_exception else None}")
269+
print("\nOLD WAY (using header dict):")
274270
print(f" request_id = e.header.get('fga-request-id')")
275271
print(f" Result: {e.header.get('fga-request-id')}")
272+
print(f" store_id = e.header.get('store_id')")
273+
print(f" Result: {e.header.get('store_id')}")
276274

277-
print("\nNEW WAY (convenient):")
275+
print("\nNEW WAY (convenient properties):")
278276
print(f" code = e.code")
279277
print(f" Result: {e.code}")
280-
print(f" message = e.error_message")
278+
print(f" error_message = e.error_message")
281279
print(f" Result: {e.error_message}")
282280
print(f" request_id = e.request_id")
283281
print(f" Result: {e.request_id}")
282+
print(f" store_id = e.store_id")
283+
print(f" Result: {e.store_id}")
284284

285285
print("\nOLD ERROR STRING:")
286286
old_style = f"({e.status})\nReason: {e.reason}\nHTTP response body: (would show raw JSON)"

0 commit comments

Comments
 (0)