Package: failures
FailuresSuccessfully dealing with failuresWhat is failuresThis library simplifies dealing with application failures especially when using multiple nested components
that process some data and could fail at any point.Often in production, the app must be robust enough against errors but still report them so we can improve it regularly,
this requires us as developers to isolate those operations(usually withintry...exceptblocks)then catch
any possible errors to report them and provide an alternative result(like returningNone).
This is an additional job to be done to keep achieve the production robustness.Meanwhile, this library suggests some tools to ease this process and divides it into two main phases,
the first is capturing failures through an application action and between function calls while giving them
meaningful labels and optionally explicit metadata to help us understand them later and their context,
the second phase is to process those collected failures with the ability to filter and choose different
handling action for different type of failures.The library focuses on two main factors, simplicity and performance, by keeping the syntax easy, clean and intuitive
for a better and more readable code, and by optimizing its code to minimize the impact on your application.Installationfailuresis available at PyPI, it requires python 3.8 or higher, to install it run thepipcommand:pipinstallfailuresExampleThis example will show the tip of the iceberg, for a more complete tutorial refer to the documentation page atdocumentation page.failurescontains two main objects,ReporterandHandler, the first one gathers all failures and the second
on processes them.Let's define some utilities for this example in a file nameddefs.pyimportjsonimportloggingfromdataclassesimportdataclassfromfailuresimportReporter,Handler,Failure_database={'692999103396568d':b'{"name": "alertCheetah2", "email": "alertCheetah2@example.com", "phone": "(+212)645-882-425"}','e2ddc4f976f8ccf8':b'{"name": "ardentJaguar0", "phone": "(+212)611-962-964"}','d097eba4d97a1ce0':b'{"name": "tautWeaver8", "email": "tautWeaver8@example.com", "phone": "(+212)683-480-745"}','ef0eb816db00f939':b'{"name": "awedPolenta7", "email": "awedPolenta7@example.com", "phone": "(+212)641-014-059"}','b54c8bea6badd65d':b'{"name": "dearCod2", "email": "dearCod2@example.com", "website": "www.dearCod2.example.com"}','bad-8394313d4c44':b'{"name": "panickyViper9", "ema'}@dataclassclassUser:id:strname:stremail:str|Nonephone:str|Nonedefnot_found_404(failure:Failure)->None:try:user_id=failure.details['id']exceptKeyError:returnlogging.getLogger(failure.source).error(f"No user found with id ={user_id!r}",exc_info=failure.error)handler=Handler((not_found_404,"*.retrieve"),print)defget_user(user_id:str)->User|None:reporter=Reporter('user')withhandler:withreporter('retrieve',id=user_id):raw_json=_database[user_id]withreporter('json_decode'):user_data=json.loads(raw_json)rep_get=reporter('get',data=user_data)user=User(id=user_id,name=rep_get('name').required(lambda:user_data['name']),email=rep_get('email').safe(user_data.__getitem__,'email'),phone=rep_get('phone').optional(user_data.__getitem__,'phone'))handler.from_reporter(reporter)returnuserCode explainedWe declared a dummy data collection_databaseto act as a database, then we defined a data classUserto wrap user data.We define then a custom handlernot_found_404to be called only when a user is not found, this handler takes
aFailureobject and returns nothing, all handlers should have this same signature.Then we create aHandlerobject used to handle application failures, we pass the custom handler under a condition'*.retrieve', the condition means every label that ends with'retrieve'like in this case'user.retrieve'.After that we define our main actionget_user(user_id: str) -> User | Nonethat takes an id and returns a user if
it exists, we evaluate all its instructions within the handler context to capture failures, then we create the root
reporter labeled'user', all following operations will be labeleduser.(somthing), then we try to retrieve
a user by id_database[user_id], and as this might potentially fail, we execute it inside the reporter's context
under the labelretrieve, this context is holding theuser_idas additional detail.
Using the reporter as context managerwith reporter:ensures that the functions returns directly to the handler in
case of failure, the next code won't be executed.
The same for the next instruction, we wrap it into a labeled scopejson_decodeto label it and mark it as required
step.Then we create aUserobject, this time using inline reporter context, there is three;reporter.required(...)for mandatory operations and it's the same aswith reporter: ..., it stops and returns to the handler in case
of failure,reporter.safe(...)does capture and keep the failure to be processed later and returnsNoneas
alternative result, andreporter.optional(...)does the same but without reporting the failure.
All the three expect a callable with or without parameters to be called soreporter.safe(func, 1, 2, a=3, b=4)executesfunc(1, 2, a=3, b=4)in isolation.After that and finally we process those reported failures and return the result.UsageNow let's jump into the interpreter and load our function and start calling it with different ids:>>>fromdefsimportget_user>>># Existing users>>>get_user('692999103396568d')User(id='692999103396568d', name='alertCheetah2', email='alertCheetah2@example.com', phone='(+212)645-882-425')>>>get_user('e2ddc4f976f8ccf8')# Missing email reported and handled with print(failure)Failure(source='user.get.email', error=KeyError('email'), details={'data': {'name': 'ardentJaguar0', 'phone': '(+212)611-962-964'}})User(id='e2ddc4f976f8ccf8', name='ardentJaguar0', email=None, phone='(+212)611-962-964')>>>get_user('d097eba4d97a1ce0')User(id='d097eba4d97a1ce0', name='tautWeaver8', email='tautWeaver8@example.com', phone='(+212)683-480-745')>>>get_user('ef0eb816db00f939')User(id='ef0eb816db00f939', name='awedPolenta7', email='awedPolenta7@example.com', phone='(+212)641-014-059')>>>get_user('b54c8bea6badd65d')# Missing phone number ignoredUser(id='b54c8bea6badd65d', name='dearCod2', email='dearCod2@example.com', phone=None)>>># Bad json format>>>get_user('bad-8394313d4c44')# returns NoneFailure(source='user.json_decode', error=JSONDecodeError('Unterminated string starting at: line 1 column 27 (char 26)'), details={})>>># Non-existing users>>>get_user('random-id')ERROR:user.retrieve:No user found with id = 'random-id'Traceback (most recent call last):File"/path/to/defs.py", line35, inget_userKeyError:'random-id'Failure(source='user.retrieve', error=KeyError('random-id'), details={'id': 'random-id'})