pax_global_header00006660000000000000000000000064136354775670014541gustar00rootroot0000000000000052 comment=af817eb7665c3b12f8e6da2745e6362e8f90f536 pyramid_retry-2.1.1/000077500000000000000000000000001363547756700144345ustar00rootroot00000000000000pyramid_retry-2.1.1/.coveragerc000066400000000000000000000002611363547756700165540ustar00rootroot00000000000000[run] parallel = true source = pyramid_retry tests [paths] source = src/pyramid_retry */site-packages/pyramid_retry [report] show_missing = true precision = 2 pyramid_retry-2.1.1/.gitignore000066400000000000000000000013511363547756700164240ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python env/ env*/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ .idea cover pyramid_retry-2.1.1/.travis.yml000066400000000000000000000011511363547756700165430ustar00rootroot00000000000000sudo: false language: python matrix: include: - python: 3.7 env: TOXENV=py37 dist: xenial sudo: true - python: '3.6' env: TOXENV=py36 - python: '3.5' env: TOXENV=py35 - python: '3.4' env: TOXENV=py34 - python: '2.7' env: TOXENV=py27 - python: 'pypy' env: TOXENV=pypy - python: 'pypy3' env: TOXENV=pypy3 - python: '3.6' env: TOXENV=py27,py36,coverage - python: '3.5' env: TOXENV=docs - python: '3.6' env: TOXENV=lint install: pip install tox script: tox cache: directories: - $HOME/.cache/pip pyramid_retry-2.1.1/CHANGES.rst000066400000000000000000000051641363547756700162440ustar00rootroot000000000000002.1.1 (2020-03-21) ================== - Ensure the threadlocals are properly popped if the ``activate_hook`` throws an error or the request body fails to read due to a client disconnect. See https://github.com/Pylons/pyramid_retry/pull/20 2.1 (2019-09-30) ================ - Add ``exception`` and ``response`` attributes to the ``pyramid_retry.IBeforeRetry`` event. See https://github.com/Pylons/pyramid_retry/pull/19 2.0 (2019-06-06) ================ - No longer call ``invoke_exception_view`` if the policy catches an exception. If on the last attempt or non-retryable then the exception will now bubble out of the app and into WSGI middleware. See https://github.com/Pylons/pyramid_retry/pull/17 1.0 (2018-10-18) ================ - Support Python 3.7. - Update the version we require for Pyramid to a non-prerelease so that pip and other tools don't accidentally install pre-release software. See https://github.com/Pylons/pyramid_retry/pull/13 0.5 (2017-06-19) ================ - Update the policy to to track changes in Pyramid 1.9b1. This is an incompatible change and requires at least Pyramid 1.9b1. See https://github.com/Pylons/pyramid_retry/pull/11 0.4 (2017-06-12) ================ - Add the ``mark_error_retryable`` function in order to easily mark certain errors as retryable for ``pyramid_retry`` to detect. See https://github.com/Pylons/pyramid_retry/pull/8 - Add the ``IBeforeRetry`` event that can be subscribed to be notified when a retry is about to occur in order to perform cleanup on the ``environ``. See https://github.com/Pylons/pyramid_retry/pull/9 0.3 (2017-04-10) ================ - Support a ``retry.activate_hook`` setting which can return a per-request number of retries. See https://github.com/Pylons/pyramid_retry/pull/4 - Configuration is deferred so that settings may be changed after ``config.include('pyramid_retry')`` is invoked until the configurator is committed. See https://github.com/Pylons/pyramid_retry/pull/4 - Rename the view predicates to ``last_retry_attempt`` and ``retryable_error``. See https://github.com/Pylons/pyramid_retry/pull/3 - Rename ``pyramid_retry.is_exc_retryable`` to ``pyramid_retry.is_error_retryable``. See https://github.com/Pylons/pyramid_retry/pull/3 0.2 (2017-03-02) ================ - Change the default attempts to 3 instead of 1. - Rename the view predicates to ``is_last_attempt`` and ``is_exc_retryable``. - Drop support for the ``tm.attempts`` setting. - The ``retry.attempts`` setting is always set now in ``registry.settings['retry.attempts']`` so that apps can inspect it. 0.1 (2017-03-01) ================ - Initial release. pyramid_retry-2.1.1/CONTRIBUTING.rst000066400000000000000000000060431363547756700171000ustar00rootroot00000000000000.. highlight:: shell ============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. You can contribute in many ways: Types of Contributions ---------------------- Report Bugs ~~~~~~~~~~~ Report bugs at https://github.com/Pylons/pyramid_retry/issues. If you are reporting a bug, please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Fix Bugs ~~~~~~~~ Look through the GitHub issues for bugs. Anything tagged with "bug" is open to whoever wants to implement it. Implement Features ~~~~~~~~~~~~~~~~~~ Look through the GitHub issues for features. Anything tagged with "feature" is open to whoever wants to implement it. Write Documentation ~~~~~~~~~~~~~~~~~~~ pyramid_retry could always use more documentation, whether as part of the official pyramid_retry docs, in docstrings, or even on the web in blog posts, articles, and such. Submit Feedback ~~~~~~~~~~~~~~~ The best way to send feedback is to file an issue at https://github.com/Pylons/pyramid_retry/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that contributions are welcome :) Get Started! ------------ Ready to contribute? Here's how to set up `pyramid_retry` for local development. 1. Fork the `pyramid_retry` repo on GitHub. 2. Clone your fork locally:: $ git clone git@github.com:your_name_here/pyramid_retry.git 3. Install your local copy into a virtualenv:: $ python3 -m venv env $ env/bin/pip install -e .[docs,testing] $ env/bin/pip install tox 4. Create a branch for local development:: $ git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: $ env/bin/tox 6. Add your name to the ``CONTRIBUTORS.txt`` file in the root of the repository. 7. Commit your changes and push your branch to GitHub:: $ git add . $ git commit -m "Your detailed description of your changes." $ git push origin name-of-your-bugfix-or-feature 8. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- Before you submit a pull request, check that it meets these guidelines: 1. The pull request should include tests. 2. If the pull request adds functionality, the docs should be updated. Put your new functionality into a function with a docstring, and add the feature to the list in README.rst. 3. The pull request should work for Python 2.7, 3.4, 3.5, 3.6, and 3.7, and for PyPy. Check https://travis-ci.org/Pylons/pyramid_retry/pull_requests and make sure that the tests pass for all supported Python versions. Tips ---- To run a subset of tests:: $ env/bin/py.test tests.test_it pyramid_retry-2.1.1/CONTRIBUTORS.txt000066400000000000000000000116531363547756700171400ustar00rootroot00000000000000Pylons Project Contributor Agreement ==================================== The submitter agrees by adding his or her name within the section below named "Contributors" and submitting the resulting modified document to the canonical shared repository location for this software project (whether directly, as a user with "direct commit access", or via a "pull request"), he or she is signing a contract electronically. The submitter becomes a Contributor after a) he or she signs this document by adding their name beneath the "Contributors" section below, and b) the resulting document is accepted into the canonical version control repository. Treatment of Account -------------------- Contributor will not allow anyone other than the Contributor to use his or her username or source repository login to submit code to a Pylons Project source repository. Should Contributor become aware of any such use, Contributor will immediately notify Agendaless Consulting. Notification must be performed by sending an email to webmaster@agendaless.com. Until such notice is received, Contributor will be presumed to have taken all actions made through Contributor's account. If the Contributor has direct commit access, Agendaless Consulting will have complete control and discretion over capabilities assigned to Contributor's account, and may disable Contributor's account for any reason at any time. Legal Effect of Contribution ---------------------------- Upon submitting a change or new work to a Pylons Project source Repository (a "Contribution"), you agree to assign, and hereby do assign, a one-half interest of all right, title and interest in and to copyright and other intellectual property rights with respect to your new and original portions of the Contribution to Agendaless Consulting. You and Agendaless Consulting each agree that the other shall be free to exercise any and all exclusive rights in and to the Contribution, without accounting to one another, including without limitation, the right to license the Contribution to others under the MIT License. This agreement shall run with title to the Contribution. Agendaless Consulting does not convey to you any right, title or interest in or to the Program or such portions of the Contribution that were taken from the Program. Your transmission of a submission to the Pylons Project source Repository and marks of identification concerning the Contribution itself constitute your intent to contribute and your assignment of the work in accordance with the provisions of this Agreement. License Terms ------------- Code committed to the Pylons Project source repository (Committed Code) must be governed by the MIT License or another license acceptable to Agendaless Consulting. Until Agendaless Consulting declares in writing an acceptable license other than the MIT License, only the MIT License shall be used. A list of exceptions is detailed within the "Licensing Exceptions" section of this document, if one exists. Representations, Warranty, and Indemnification ---------------------------------------------- Contributor represents and warrants that the Committed Code does not violate the rights of any person or entity, and that the Contributor has legal authority to enter into this Agreement and legal authority over Contributed Code. Further, Contributor indemnifies Agendaless Consulting against violations. Cryptography ------------ Contributor understands that cryptographic code may be subject to government regulations with which Agendaless Consulting and/or entities using Committed Code must comply. Any code which contains any of the items listed below must not be checked-in until Agendaless Consulting staff has been notified and has approved such contribution in writing. - Cryptographic capabilities or features - Calls to cryptographic features - User interface elements which provide context relating to cryptography - Code which may, under casual inspection, appear to be cryptographic. Notices ------- Contributor confirms that any notices required will be included in any Committed Code. Licensing Exceptions ==================== Code committed within the ``docs/`` subdirectory of the pyramid_retry source control repository and "docstrings" which appear in the documentation generated by running "make" within this directory are licensed under the Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License (http://creativecommons.org/licenses/by-nc-sa/3.0/us/). List of Contributors ==================== The below-signed are contributors to a code repository that is part of the project named "pyramid_retry". Each below-signed contributor has read, understand and agrees to the terms above in the section within this document entitled "Pylons Project Contributor Agreement" as of the date beside his or her name. Contributors ------------ - Michael Merickel (2017-03-01) - Steve Piercy (2017-06-10) - Bert JW Regeer (2018-10-17) - Sean Hammond (2019-09-27) - Keenan Graham (2020-03-20) pyramid_retry-2.1.1/LICENSE.txt000066400000000000000000000020511363547756700162550ustar00rootroot00000000000000Copyright (c) 2017-2018 Michael Merickel Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyramid_retry-2.1.1/MANIFEST.in000066400000000000000000000004001363547756700161640ustar00rootroot00000000000000graft src graft tests graft docs include README.rst include CHANGES.rst include LICENSE.txt include CONTRIBUTING.rst include CONTRIBUTORS.txt include .coveragerc include tox.ini appveyor.yml .travis.yml rtd.txt recursive-exclude * __pycache__ *.py[cod] pyramid_retry-2.1.1/README.rst000066400000000000000000000014711363547756700161260ustar00rootroot00000000000000============= pyramid_retry ============= .. image:: https://img.shields.io/pypi/v/pyramid_retry.svg :target: https://pypi.python.org/pypi/pyramid_retry .. image:: https://img.shields.io/travis/Pylons/pyramid_retry/master.svg :target: https://travis-ci.org/Pylons/pyramid_retry .. image:: https://readthedocs.org/projects/pyramid_retry/badge/?version=latest :target: https://readthedocs.org/projects/pyramid_retry/?badge=latest :alt: Documentation Status ``pyramid_retry`` is an execution policy for Pyramid that wraps requests and can retry them a configurable number of times under certain "retryable" error conditions before indicating a failure to the client. See https://docs.pylonsproject.org/projects/pyramid-retry/en/latest/ or ``docs/index.rst`` in this distribution for detailed documentation. pyramid_retry-2.1.1/appveyor.yml000066400000000000000000000011071363547756700170230ustar00rootroot00000000000000environment: matrix: - PYTHON: "C:\\Python36" TOXENV: "py36" - PYTHON: "C:\\Python35" TOXENV: "py35" - PYTHON: "C:\\Python27" TOXENV: "py27" - PYTHON: "C:\\Python36-x64" TOXENV: "py36" - PYTHON: "C:\\Python35-x64" TOXENV: "py35" - PYTHON: "C:\\Python27-x64" TOXENV: "py27" cache: - '%LOCALAPPDATA%\pip\Cache' version: '{branch}.{build}' install: - "%PYTHON%\\python.exe -m pip install -U pip" - "%PYTHON%\\python.exe -m pip install -U tox virtualenv" build: off test_script: - "%PYTHON%\\Scripts\\tox.exe" pyramid_retry-2.1.1/docs/000077500000000000000000000000001363547756700153645ustar00rootroot00000000000000pyramid_retry-2.1.1/docs/Makefile000066400000000000000000000151521363547756700170300ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/hupper.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/hupper.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/hupper" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/hupper" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." pyramid_retry-2.1.1/docs/_static/000077500000000000000000000000001363547756700170125ustar00rootroot00000000000000pyramid_retry-2.1.1/docs/_static/.keep000066400000000000000000000000001363547756700177250ustar00rootroot00000000000000pyramid_retry-2.1.1/docs/api.rst000066400000000000000000000010551363547756700166700ustar00rootroot00000000000000======================== :mod:`pyramid_retry` API ======================== .. automodule:: pyramid_retry .. autofunction:: includeme .. autofunction:: RetryableExecutionPolicy .. autofunction:: mark_error_retryable .. autofunction:: is_error_retryable .. autofunction:: is_last_attempt .. autoclass:: LastAttemptPredicate :members: .. autoclass:: RetryableErrorPredicate :members: .. autoexception:: RetryableException :members: .. autointerface:: IRetryableError .. autointerface:: IBeforeRetry :members: pyramid_retry-2.1.1/docs/changes.rst000066400000000000000000000000651363547756700175270ustar00rootroot00000000000000======= Changes ======= .. include:: ../CHANGES.rst pyramid_retry-2.1.1/docs/conf.py000066400000000000000000000214351363547756700166700ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # pyramid_retry documentation build configuration file, created by # sphinx-quickstart on Tue Jul 9 22:26:36 2013. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import datetime import sys import os import pkg_resources # If extensions (or modules to document with autodoc) are in another # directory, add these directories to sys.path here. If the directory is # relative to the documentation root, use os.path.abspath to make it # absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # Get the project root dir, which is the parent dir of this cwd = os.getcwd() project_root = os.path.dirname(cwd) # Insert the project root dir as the first element in the PYTHONPATH. # This lets us ensure that the source package is imported, and that its # version is used. sys.path.insert(0, project_root) # ensure the code is importable for use with autodoc import pyramid_retry # -- General configuration --------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'repoze.sphinx.autointerface', 'sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'pyramid_retry' thisyear = datetime.datetime.now().year copyright = u'2017-%s, Michael Merickel' % thisyear # The version info for the project you're documenting, acts as replacement # for |version| and |release|, also used in various other places throughout # the built documents. # # The short X.Y version. version = pkg_resources.get_distribution('pyramid_retry').version # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to # some non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = ['_build'] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built # documents. #keep_warnings = False # Do not use smart quotes. smartquotes = False # -- Options for HTML output ------------------------------------------- # Add and use Pyramid theme sys.path.append(os.path.abspath('_themes')) import pylons_sphinx_themes html_theme_path = pylons_sphinx_themes.get_html_themes_path() html_theme = 'pyramid' html_theme_options = { 'github_url': 'https://github.com/Pylons/pyramid_retry' } # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as # html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the # top of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon # of the docs. This file should be a Windows icon file (.ico) being # 16x16 or 32x32 pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) # here, relative to this directory. They are copied after the builtin # static files, so a file named "default.css" will overwrite the builtin # "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # Control display of sidebars and include ethical ads from RTD html_sidebars = {'**': [ 'localtoc.html', 'ethicalads.html', 'relations.html', 'sourcelink.html', 'searchbox.html', ]} # Additional templates that should be rendered to pages, maps page names # to template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. # Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. # Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages # will contain a tag referring to it. The value of this option # must be the base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'pyramid_retrydoc' # -- Options for LaTeX output ------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'pyramid_retry.tex', u'pyramid_retry Documentation', u'Michael Merickel', 'manual'), ] # The name of an image file (relative to this directory) to place at # the top of the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings # are parts, not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output ------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'pyramid_retry', u'pyramid_retry Documentation', [u'Michael Merickel'], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ---------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'pyramid_retry', u'pyramid_retry Documentation', u'Michael Merickel', 'pyramid_retry', 'pyramid_retry is an execution policy for Pyramid that wraps requests and ' 'can retry them a configurable number of times under certain "retryable" ' 'error conditions before indicating a failure to the client.', 'Web application'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False pyramid_retry-2.1.1/docs/contributing.rst000066400000000000000000000000411363547756700206200ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst pyramid_retry-2.1.1/docs/glossary.rst000066400000000000000000000017261363547756700177670ustar00rootroot00000000000000.. _glossary: Glossary ======== .. glossary:: :sorted: Pyramid A `web framework `_. retryable error An exception indicating that a request failed due to a transient error which may succeed if tried again. Examples might include lock contention or a flaky network connection to a third party service. A retryable error is usually an exception that inherits from :class:`pyramid_retry.RetryableException` but may also be any other exception that implements the :class:`pyramid_retry.IRetryableError` interface. execution policy A hook in :term:`Pyramid` which can control the entire request lifecycle. view predicate A predicate in :term:`Pyramid` which can help determine which view should be executed for a given request. Many views may be registered for a similar url, query strings etc and all predicates must pass in order for the view to be considered. pyramid_retry-2.1.1/docs/index.rst000066400000000000000000000153601363547756700172320ustar00rootroot00000000000000============= pyramid_retry ============= ``pyramid_retry`` is an execution policy for Pyramid that wraps requests and can retry them a configurable number of times under certain "retryable" error conditions before indicating a failure to the client. .. warning:: This package will only work with Pyramid 1.9 and newer. Installation ============ Stable release -------------- To install pyramid_retry, run this command in your terminal: .. code-block:: console $ pip install pyramid_retry If you don't have `pip`_ installed, this `Python installation guide`_ can guide you through the process. .. _pip: https://pip.pypa.io/en/stable/ .. _Python installation guide: http://docs.python-guide.org/en/latest/starting/installation/ From sources ------------ The sources for pyramid_retry can be downloaded from the `Github repo`_. .. code-block:: console $ git clone https://github.com/Pylons/pyramid_retry.git Once you have a copy of the source, you can install it with: .. code-block:: console $ pip install -e . .. _Github repo: https://github.com/Pylons/pyramid_retry Usage ===== Activate ``pyramid_retry`` by including it in your application: .. code-block:: python def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_retry') # ... config.add_route('home', '/') By default ``pyramid_retry`` will register an instance of :func:`pyramid_retry.RetryableExecutionPolicy` as an :term:`execution policy` in your application using the ``retry.attempts`` setting as the maximum number of attempts per request. The default number of attempts is ``3``. This number is configurable in your application's ``.ini`` file as follows: .. code-block:: ini [app:main] # ... retry.attempts = 3 The policy will handle any requests that fail because the application raised an instance of :class:`pyramid_retry.RetryableException` or another exception implementing the :class:`pyramid_retry.IRetryableError` interface. The below, very contrived example, shows conceptually what's going on when a request is retried. The ``failing_view`` is executed initially and for the final attempt the ``recovery_view`` is executed. .. code-block:: python @view_config(route_name='home') def failing_view(request): raise RetryableException @view_config(route_name='home', is_last_attempt=True, renderer='string') def recovery_view(request): return 'success' Of course you probably wouldn't write actual code that expects to fail like this. More realistically you may use a library like pyramid_tm_ to translate certain transactional errors marked as "transient" into retryable errors. .. _pyramid_tm: https://docs.pylonsproject.org/projects/pyramid-tm/en/latest/ Custom Retryable Errors ----------------------- The simple approach to marking errors as retryable is to simply catch the error and raise a :class:`pyramid_retry.RetryableException` instead: .. code-block:: python from pyramid_retry import RetryableException import requests def view(request): try: response = requests.get('https://www.google.com') except requests.Timeout: raise RetryableException This will work but if this is the last attempt then the failed request will not actually be retried and on top of that the original exception is lost. A better approach is to preserve the original exception and simply mark it as retryable using the :class:`pyramid_retry.IRetryableError` marker interface: .. code-block:: python from pyramid_retry import mark_error_retryable import requests import zope.interface # mark requests.Timeout errors as retryable mark_error_retryable(requests.Timeout) def view(request): response = requests.get('https://www.google.com') Per-Request Attempts -------------------- It may be desirable to override the attempts per-request. For example, if one endpoint on the system cannot afford to make a copy of the request via ``request.make_body_seekable()`` then the activate hook can be used to set ``attempts=`` on that endpoint. .. code-block:: python def activate_hook(request): if request.path == '/upload': return 1 # disable retries on this endpoint config.add_settings({'retry.activate_hook': activate_hook}) The ``activate_hook`` should return a number ``>= 1`` or ``None``. If ``None`` then the policy will fallback to the ``retry.attempts`` setting. View Predicates --------------- When the library is included in your application it registers two new view predicates which are especially useful on exception views to determine when to handle certain errors. ``retryable_error=[True/False]`` will match the exception view only if the exception is both an :term:`retryable error` **and** there are remaining attempts in which the request would be retried. See :class:`pyramid_retry.RetryableErrorPredicate` for more information. ``last_retry_attempt=[True/False]`` will match only if, when the view is executed, there will not be another attempt for this request. See :class:`pyramid_retry.LastAttemptPredicate` for more information. Receiving Retry Notifications ----------------------------- The :class:`pyramid_retry.IBeforeRetry` event can be subscribed to receive a callback with the ``request`` and ``environ`` prior to the pipeline being completely torn down. This can be very helpful if any state is stored on the ``environ`` itself that needs to be reset prior to the retry attempt. .. code-block:: python from pyramid.events import subscriber from pyramid_retry import IBeforeRetry @subscriber(IBeforeRetry) def retry_event(event): print(f'A retry is about to occur due to {event.exception}.') The ``exception`` attribute indicates the exception that triggered the retry. The exception may come from either ``request.exception`` if it was caught and a response was rendered, or it may come from an uncaught exception. Caveats ======= - In order to guarantee that a request can be retried it must make the body seekable. This is done via ``request.make_body_seekable()``. Generally the body is loaded directly from ``environ['wsgi.input']`` which is controlled by the WSGI server. However to make the body seekable it is copied into a seekable wrapper. In some cases this can lead to a very large copy operation before the request is executed. - ``pyramid_retry`` does not copy the ``environ`` or make any attempt to restore it to its original state before retrying a request. This means anything stored on the ``environ`` will persist across requests created for that ``environ``. More Information ================ .. toctree:: :maxdepth: 1 api glossary contributing changes Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` pyramid_retry-2.1.1/docs/make.bat000066400000000000000000000144731363547756700170020ustar00rootroot00000000000000@ECHO OFF REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) set BUILDDIR=_build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . set I18NSPHINXOPTS=%SPHINXOPTS% . if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. singlehtml to make a single large HTML file echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. devhelp to make HTML files and a Devhelp project echo. epub to make an epub echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. text to make text files echo. man to make manual pages echo. texinfo to make Texinfo files echo. gettext to make PO message catalogs echo. changes to make an overview over all changed/added/deprecated items echo. xml to make Docutils-native XML files echo. pseudoxml to make pseudoxml-XML files for display purposes echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) %SPHINXBUILD% 2> nul if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx echo.installed, then set the SPHINXBUILD environment variable to point echo.to the full path of the 'sphinx-build' executable. Alternatively you echo.may add the Sphinx directory to PATH. echo. echo.If you don't have Sphinx installed, grab it from echo.http://sphinx-doc.org/ exit /b 1 ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "singlehtml" ( %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml if errorlevel 1 exit /b 1 echo. echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp if errorlevel 1 exit /b 1 echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\hupper.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\hupper.ghc goto end ) if "%1" == "devhelp" ( %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp if errorlevel 1 exit /b 1 echo. echo.Build finished. goto end ) if "%1" == "epub" ( %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub if errorlevel 1 exit /b 1 echo. echo.Build finished. The epub file is in %BUILDDIR%/epub. goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex if errorlevel 1 exit /b 1 echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdf" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "latexpdfja" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex cd %BUILDDIR%/latex make all-pdf-ja cd %BUILDDIR%/.. echo. echo.Build finished; the PDF files are in %BUILDDIR%/latex. goto end ) if "%1" == "text" ( %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text if errorlevel 1 exit /b 1 echo. echo.Build finished. The text files are in %BUILDDIR%/text. goto end ) if "%1" == "man" ( %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man if errorlevel 1 exit /b 1 echo. echo.Build finished. The manual pages are in %BUILDDIR%/man. goto end ) if "%1" == "texinfo" ( %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo if errorlevel 1 exit /b 1 echo. echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. goto end ) if "%1" == "gettext" ( %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale if errorlevel 1 exit /b 1 echo. echo.Build finished. The message catalogs are in %BUILDDIR%/locale. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes if errorlevel 1 exit /b 1 echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck if errorlevel 1 exit /b 1 echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest if errorlevel 1 exit /b 1 echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) if "%1" == "xml" ( %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml if errorlevel 1 exit /b 1 echo. echo.Build finished. The XML files are in %BUILDDIR%/xml. goto end ) if "%1" == "pseudoxml" ( %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml if errorlevel 1 exit /b 1 echo. echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. goto end ) :end pyramid_retry-2.1.1/rtd.txt000066400000000000000000000000131363547756700157600ustar00rootroot00000000000000-e .[docs] pyramid_retry-2.1.1/setup.cfg000066400000000000000000000004661363547756700162630ustar00rootroot00000000000000[wheel] universal = 1 [metadata] license_file = LICENSE.txt [flake8] show-source = True max-line-length = 80 [check-manifest] ignore = .gitignore PKG-INFO *.egg-info *.egg-info/* ignore-default-rules = true [tool:pytest] python_files = test_*.py testpaths = src/pyramid_retry tests pyramid_retry-2.1.1/setup.py000066400000000000000000000034461363547756700161550ustar00rootroot00000000000000from setuptools import setup, find_packages def readfile(name): with open(name) as f: return f.read() readme = readfile('README.rst') changes = readfile('CHANGES.rst') install_requires = [ 'pyramid >= 1.9', 'zope.interface', ] docs_require = [ 'Sphinx', 'pylons-sphinx-themes', 'repoze.sphinx.autointerface', ] tests_require = [ 'pytest', 'pytest-cov', 'WebTest', ] setup( name='pyramid_retry', version='2.1.1', description=( 'An execution policy for Pyramid that supports retrying requests ' 'after certain failure exceptions.' ), long_description=readme + '\n\n' + changes, author='Michael Merickel', author_email='pylons-discuss@googlegroups.com', url='https://github.com/Pylons/pyramid_retry', packages=find_packages('src', exclude=['tests']), package_dir={'': 'src'}, include_package_data=True, install_requires=install_requires, extras_require={ 'docs': docs_require, 'testing': tests_require, }, zip_safe=False, keywords='pyramid wsgi retry attempt', classifiers=[ 'Development Status :: 5 - Production/Stable', 'Framework :: Pyramid', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', ], ) pyramid_retry-2.1.1/src/000077500000000000000000000000001363547756700152235ustar00rootroot00000000000000pyramid_retry-2.1.1/src/pyramid_retry/000077500000000000000000000000001363547756700201155ustar00rootroot00000000000000pyramid_retry-2.1.1/src/pyramid_retry/__init__.py000066400000000000000000000241001363547756700222230ustar00rootroot00000000000000import inspect from pyramid.config import PHASE1_CONFIG from pyramid.exceptions import ConfigurationError from zope.interface import ( Attribute, Interface, alsoProvides, classImplements, implementer, ) class IRetryableError(Interface): """ A marker interface for retryable errors. An interface can be applied to any ``Exception`` class or object to indicate that it should be treated as a :term:`retryable error`. """ class IBeforeRetry(Interface): """ An event emitted immediately prior to throwing away the request and creating a new one. This event may be useful when state is stored on the ``request.environ`` that needs to be updated before a new request is created. """ environ = Attribute('The environ object that is reused between requests.') request = Attribute('The request object that is being discarded.') exception = Attribute('The exception that request processing raised.') response = Attribute('The response object that is being discarded. ' 'This may be ``None`` if no response was generated, ' 'which happens when request processing raises an ' "exception that isn't caught by any exception view.") @implementer(IBeforeRetry) class BeforeRetry(object): """ An event emitted immediately prior to throwing away the request and creating a new one. This event may be useful when state is stored on the ``request.environ`` that needs to be updated before a new request is created. :ivar request: The :class:`pyramid.request.Request` object that is being discarded. """ def __init__(self, request, exception, response=None): self.request = request self.environ = request.environ self.exception = exception self.response = response @implementer(IRetryableError) class RetryableException(Exception): """ A retryable exception should be raised when an error occurs.""" def RetryableExecutionPolicy(attempts=3, activate_hook=None): """ Create a :term:`execution policy` that catches any :term:`retryable error` and sends it through the pipeline again up to a maximum of ``attempts`` attempts. If ``activate_hook`` is set it will be consulted prior to each request to determine if retries should be enabled. It should return a number > 0 of attempts to be used or ``None`` which will indicate to use the default number of attempts. """ assert attempts > 0 def retry_policy(environ, router): # make the original request request_ctx = router.request_context(environ) request = request_ctx.begin() try: if activate_hook: retry_attempts = activate_hook(request) if retry_attempts is None: retry_attempts = attempts else: assert retry_attempts > 0 else: retry_attempts = attempts # if we are supporting multiple attempts then we must make # make the body seekable in order to re-use it across multiple # attempts. make_body_seekable will copy wsgi.input if # necessary, otherwise it will rewind the copy to position zero if retry_attempts != 1: request.make_body_seekable() # Catch make_body_seekable (e.g. 408 RequestTimeout) # and activate_hook exceptions and clean up. except BaseException: request_ctx.end() raise for number in range(retry_attempts): # track the attempt info in the environ # try to set it as soon as possible so that it's available # in the request factory and elsewhere if people want it # note: set all of these values here as they are cleared after # each attempt environ['retry.attempt'] = number environ['retry.attempts'] = retry_attempts # if we are not on the first attempt then we should start # with a new request object and throw away any changes to # the old object, however we do this carefully to try and # avoid extra copies of the body if number > 0: # try to make sure this code stays in sync with pyramid's # router which normally creates requests request_ctx = router.request_context(environ) request = request_ctx.begin() try: response = router.invoke_request(request) # check for a squashed exception and handle it # this would happen if an exception view was invoked and # rendered an error response exc = getattr(request, 'exception', None) if exc is not None: # if this is a retryable exception then continue to the # next attempt, discarding the current response if is_error_retryable(request, exc): request.registry.notify( BeforeRetry(request, exc, response=response)) continue return response except Exception as exc: # if this was the last attempt or the exception is not # retryable then there's nothing left for us to do if not is_error_retryable(request, exc): raise else: request.registry.notify(BeforeRetry(request, exc)) # cleanup any changes we made to the request finally: request_ctx.end() del environ['retry.attempt'] del environ['retry.attempts'] return retry_policy def mark_error_retryable(error): """ Mark an exception instance or type as retryable. If this exception is caught by ``pyramid_retry`` then it may retry the request. """ if isinstance(error, Exception): alsoProvides(error, IRetryableError) elif inspect.isclass(error) and issubclass(error, Exception): classImplements(error, IRetryableError) else: raise ValueError( 'only exception objects or types may be marked retryable') def is_error_retryable(request, exc): """ Return ``True`` if the exception is recognized as :term:`retryable error`. This will return ``False`` if the request is on its last attempt. This will return ``False`` if ``pyramid_retry`` is inactive for the request. """ if is_last_attempt(request): return False return ( isinstance(exc, RetryableException) or IRetryableError.providedBy(exc) ) def is_last_attempt(request): """ Return ``True`` if the request is on its last attempt, meaning that ``pyramid_retry`` will not be issuing any new attempts, regardless of what happens when executing this request. This will return ``True`` if ``pyramid_retry`` is inactive for the request. """ environ = request.environ attempt = environ.get('retry.attempt') attempts = environ.get('retry.attempts') if attempt is None or attempts is None: return True return attempt + 1 == attempts class RetryableErrorPredicate(object): """ A :term:`view predicate` registered as ``retryable_error``. Can be used to determine if an exception view should execute based on whether the exception is a :term:`retryable error`. .. seealso:: See :func:`pyramid_retry.is_error_retryable`. """ def __init__(self, val, config): if not isinstance(val, bool): raise ConfigurationError( 'The "retryable_error" view predicate value must be ' 'True or False.', ) self.val = val def text(self): return 'retryable_error = %s' % (self.val,) phash = text def __call__(self, context, request): exc = getattr(request, 'exception', None) is_retryable = is_error_retryable(request, exc) return ( (self.val and is_retryable) or (not self.val and not is_retryable) ) class LastAttemptPredicate(object): """ A :term:`view predicate` registered as ``last_retry_attempt``. Can be used to determine if an exception view should execute based on whether it's the last retry attempt before aborting the request. .. seealso:: See :func:`pyramid_retry.is_last_attempt`. """ def __init__(self, val, config): if not isinstance(val, bool): raise ConfigurationError( 'The "last_retry_attempt" view predicate value must be ' 'True or False.', ) self.val = val def text(self): return 'last_retry_attempt = %s' % (self.val,) phash = text def __call__(self, context, request): is_last = is_last_attempt(request) return ((self.val and is_last) or (not self.val and not is_last)) def includeme(config): """ Activate the ``pyramid_retry`` execution policy in your application. This will add the :func:`pyramid_retry.RetryableErrorPolicy` with ``attempts`` pulled from the ``retry.attempts`` setting. The ``last_retry_attempt`` and ``retryable_error`` view predicates are registered. This should be included in your Pyramid application via ``config.include('pyramid_retry')``. """ settings = config.get_settings() config.add_view_predicate('last_retry_attempt', LastAttemptPredicate) config.add_view_predicate('retryable_error', RetryableErrorPredicate) def register(): attempts = int(settings.get('retry.attempts') or 3) settings['retry.attempts'] = attempts activate_hook = settings.get('retry.activate_hook') activate_hook = config.maybe_dotted(activate_hook) policy = RetryableExecutionPolicy( attempts, activate_hook=activate_hook, ) config.set_execution_policy(policy) # defer registration to allow time to modify settings config.action(None, register, order=PHASE1_CONFIG) pyramid_retry-2.1.1/tests/000077500000000000000000000000001363547756700155765ustar00rootroot00000000000000pyramid_retry-2.1.1/tests/__init__.py000066400000000000000000000000001363547756700176750ustar00rootroot00000000000000pyramid_retry-2.1.1/tests/conftest.py000066400000000000000000000004121363547756700177720ustar00rootroot00000000000000import pyramid.testing import pytest @pytest.yield_fixture def config(): config = pyramid.testing.setUp( settings={'retry.attempts': 3}, autocommit=False, ) config.include('pyramid_retry') yield config pyramid.testing.tearDown() pyramid_retry-2.1.1/tests/test_it.py000066400000000000000000000235401363547756700176270ustar00rootroot00000000000000import pyramid.request import pyramid.response import pyramid.testing import pytest import webtest def test_raising_RetryableException_is_caught(config): from pyramid_retry import RetryableException calls = [] def final_view(request): calls.append('ok') return 'ok' def bad_view(request): calls.append('fail') raise RetryableException config.add_view(bad_view, last_retry_attempt=False) config.add_view(final_view, last_retry_attempt=True, renderer='string') app = config.make_wsgi_app() app = webtest.TestApp(app) response = app.get('/') assert response.body == b'ok' assert calls == ['fail', 'fail', 'ok'] def test_raising_IRetryableError_instance_is_caught(config): from pyramid_retry import mark_error_retryable calls = [] def final_view(request): calls.append('ok') return 'ok' def bad_view(request): calls.append('fail') ex = Exception() mark_error_retryable(ex) raise ex config.add_view(bad_view, last_retry_attempt=False) config.add_view(final_view, last_retry_attempt=True, renderer='string') app = config.make_wsgi_app() app = webtest.TestApp(app) response = app.get('/') assert response.body == b'ok' assert calls == ['fail', 'fail', 'ok'] def test_raising_IRetryableError_type_is_caught(config): from pyramid_retry import mark_error_retryable class MyRetryableError(Exception): pass mark_error_retryable(MyRetryableError) calls = [] def final_view(request): calls.append('ok') return 'ok' def bad_view(request): calls.append('fail') raise MyRetryableError config.add_view(bad_view, last_retry_attempt=False) config.add_view(final_view, last_retry_attempt=True, renderer='string') app = config.make_wsgi_app() app = webtest.TestApp(app) response = app.get('/') assert response.body == b'ok' assert calls == ['fail', 'fail', 'ok'] def test_raising_nonretryable_is_not_caught(config): calls = [] def bad_view(request): calls.append('fail') raise Exception config.add_view(bad_view) app = config.make_wsgi_app() app = webtest.TestApp(app) with pytest.raises(Exception): app.get('/') assert calls == ['fail'] def test_handled_error_is_retried(config): from pyramid_retry import RetryableException calls = [] def bad_view(request): calls.append('fail') raise RetryableException def retryable_exc_view(request): calls.append('caught') return 'caught' def default_exc_view(request): calls.append('default') return 'default' config.add_view(bad_view) config.add_exception_view(default_exc_view, renderer='string') config.add_exception_view( retryable_exc_view, retryable_error=True, renderer='string') app = config.make_wsgi_app() app = webtest.TestApp(app) response = app.get('/') assert response.body == b'default' assert calls == ['fail', 'caught', 'fail', 'caught', 'fail', 'default'] def test_retryable_exception_is_ignored_on_last_attempt(config): from pyramid_retry import RetryableException calls = [] def bad_view(request): calls.append('fail') raise RetryableException config.add_view(bad_view) app = config.make_wsgi_app() app = webtest.TestApp(app) with pytest.raises(Exception): app.get('/') assert calls == ['fail', 'fail', 'fail'] def test_BeforeRetry_event_is_raised(config): from pyramid_retry import RetryableException from pyramid_retry import IBeforeRetry calls = [] retries = [] first_exception = RetryableException() second_exception = RetryableException() exceptions_to_be_raised = [first_exception, second_exception] def retry_subscriber(event): retries.append(event) def final_view(request): calls.append('ok') return 'ok' def bad_view(request): calls.append('fail') raise exceptions_to_be_raised.pop(0) config.add_subscriber(retry_subscriber, IBeforeRetry) config.add_view(bad_view, last_retry_attempt=False) config.add_view(final_view, last_retry_attempt=True, renderer='string') app = config.make_wsgi_app() app = webtest.TestApp(app) response = app.get('/') assert response.body == b'ok' assert calls == ['fail', 'fail', 'ok'] assert len(retries) == 2 assert retries[0].exception == first_exception assert retries[0].response is None assert retries[1].exception == second_exception assert retries[1].response is None def test_BeforeRetry_event_is_raised_from_squashed_exception(config): from pyramid_retry import IBeforeRetry from pyramid_retry import RetryableException calls = [] retries = [] first_exception = RetryableException() second_exception = RetryableException() exceptions_to_be_raised = [first_exception, second_exception] def retry_subscriber(event): retries.append(event) def final_view(request): calls.append('ok') return 'ok' def bad_view(request): raise exceptions_to_be_raised.pop(0) def exc_view(request): calls.append('squash') return 'squash' config.add_subscriber(retry_subscriber, IBeforeRetry) config.add_view(bad_view, last_retry_attempt=False) config.add_view(exc_view, context=RetryableException, exception_only=True, renderer='string') config.add_view(final_view, last_retry_attempt=True, renderer='string') app = config.make_wsgi_app() app = webtest.TestApp(app) response = app.get('/') assert response.body == b'ok' assert calls == ['squash', 'squash', 'ok'] assert len(retries) == 2 assert retries[0].exception == first_exception assert isinstance(retries[0].response, pyramid.response.Response) assert retries[1].exception == second_exception assert isinstance(retries[1].response, pyramid.response.Response) def test_activate_hook_overrides_default_attempts(config): from pyramid_retry import RetryableException calls = [] def activate_hook(request): return 1 def bad_view(request): calls.append('fail') raise RetryableException config.add_settings({ 'retry.attempts': 3, 'retry.activate_hook': activate_hook, }) config.add_view(bad_view) app = config.make_wsgi_app() app = webtest.TestApp(app) with pytest.raises(Exception): app.get('/') assert calls == ['fail'] def test_activate_hook_falls_back_to_default_attempts(config): from pyramid_retry import RetryableException calls = [] def activate_hook(request): return None def bad_view(request): calls.append('fail') raise RetryableException config.add_settings({ 'retry.attempts': 3, 'retry.activate_hook': activate_hook, }) config.add_view(bad_view) app = config.make_wsgi_app() app = webtest.TestApp(app) with pytest.raises(Exception): app.get('/') assert calls == ['fail', 'fail', 'fail'] def test_request_make_body_seekable_cleans_up_threadmanger_on_exception(config): from pyramid.threadlocal import manager # Clear defaults. manager.pop() assert len(manager.stack) == 0 app = config.make_wsgi_app() app = webtest.TestApp(app) with pytest.raises(Exception): # Content-length=1 and empty body causes # webob.request.DisconnectionError: # The client disconnected while sending the body # (1 more bytes were expected) when you call # request.make_body_seekable(). app.get('/', headers={'Content-Length': '1'}) # len(manager.stack) == 1 when you don't catch exception # from request.make_body_seekable() and clean up. assert len(manager.stack) == 0 def test_activate_hook_cleans_up_threadmanager_on_exception(config): from pyramid.threadlocal import manager # Clear defaults. manager.pop() assert len(manager.stack) == 0 def activate_hook(request): raise Exception config.add_settings({ 'retry.attempts': 3, 'retry.activate_hook': activate_hook, }) app = config.make_wsgi_app() app = webtest.TestApp(app) with pytest.raises(Exception): app.get('/') # len(manager.stack) == 1 when you don't catch exception # from activate_hook and clean up. assert len(manager.stack) == 0 def test_activate_hook_cleans_up_threadmanager_on_generator_exit(config): from pyramid.threadlocal import manager # Clear defaults. manager.pop() assert len(manager.stack) == 0 def activate_hook(request): raise GeneratorExit config.add_settings({ 'retry.attempts': 3, 'retry.activate_hook': activate_hook, }) app = config.make_wsgi_app() app = webtest.TestApp(app) with pytest.raises(GeneratorExit): app.get('/') # len(manager.stack) == 1 when you don't catch GeneratorExit # from activate_hook and clean up. assert len(manager.stack) == 0 def test_is_last_attempt_True_when_inactive(): from pyramid_retry import is_last_attempt request = pyramid.request.Request.blank('/') assert is_last_attempt(request) def test_retryable_error_predicate_is_bool(config): from pyramid.exceptions import ConfigurationError view = lambda r: 'ok' with pytest.raises(ConfigurationError): config.add_view(view, retryable_error='yes', renderer='string') config.commit() def test_last_retry_attempt_predicate_is_bool(config): from pyramid.exceptions import ConfigurationError view = lambda r: 'ok' with pytest.raises(ConfigurationError): config.add_view(view, last_retry_attempt='yes', renderer='string') config.commit() def test_mark_error_retryable_on_non_error(): from pyramid_retry import mark_error_retryable with pytest.raises(ValueError): mark_error_retryable('some string') pyramid_retry-2.1.1/tox.ini000066400000000000000000000022051363547756700157460ustar00rootroot00000000000000[tox] envlist = lint, py27,py34,py35,py36,py37,pypy,pypy3, docs,coverage [testenv] commands = py.test --cov --cov-report= {posargs:} setenv = COVERAGE_FILE=.coverage.{envname} extras = testing [testenv:coverage] skip_install = true commands = coverage combine coverage report --fail-under=100 deps = coverage setenv = COVERAGE_FILE=.coverage [testenv:docs] whitelist_externals = make commands = make -C docs html BUILDDIR={envdir} SPHINXOPTS="-W -E" extras = docs [testenv:lint] skip_install = true commands = flake8 src/pyramid_retry/ setup.py python setup.py check -r -s -m check-manifest deps = flake8 readme_renderer check-manifest [testenv:build] skip_install = true commands = # clean up build/ and dist/ folders python -c 'import shutil; shutil.rmtree("dist", ignore_errors=True)' python setup.py clean --all # build sdist python setup.py sdist --dist-dir {toxinidir}/dist # build wheel from sdist pip wheel -v --no-deps --no-index --wheel-dir {toxinidir}/dist --find-links {toxinidir}/dist pyramid_retry deps = setuptools wheel