Package: puffy
PUFFYA collection of modules with zero-dependencies to help manage common programming tasks.pip install puffyUsage examples:frompuffy.errorimportcatch_errors# This function will never fail. Instead, the error is safely caught.@catch_errorsdeffail():raiseException("Failed")return"yes"err,resp=fail()# `err` and `resp` are respectively None and Object when the function is successull. Otherwise, they are respectively StackedException and None.frompuffy.objectimportJSONasjsobj=js({'hello':'world'})obj['person']['name']='Nic'# Notice it does not fail.obj.s('address.line1','Magic street')# Sets obj.address.line1 to 'Magic street' and return 'Magic street'Table of contentsAPIserrorBasicerrorAPIs - Getting in control of your errorsNested errors and error stackManaging errors inasync/awaitcorountineslogBasiclogAPIsLogging errorsEnvironment variablesGlobal contextobjectJSONAPIDevGetting startedCLI commandsInstall dependencies witheasypipinstallLinting, formatting and testingIgnoringflake8errorsSkipping testsExecuting a specific test onlyBuilding and distributing this packageFAQReferencesLicenseAPIserrorTheerrormodule exposes the following APIs:catch_errors: A higher-order function that returns a function that always return a tuple(error, response). If theerrorisNone, then the function did not fail. Otherwise, it did and theerrorobject can be used to build an error stack.StackedException: A class that inherits fromException. Use it to stack errors.BasicerrorAPIs - Getting in control of your errorsfrompuffy.errorimportcatch_errors# This function will never fail. Instead, the error is safely caught.@catch_errorsdeffail():raiseException("Failed")return"yes"err,resp=fail()print(resp)# Noneprint(type(err))# <class 'src.puffy.error.StackedException'> which inherits from Exceptionprint(str(err))# Failedprint(len(err.stack))# 1print(str(err.stack[0]))# Failedprint(err.stack[0].__traceback__)# <traceback object at 0x7fc69066bf00># Use the `strinfigy` method to extract the full error stack details.print(err.strinfigy())# error: Failed#   File "blablabla.py", line 72, in safe_exec#     data = ffn(*args, **named_args)#   File "blablabla.py", line 28, in fail#     raise Exception("Failed")Nested errors and error stackfrompuffy.errorimportcatch_errors,StackedException# This function will never fail. Instead, the error is safely caught.@catch_errors("Should fail")deffail():err,resp=fail_again()iferr:raiseStackedException("As expected, it failed!",err)# StackedException accepts an arbitrary number of inputs of type str or Exception:# 	- raise StackedException(err)# 	- raise StackedException('This', 'is', 'a new error')return"yes"@catch_errors("Should fail again")deffail_again():raiseException("Failed again")return"yes"err,resp=fail()print(len(err.stack))# 4print(str(err.stack[0]))# Should failprint(str(err.stack[1]))# As expected, it failed!print(str(err.stack[2]))# Should fail againprint(str(err.stack[3]))# Failed again# Use the `strinfigy` method to extract the full error stack details.print(err.strinfigy())# error: Should fail#   File "blablabla.py", line 72, in fail# error: As expected, it failed!#   File "blablabla.py", line 72, in fail# error: Should fail again#   File "blablabla.py", line 72, in fail# error: Failed again#   File "blablabla.py", line 72, in safe_exec#     data = ffn(*args, **named_args)#   File "blablabla.py", line 28, in fail_again#     raise Exception("Failed")Managing errors inasync/awaitcorountinesfrompuffy.errorimportasync_catch_errorsimportasyncio# This function will never fail. Instead, the error is safely caught.@async_catch_errorsasyncdeffail():awaitasyncio.sleep(0.01)raiseException("Failed")return"yes"loop=asyncio.get_event_loop()err,resp=loop.run_until_complete(fail())print(resp)# Noneprint(type(err))# <class 'src.puffy.error.StackedException'> which inherits from Exceptionprint(str(err))# Failedprint(len(err.stack))# 1print(str(err.stack[0]))# Failedprint(err.stack[0].__traceback__)# <traceback object at 0x7fc69066bf00># Use the `strinfigy` method to extract the full error stack details.print(err.strinfigy())# error: Failed#   File "blablabla.py", line 72, in safe_exec#     data = ffn(*args, **named_args)#   File "blablabla.py", line 28, in fail#     raise Exception("Failed")logBasiclogAPIsThis method prints a structured log to stdout. That structured log is a standard Pythondictwhich is then serialized tostrusingjson.dumps. This method is designed to never fail. It was originally designed to log messages to AWS CloudWatch.frompuffy.logimportloglog()# '{ "level":"INFO" }'log(level="WARN",# Supported values: "INFO" (default), "WARN" (or "WARNING"), "ERROR", "CRITICAL"message="Seems drunk",code="drunky_drunky",metric=23,unit="beers",# Default is "ms" (i.e., milliseconds)data={"name":"Dave","age":45},op_id=12345,test=True)# '{"level": "WARN", "message": "Seems drunk", "code": "drunky_drunky", "test": true, "metric": 23, "unit": "beers", "op_id": 12345, "data": {"name": "Dave", "age": 45}}'# Logging time:log(level="WARN",# Supported values: "INFO" (default), "WARN" (or "WARNING"), "ERROR", "CRITICAL"message="Seems drunk",code="drunky_drunky",time=34# This is converted to the "metric" input with "unit" set to "ms" (cannot be overwritten))# '{"level": "WARN", "message": "Seems drunk", "code": "drunky_drunky", "metric": 34, "unit": "ms"}'Logging errorsThelogAPI is designed to support puffy'sStackedExceptionerrors. The advantage of usingStackedExceptionis that you can have confidence that the stacked errors are properly serialized (i.e., including message and traceback).frompuffy.logimportlogfrompuffy.errorimportcatch_errors,StackedExceptionase@catch_errors("Should fail")deffail():err,resp=fail_again()iferr:raisee(err)return"yes"@catch_errors("Should fail again")deffail_again():raiseException("Failed again")return"yes"err,*_=fail()# Supports `StackedException`log(level="ERROR",errors=err)# '{"level": "INFO", "errors": "error: Should fail\n  File \"/Users/.../ur_code.py\", line 153, in fail\nerror: Should fail again\n  File \"/Users/.../ur_code.py\", line 153, in fail\nerror: Failed again\n  File \"/Users/.../ur_code.py\", line 112, in safe_exec\n    data = ffn(*args, **named_args)\n  File \"/Users/.../ur_code.py\", line 162, in fail_again\n    raise Exception(\"Failed again\")\n"}'# Supports standard errorslog(level="ERROR",errors=Exception("Bim bam boom"))# '{"level": "ERROR", "errors": "Bim bam boom"}'# Supports stringslog(level="ERROR",errors="Bim bam boom")# '{"level": "ERROR", "errors": "Bim bam boom"}'# Supports list of errorslog(level="ERROR",errors=["Bim bam boom",Exception("Booom"),err])# '{"level": "ERROR", "errors": "Bim bam boom\nBooom\nerror: Should fail\n  File \"/Users/.../ur_code.py\", line 153, in fail\nerror: Should fail again\n  File \"/Users/.../ur_code.py\", line 153, in fail\nerror: Failed again\n  File \"/Users/.../ur_code.py\", line 112, in safe_exec\n    data = ffn(*args, **named_args)\n  File \"/Users/.../ur_code.py\", line 162, in fail_again\n    raise Exception(\"Failed again\")\n"}'Environment variablesOften, specific common metadata must be added to all logs (e.g., server's details, api name, ...). For this purpose, use theLOG_METAenvironment variable. This environment variable expects a stringified JSON object:frompuffy.logimportlogos.environ["LOG_META"]=json.dumps({"api_name":"hello"})log(level="INFO",message="hello world")# '{"api_name": "hello", "level": "INFO", "message": "hello world"}'Global contextpuffy supports setting up a context globally. That context is a dictionary global to the current execution thread. By default, that context contains no keys (i.e.,{}). If that context is set as follow:{"hello":"world"}Then, all logs include that keyvalue pair.This global context can be accessed as follow:frompuffy.logimportlog,set_context,get_context,reset_contextlog(level="INFO",message="hello world")# '{"level": "INFO", "message": "hello world"}'print(get_context())# {}set_context(hello="world",whatever="you want")log(level="INFO",message="hello world")# '{"hello":"world", "whatever":"you want", "level": "INFO", "message": "hello world"}'print(get_context())# {"hello":"world", "whatever":"you want"}reset_context()log(level="INFO",message="hello world")# '{"level": "INFO", "message": "hello world"}'print(get_context())# {}objectJSONAPIfrompuffy.objectimportJSONasjsobj=js({'hello':'world'})obj['person']['name']='Nic'# Notice it does not fail.obj.s('address.line1','Magic street')# Sets obj.address.line1 to 'Magic street' and return 'Magic street'print(obj['person']['name'])# Nicprint(obj)# { 'hello':'world', 'person': { 'name': 'Nic' } }print(obj.g('address.line1'))# Magic streetprint(obj)# { 'hello':'world', 'person': { 'name': None }, 'address': { 'line1': 'Magic street' } }print(obj.g('address.line2'))# Nonceprint(obj)# { 'hello':'world', 'person': { 'name': None }, 'address': { 'line1': 'Magic street', line2: None } }DevDev - Getting startedClone this project:gitclonehttps://github.com/nicolasdao/pypuffy.gitBrowse to the root folder:cdpypuffyCreate a new virtual environment:python3-mvenv.venvActivate this virtual environment:source.venv/bin/activateTo deactivate that virtual environment:deactivateCLI commandsmakecommands:CommandDescriptionpython3 -m venv .venvCreate a new virtual environment.source .venv/bin/activateActivate the virtual environmentdeactivateDeactivate the virtual environmentmake bBuilds the package.make pPublish the package tohttps://pypi.org.make bpBuilds the package and then publish it tohttps://pypi.org.make biBuilds the package and install it locally (pip install -e .).make installInstall the dependencies defined in therequirements.txt. This file contains all the dependencies (i.e., both prod and dev).make install-prodInstall the dependencies defined in theprod-requirements.txt. This file only contains the production dependencies.make nStarts a Jupyter notebook for this project.make tFormats, lints and then unit tests the project.make t testpath=<FULLY QUALIFIED TEST PATH>Foccuses the unit test on a specific test. For a concrete example, please refer to theExecuting a specific test onlysection.easyi numpyInstalsnumpyand updatesetup.cfg,prod-requirements.txtandrequirements.txt.easyi flake8 -DInstalsflake8and updatesetup.cfgandrequirements.txt.easyu numpyUninstalsnumpyand updatesetup.cfg,prod-requirements.txtandrequirements.txt.easyvReturns the version defined insetup.cfg.easyv bumpBumps the patch version defined insetup.cfg(1).easyv bump minorBumps the minor version defined insetup.cfg(1).easyv bump majorBumps the major version defined insetup.cfg(1).easyv bump x.x.xSets the version defined insetup.cfgto x.x.x (1).(1):Bumping a version usingeasyvcan apply up to three updates:Updates the version property in thesetup.cfgfile.If the project is under source control with git and git is installed:Updates theCHANGELOG.mdfile using the commit messages between the current branch and the last version tag. If theCHANGELOG.mdfile does not exist, it is automatically created.git commit and tag (using the version number prefixed withv) the project.Install dependencies witheasypipinstalleasypipinstalladds three new CLI utilities:easyi(install)easyu(uninstall) andeasyv(manages package's version). To learn the full details abouteasypipinstall, please refer tohttps://github.com/nicolasdao/easypipinstall.Examples:easyi numpyThis installsnumpy(viapip install) then automatically updates the following files:setup.cfg(WARNING: this file must already exists):[options]
 install_requires = 
 	numpyrequirements.txtandprod-requirements.txteasyi flake8 black -DThis installsflake8andblack(viapip install) then automatically updates the following files:setup.cfg(WARNING: this file must already exists):[options.extras_require]
 dev = 
 	black
 	flake8requirements.txtonly, as those dependencies are installed for development purposes only.easyu flake8This uninstallsflake8as well as all its dependencies. Those dependencies are uninstalled only if they are not used by other project dependencies. Thesetup.cfgandrequirements.txtare automatically updated accordingly.Linting, formatting and testingmake tThis command runs the following three python executables:black ./
flake8 ./
pytest --capture=no --verbose $(testpath)blackformats all the.pyfiles, whileflake8lints them.blackis configured in thepyproject.tomlfile under the[tool.black]section.flake8is configured in thesetup.cfgfile under the[flake8]section.pytestruns all the.pyfiles located under thetestsfolder. The meaning of each option is as follow:--capture=noallows theprintfunction to send outputs to the terminal.--verbosedisplays each test. Without it, the terminal would only display the count of how many passed and failed.$(testpath)references thetestpathvariable. This variable is set totests(i.e., thetestsfolder) by default. This allows to override this default variable with something else (e.g., a specific test to only run that one).Ignoringflake8errorsThis project is pre-configured to ignore certainflake8errors. To add or removeflake8errors, update theextend-ignoreproperty under the[flake8]section in thesetup.cfgfile.Skipping testsIn your test file, add the@pytest.mark.skip()decorator. For example:importpytest@pytest.mark.skip()deftest_self_describing_another_test_name():# ... your test hereExecuting a specific test onlyOne of the output of themake tcommand is list of all the test that were run (PASSED and FAILED). For example:tests/error/test_catch_errors.py::test_catch_errors_basic PASSED
tests/error/test_catch_errors.py::test_catch_errors_wrapped PASSED
tests/error/test_catch_errors.py::test_catch_errors_nested_errors PASSED
tests/error/test_catch_errors.py::test_catch_errors_StackedException_arbitrary_inputs FAILEDTo execute a specific test only, add thetestpathoption with the test path. For example, to execute the only FAILED test in the example above, run this command:make t testpath=tests/error/test_catch_errors.py::test_catch_errors_StackedException_arbitrary_inputsBuilding and distributing this packageMake sure the test and lint operations have not produced errors:maketVersion and tag this package using one of the following commands (1):easyv bump: Use this to bump the patch version.easyv bump minor: Use this to bump the minor version.easyv bump major: Use this to bump the major version.easyv bump x.x.x: Use this to bump the version to a specific value.Push those latest changes to your source control repository (incl. tags). For example:gitpushoriginmaster--follow-tagsBuild this package:makebThis command is a wrapper aroundpython3 -m build.Publish this package tohttps://pypi.org:makepThis command is a wrapper around the following commands:python3 -m build; twine upload dist/*To test your package locally before deploying it tohttps://pypi.org, you can run build and install it locally with this command:makebiThis command buils the package and follows withpip install -e ..(1): This step applies three updates:Updates the version property in thesetup.cfgfile.Updates theCHANGELOG.mdfile using the commit messages between the current branch and the last version tag.git commit and tag (using the version number prefixed withv) the project.FAQReferencesLicenseBSD 3-Clause LicenseCopyright (c) 2019-2023, Cloudless Consulting Pty Ltd
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice, this
   list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
   this list of conditions and the following disclaimer in the documentation
   and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
   contributors may be used to endorse or promote products derived from
   this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.