{ "info": { "author": "Bryan W. Berry", "author_email": "bryan.berry@gmail.com", "bugtrack_url": null, "classifiers": [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.3", "Programming Language :: Python :: 3.4", "Topic :: Software Development :: Libraries :: Python Modules" ], "description": "======================================================================\ntryme - Error handling for humans\n======================================================================\n\n\nIntroduction\n============\n\nTryme is a module that makes it easier to treat errors as values. Additionally,\ntryme helps you handle the absence of a value (None) in a more composable way.\n\nThe entire tryme library is contained in a single file `tryme.py` to\nmake it as easy as possible to drop into an existing project or script.\n\nWhy should I care?\n--------------------\n \nTreating errors as values makes it easier to retain information related to errors and\nlets us defer error handling until the last possible moment. In contrast, exceptions are\nfundamentally lossy in they by convention contain tracebacks and error messages not values.\nFurther, exceptions abort execution by default and break normal code flow.\n\nHere are a few tasks where we might wish to defer error handling.\n\n* Processing multiple operations in batch where one or more operations may fail\n* Executing long running operations that require multiple retries\n\nTreating failed operations as values rather than exceptions can simplify conditional logic and\nmake programs more composable.\n \n*But c'mon exceptions are pythonic*\n\nA common convention for handling errors in Python is to raise exceptions. There\nis no reason however that this is always the best mechanism to handle errors,\nespecially *expected errors*. The author of this package feels strongly\nthat exceptions are best used to represent exceptional circumstances\nsuch as when undefined behavior is encountered.\n\n\nError Handling Strategies\n------------------------------\n\nA good error handling strategy retains relevant information and defers side\neffects related to a failure as long as possible to allow upstream code to\ndecide how to handle the failure.\n\nHere are four error handling strategies that I have encountered:\n\n1. Return a boolean to indicate success/failure\n2. Return a tuple of (err, value) where err represents a possible error and value\n is well, a value\n3. Return a value for success and raise an exception in the case of an error\n4. Return a value in case of success and a custom Error class that contains information\n about the failure but is not necessarily a subclass of `Exception`\n5. Return an instance of Success or Failure containing a value\n\n\nStrategy #1 Return a boolean\n-------------------------------\n\nThe simplest possible strategy is to return a boolean to indicate the success or\nfailure. For a simple example, let's build a trivial program to check if Google is accessible.\n\nOur first iteration will simply return a boolean to indicate if Google is accessible.\n::\n\n import requests\n\n\n def google_is_accessible():\n response = requests.get('http://google.com')\n return response.ok\n\n is_accessible = google_is_accessible()\n if is_accessible:\n print('It works!')\n else:\n print('Google is not accessible. No idea why')\n\nAwesome! We now can check if Google is accessible from our remote location. The big drawback\nis that `google_is_accessible` doesn't tell me what went wrong in case of a failure nor\ndoes give any information to figure that out for myself.\n\n\nStrategy #2 Return a tuple of (err, value)\n---------------------------------------------\n\nA similar approach is to return a tuple of (err, value) where err indicates if an error\noccurred. This approach is idiomatic in the Go programming language.\n::\n\n import requests\n\n\n def google_is_accessible():\n response = requests.get('http://google.com')\n return response.ok, response\n\n is_accessible, response = google_is_accessible()\n if is_accessible:\n print('It works!')\n else:\n print('Google is not accessible, received http status %s' % response.status_code)\n\n\nThis is a big improvement! We now can determine what went wrong should we care.\n\nThe main drawback of returning a tuple to indicate errors is that it makes it harder to\ncompose functions. Let's extend our simple example to try multiple search engines in case\nGoogle is not accessible.::\n\n import requests\n\n\n def duck_duck_go_is_accessible():\n response = requests.get('http://google.com')\n return response.ok, response\n\n \n def google_is_accessible():\n response = requests.get('http://google.com')\n return response.ok, response\n\n \n is_accessible, response = google_is_accessible()\n if is_accessible:\n print('It works! Using Google')\n else:\n print('Google is not accessible, received http status %s. Trying duckduckgo'\n % response.status_code))\n is_accessible, response = duck_duck_go_is_accessible()\n if is_accessible:\n print('It works! Using DuckDuckGo')\n else:\n print('DuckDuckGo is not accessible, received http status %s. Out of options'\n % response.status_code))\n\nThe conditionals in the above example can be reduced but it is apparent that returning a tuple\nadds more conditional logic to your code.\n\nStrategy #3 Raise an Exception\n---------------------------------\n\nHere is our example using good old ``try/except``::\n\n import requests\n\n\n class GoogleNotAccessibleError(Exception):\n pass\n \n \n def google_is_accessible():\n response = requests.get('http://google.com')\n if response.ok:\n return response.ok\n else:\n return GoogleNotAccessibleError(\n \"http request to google.com failed with status code %s\" % response.status_code)\n\n try:\n is_accessible = google_is_accessible()\n except GoogleNotAccessibleError as e:\n is_accessible = False\n print(e.message)\n\nThere are pros and cons to the above. We do get back some useful information about the failure.\nUnfortunately, we do not get back the response object so we cannot dig deeper into the response\nto determine the cause of the error. To get the HTTP status code we have search the error message.\n\nAnother drawback is that the raised exception is a side effect that we have to\nhandle immediately and cannot be deferred until later. Raising an exception also\ngenerates something we don't need, a stacktrace.\n\nOne big positive here is that we can subclass exception to indicate the particular problem that\noccurred.\n\nStrategy #4 Return a custom Error in case of Failure\n------------------------------------------------------\n\nInstead of raising an Exception, you can simply return an `Error` in case of\nfailure where Error is an object that is an exception or looks a lot like one.\n::\n\n import requests\n\n\n class GoogleNotAccessibleError():\n\n def __init__(self, message, response):\n self.message = message\n self.response = response\n \n def google_is_accessible():\n response = requests.get('http://google.com')\n if response.ok:\n return response.ok\n else:\n return GoogleNotAccessibleError(\n \"http request to google.com failed\", response)\n\n result = google_is_accessible()\n if result is True:\n print('It worked!')\n else:\n print(result.message)\n print('HTTP request failed with status code %d' result.value.status_code)\n\nThis is a big improvement! We can quickly determine if google is accessible and have\naccess to all the information in the request. The main drawback to returning a custom\nerror is that each implementation is likely custom. The calling code has\nto know the internals of the returned Error class.\n\n\nStrategy #5 Return an instance of Success or Failure containing a value\n-------------------------------------------------------------------------\n\nThis final strategy refines the custom Error with standard semantics. As it turns out there\na standard paradigm in the `Either class `_ that is present in Clojure, Scala, and other languages. This package\nimplements the Either class under the name `Try` as your dear author believes it\nis a more intuitive name.\n\nThe `Try` class has two subclasses, `Success` and `Failure`. Success is used to\ncontain the result of an operation that-you guessed it-succeeded. Likewise, Failure\ncontains the result of an operation that failed.\n\nHere is the same task using the ``Success`` and ``Failure``::\n\n import requests\n from tryme import Success, Failure\n\n\n def google_is_accessible():\n response = requests.get('http://google.com')\n if response.ok:\n return Success(response)\n else:\n return Failure(response)\n\n result = google_is_accessible()\n if result.succeeded():\n print('it worked!')\n else:\n response = result.get_failure()\n print('HTTP request failed with status %d' % response.status_code)\n\nWe noted earlier that an advantage of returning exceptions is that we can subclass\nException to more specifically indicate the failure. We can do the same with\nSuccess in Failure. One obvious omission from our google_is_accessible is\nthat it does not account for a network failure.::\n\n\n import requests\n from tryme import Success, Failure\n\n\n class ConnectionFailure(Failure):\n pass\n \n \n def google_is_accessible():\n try:\n response = requests.get('http://google.com')\n except requests.exceptions.ConnectionError as e:\n return ConnectionFailure(e.message)\n if response.ok:\n return Success(response)\n else:\n return Failure(response)\n\n result = google_is_accessible()\n if result.succeeded():\n print('it worked!')\n elif isinstance(result, ConnectionFailure):\n print(result.get_message())\n else:\n response = result.get_failure()\n print('HTTP request failed with status %d' % response.status_code)\n\n\nNote that while we return a custom Failure in this case there are many cases where it\nis quite reasonable to raise an exception. As mentioned earlier, exceptions work well\nfor **unexpected** behavior and not expected behavior.\n\nSuccess and Failure have some useful helpers for reporting to the terminal.\n\nThe constructors for both Success and Failure take the optional argument `message`\nto capture a message intended for the end user. the `to_console` method prints the\nmessage to the terminal if it is not None otherwise prints a a string representation\nof the contained value.\n\n* `Success.to_console` prints the message if set otherwise prints a string representation of\n the contained value to stdout\n* `Failure.to_console` prints the message if set otherwise prints a string representation of\n the contained value to stderr\n* `Try.raise_for_error` raise an exception if the instance is a Failure\n* `Try.fail_for_error` if a Failure, print the message and exit with a non-zero return code\n\n \nRetrying with Style\n---------------------------------------------------\n\nLet's say we want to create a single server using a new Cloud computing provider\nnamed HighlyVariable Inc. HighlyVariable can provision our new server in a few\nseconds, several minutes, or occasionally not at all. Your dear author has used\ncloud services where the \"not at all\" is not so uncommon an outcome!\n\nLet's create a `server_ready` function that returns a `Success` when the server\nis ready, a `Failure` when the operation times out. A \"terminal\" state such as\n\"Ready\" or \"Failed\" will terminate retries immediately whereas a Failed will\ncontinue execution of the `server_ready` function until 300 seconds after the\nfunction was first called.\n\nIf our new server is not ready after 300 seconds, `server_ready` will return an\ninstance of `Failure`.\n\n::\n\n from tryme import retry, Success, Failure\n\n \n def create_server(name):\n return {'Name': name}\n\n status_iterator = iter(['Preparing', 'Preparing', 'Preparing', 'Ready'])\n\n \n def check_instance_status(name):\n return next(status_iterator)\n\n \n @retry\n def wait_for_server_ready_or_failed(name):\n status = check_instance_status(name)\n if status in ['Ready', 'Failed']:\n return Success(status)\n else:\n return Failure(status)\n\n \n def server_ready(name):\n # the decorated function will return two values,\n # the result of wrapped function is updated with start and end times of the\n # retry loop and the total count of attempts\n # note that the wrapped value is not modified\n result = wait_for_server_ready_or_failed(name)\n\n # a failure here only indicates a timeout\n if result.failed():\n return Failure(\"Server %s not ready after %d seconds and %d attempts\"\n % (name, result.elapsed, result.count))\n\n # unwrap the value to see what really happened\n status = result.get()\n if status == 'Ready':\n return Success(\"server %s is ready after %d seconds and %d attempts!\"\n % (name, result.elapsed, result.count))\n else:\n return Failure(\"server %s failed after %d seconds!\"\n % (name, result.elapsed))\n\n result = server_ready('jenkins')\n assert result.succeeded()\n print(\"Server jenkins is ready after %d seconds and %d attempts!\"\n % (result.elapsed, result.count))\n \nThere something a little weird about the above example. Why did we return\nSuccess when the status was \"Failed\"? This is because the return value of\nFailure in the wrapped function is a signal to the `@retry` decorator to\ncontinue retrying until the timeout is reached. As noted earlier, you\ncan subclass Success and Failure with names that make more sense for your context.\nTryme in fact comes with two subclasses py:class:`Stop` and py:class:`Again`. Let's\nrefactor the previous example to use them.::\n\n from tryme import retry, Success, Failure, Stop, Again\n\n def create_server(name):\n return {'Name': name}\n\n status_iterator = iter(['Preparing', 'Preparing', 'Preparing', 'Ready'])\n\n def check_instance_status(name):\n return next(status_iterator)\n\n @retry\n def wait_for_server_ready_or_failed(name):\n status = check_instance_status(name)\n if status in ['Ready', 'Failed']:\n return Stop(status)\n else:\n return Again(status)\n\n def server_ready(name):\n # the decorated function will return two values,\n # the result of wrapped function is updated with start and end times of the\n # retry loop and the total count of attempts\n # note that the wrapped value is not modified\n result = wait_for_server_ready_or_failed(name)\n\n # a failure here only indicates a timeout\n if result.failed():\n return Failure(\"Server %s not ready after %d seconds and %d attempts\"\n % (name, result.elapsed, result.count))\n\n # unwrap the value to see what really happened\n status = result.get()\n if status == 'Ready':\n return Success(\"server %s is ready after %d seconds and %d attempts!\"\n % (name, result.elapsed, result.count))\n else:\n return Failure(\"server %s failed after %d seconds!\"\n % (name, result.elapsed))\n\n result = server_ready('jenkins')\n assert result.succeeded()\n print(\"Server jenkins is ready after %d seconds and %d attempts!\"\n % (result.elapsed, result.count))\n \nUtility methods\n--------------------\n \nThe utility method ``try_out`` executes a callable and wraps a raised exception\nin a Failure class. If an exception was not raised, a Success is returned\n\n::\n\n >>> from tryme import try_out\n >>> result = try_out(lambda: 1 / 0)\n >>> print(result) # doctest: +SKIP\n Failure(ZeroDivisionError('integer division or modulo by zero',))\n >>> exc = result.get_failure()\n >>> exc # doctest: +SKIP\n ZeroDivisionError('integer division or modulo by zero',)\n >>> # the following would fail as it does not catch the correct exception, ZeroDivisionError\n >>> # result = try_out(lambda: 1 / 0, exception=ValueError)\n >>> result = try_out(lambda: 1 / 1)\n >>> print(result) # doctest: +SKIP\n Success(1)\n >>> result.get() # doctest: +SKIP\n 1\n\n\n\n\nRequirements\n============\n\n- CPython >= 2.7\n\n\nBackground\n============\n\nThis package is inspired by Philip Xu's excellent `monad package `_.\nIt also takes some inspiration from the excellent `vavr `_ library for java and the Scala language.\nSee this excellent `tutorial `_\non the Try utility in Scala.\n\nPssssh! This package uses *gasp* monads as the core abstraction. *Don't tell\nanyone!* They will sick the procedural programming police on your dear author. While this\npackage does have a Monad class, it does not provide general purpose\nimplementations of monad, applicative, and functor. Further it does not\nattempt to overload common Python operators to support function composition. This is\nbecause your dear author is of the opinion that Python's syntax is too limited\nto support monadic composition.\n\n\nInstallation\n============\n\nInstall from PyPI::\n\n pip install tryme\n\nInstall from source, download source package, decompress, then ``cd`` into source directory, run::\n\n make install\n\n\nLicense\n=======\n\nBSD New, see LICENSE for details.\n\n\nLinks\n=====\n\nDocumentation:\n http://tryme.readthedocs.org/\n\nIssue Tracker:\n https://github.com/bryanwb/tryme/issues/\n\nSource Package @ PyPI:\n https://pypi.python.org/bryanwb/tryme\n\nGit Repository @ Github:\n https://github.com/bryanwb/tryme/", "description_content_type": null, "docs_url": null, "download_url": "https://github.com/bryanwb/tryme/archive/0.0.5.zip", "downloads": { "last_day": -1, "last_month": -1, "last_week": -1 }, "home_page": "https://github.com/bryanwb/tryme/", "keywords": "", "license": "BSD-New", "maintainer": "", "maintainer_email": "", "name": "tryme", "package_url": "https://pypi.org/project/tryme/", "platform": "", "project_url": "https://pypi.org/project/tryme/", "project_urls": { "Download": "https://github.com/bryanwb/tryme/archive/0.0.5.zip", "Homepage": "https://github.com/bryanwb/tryme/" }, "release_url": "https://pypi.org/project/tryme/0.0.5/", "requires_dist": null, "requires_python": "", "summary": "tryme - error handling for humans", "version": "0.0.5" }, "last_serial": 3199086, "releases": { "0.0.1": [ { "comment_text": "", "digests": { "md5": "29122c7bcf6309da08b3540629ad21d1", "sha256": "d9d9ef8aa702c06f5c5021560651ad86847f539f0aeaad9a863c9960790b6d24" }, "downloads": -1, "filename": "tryme-0.0.1.tar.gz", "has_sig": false, "md5_digest": "29122c7bcf6309da08b3540629ad21d1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 11032, "upload_time": "2017-09-20T05:11:53", "url": "https://files.pythonhosted.org/packages/a3/f0/c1571f629175e24b224bead692ee9ea690276da98727b42953f8ebe26b8e/tryme-0.0.1.tar.gz" } ], "0.0.2": [ { "comment_text": "", "digests": { "md5": "8abd689009c1a7740a19c6534c41b6c1", "sha256": "3c6b6a1fd9372c80992b0eb9b7456e37ceab7ac614f81f1b6b36913920b7813d" }, "downloads": -1, "filename": "tryme-0.0.2.tar.gz", "has_sig": false, "md5_digest": "8abd689009c1a7740a19c6534c41b6c1", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 18517, "upload_time": "2017-09-21T06:40:04", "url": "https://files.pythonhosted.org/packages/d4/e9/790458abd9b30edd42e1eb4b6cc7fdc3b8edaa09ef6699771d7da9f1c714/tryme-0.0.2.tar.gz" } ], "0.0.3": [ { "comment_text": "", "digests": { "md5": "47d8479edd52c338bdd2d960c7f6517e", "sha256": "877dd38c908fe2ebf73f1bb606237c902b10b093f3d7f7a1fa1435905963564d" }, "downloads": -1, "filename": "tryme-0.0.3.tar.gz", "has_sig": false, "md5_digest": "47d8479edd52c338bdd2d960c7f6517e", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19309, "upload_time": "2017-09-22T05:13:02", "url": "https://files.pythonhosted.org/packages/ea/e5/7287f47b3b8b05f4e36ef9caa28ca3b9b3f609d7c8b963643017c24cb1c5/tryme-0.0.3.tar.gz" } ], "0.0.4": [ { "comment_text": "", "digests": { "md5": "0fb647df21d11689b4ec076f7a321532", "sha256": "45fcf68d7aa5b07de46e964f533ee9c88a8114aeb123404981341fcc65bbc992" }, "downloads": -1, "filename": "tryme-0.0.4.tar.gz", "has_sig": false, "md5_digest": "0fb647df21d11689b4ec076f7a321532", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19728, "upload_time": "2017-09-23T23:40:48", "url": "https://files.pythonhosted.org/packages/77/60/7a1e4fa00d359d8e59c697d7dfadc697287ffb407593333d128ab55fa800/tryme-0.0.4.tar.gz" } ], "0.0.5": [ { "comment_text": "", "digests": { "md5": "1e6325691b2278684808638a4f9b12c6", "sha256": "3d0775c2e45e6446cfdc9cadf434b161f1495932badbb7c1eb5bf1bf7661a7b5" }, "downloads": -1, "filename": "tryme-0.0.5.tar.gz", "has_sig": false, "md5_digest": "1e6325691b2278684808638a4f9b12c6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19737, "upload_time": "2017-09-24T20:38:49", "url": "https://files.pythonhosted.org/packages/db/80/82bd6aacf3871fb36045cb904f84389dc80ebf05a3699729f75e84aac971/tryme-0.0.5.tar.gz" } ] }, "urls": [ { "comment_text": "", "digests": { "md5": "1e6325691b2278684808638a4f9b12c6", "sha256": "3d0775c2e45e6446cfdc9cadf434b161f1495932badbb7c1eb5bf1bf7661a7b5" }, "downloads": -1, "filename": "tryme-0.0.5.tar.gz", "has_sig": false, "md5_digest": "1e6325691b2278684808638a4f9b12c6", "packagetype": "sdist", "python_version": "source", "requires_python": null, "size": 19737, "upload_time": "2017-09-24T20:38:49", "url": "https://files.pythonhosted.org/packages/db/80/82bd6aacf3871fb36045cb904f84389dc80ebf05a3699729f75e84aac971/tryme-0.0.5.tar.gz" } ] }