pax_global_header00006660000000000000000000000064147153234060014517gustar00rootroot0000000000000052 comment=dfe9d801e669b79e7a434c5646a7461d96a4e788 pyramid_tm-2.6/000077500000000000000000000000001471532340600135335ustar00rootroot00000000000000pyramid_tm-2.6/.coveragerc000066400000000000000000000002641471532340600156560ustar00rootroot00000000000000[run] parallel = true source = pyramid_tm [paths] source = src/pyramid_tm */src/pyramid_tm */site-packages/pyramid_tm [report] show_missing = true precision = 2 pyramid_tm-2.6/.flake8000066400000000000000000000030641471532340600147110ustar00rootroot00000000000000# Recommended flake8 settings while editing, we use Black for the final linting/say in how code is formatted # # pip install flake8 flake8-bugbear # # This will warn/error on things that black does not fix, on purpose. # # Run: # # tox -e run-flake8 # # To have it automatically create and install the appropriate tools, and run # flake8 across the source code/tests [flake8] # max line length is set to 88 in black, here it is set to 80 and we enable bugbear's B950 warning, which is: # # B950: Line too long. This is a pragmatic equivalent of pycodestyle’s E501: it # considers “max-line-length” but only triggers when the value has been # exceeded by more than 10%. You will no longer be forced to reformat code due # to the closing parenthesis being one character too far to satisfy the linter. # At the same time, if you do significantly violate the line length, you will # receive a message that states what the actual limit is. This is inspired by # Raymond Hettinger’s “Beyond PEP 8” talk and highway patrol not stopping you # if you drive < 5mph too fast. Disable E501 to avoid duplicate warnings. max-line-length = 80 max-complexity = 12 select = E,F,W,C,B,B9 ignore = # E123 closing bracket does not match indentation of opening bracket’s line E123 # E203 whitespace before ‘:’ (Not PEP8 compliant, Python Black) E203 # E501 line too long (82 > 79 characters) (replaced by B950 from flake8-bugbear, https://github.com/PyCQA/flake8-bugbear) E501 # W503 line break before binary operator (Not PEP8 compliant, Python Black) W503 pyramid_tm-2.6/.github/000077500000000000000000000000001471532340600150735ustar00rootroot00000000000000pyramid_tm-2.6/.github/dependabot.yml000066400000000000000000000003321471532340600177210ustar00rootroot00000000000000# Set update schedule for GitHub Actions version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: # Check for updates to GitHub Actions every weekday interval: "daily" pyramid_tm-2.6/.github/workflows/000077500000000000000000000000001471532340600171305ustar00rootroot00000000000000pyramid_tm-2.6/.github/workflows/ci-tests.yml000066400000000000000000000073451471532340600214170ustar00rootroot00000000000000name: Build and test on: # Only on pushes to main or one of the release branches we build on push push: branches: - main - "[0-9].[0-9]+-branch" tags: # Build pull requests pull_request: jobs: test: strategy: matrix: py: - "3.9" - "3.10" - "3.11" - "3.12" - "3.13" - "pypy-3.9" - "pypy-3.10" os: - "ubuntu-22.04" - "windows-latest" - "macos-13" # x64 - "macos-14" # arm64 architecture: - x64 - arm64 include: - py: "pypy-3.9" toxenv: "pypy39" - py: "pypy-3.10" toxenv: "pypy310" exclude: # Ubuntu does not have arm64 releases - os: "ubuntu-22.04" architecture: arm64 # MacOS we need to make sure to remove x86 on all # We need to run no arm64 on macos-13 (Intel), but some # Python versions: 3.9/3.10 # # From 3.11 onward, there is support for running x64 and # arm64 on Apple Silicon based systems (macos-14) - os: "macos-13" architecture: arm64 - os: "macos-14" architecture: x64 py: "3.9" - os: "macos-14" architecture: x64 py: "3.10" # Windows does not have arm64 releases - os: "windows-latest" architecture: arm64 name: "Python: ${{ matrix.py }}-${{ matrix.architecture }} on ${{ matrix.os }}" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: python-version: ${{ matrix.py }} architecture: ${{ matrix.architecture }} - run: pip install tox - name: Running tox with specific toxenv if: ${{ matrix.toxenv != '' }} env: TOXENV: ${{ matrix.toxenv }} run: tox - name: Running tox for current python version if: ${{ matrix.toxenv == '' }} run: tox -e py coverage: runs-on: ubuntu-latest name: Validate coverage steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: python-version: "3.10" architecture: x64 - run: pip install tox - run: tox -e py310-pyramid20,coverage docs: runs-on: ubuntu-latest name: Build the documentation steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: python-version: "3.10" architecture: x64 - run: pip install tox - run: tox -e docs lint: runs-on: ubuntu-latest name: Lint the package steps: - uses: actions/checkout@v4 - name: Setup python uses: actions/setup-python@v5 with: python-version: "3.10" architecture: x64 - run: pip install tox - run: tox -e lint pyramid_tm-2.6/.gitignore000066400000000000000000000001161471532340600155210ustar00rootroot00000000000000*.egg-info/ *.pyc env*/ .coverage .coverage.* .tox/ dist/ build/ coverage.xml pyramid_tm-2.6/.readthedocs.yaml000066400000000000000000000003771471532340600167710ustar00rootroot00000000000000# https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2 build: os: ubuntu-22.04 tools: python: '3.12' sphinx: configuration: docs/conf.py python: install: - method: pip path: . extra_requirements: - docs pyramid_tm-2.6/CHANGES.rst000066400000000000000000000304211471532340600153350ustar00rootroot00000000000000Changes ------- 2.6 (2024-11-14) ^^^^^^^^^^^^^^^^ - Drop support for Python 3.7, and 3.8. - Add support for Python 3.11, 3.12, and 3.13. - rename "master" to "main" - Fix retrying transactions with `pyramid_retry` when using veto and a datamanger marks the exception as retryable. 2.5 (2022-03-12) ^^^^^^^^^^^^^^^^ - Drop support for Python 2.7, 3.4, 3.5, and 3.6. - Add support for Python 3.8, 3.9, and 3.10. - Blackify project source. 2.4 (2020-01-06) ^^^^^^^^^^^^^^^^ - Allow overriding ``pyramid_tm`` via the environ for testing purposes. See https://github.com/Pylons/pyramid_tm/pull/72 - When ``tm.annotate_user`` is enabled, use ``request.authenticated_userid`` instead of ``request.unauthenticated_userid``. The latter is deprecated in Pyramid 2.0. See https://github.com/Pylons/pyramid_tm/pull/72 2.3 (2019-09-30) ^^^^^^^^^^^^^^^^ - Mark all ``transaction.interfaces.TransientError`` exceptions automatically as retryable by ``pyramid_retry`` if it is installed. See https://github.com/Pylons/pyramid_tm/pull/71 2.2.1 (2018-10-23) ^^^^^^^^^^^^^^^^^^ - Support Python 3.7. - Fix error handling when using ``transaction >= 2.4.0``. See https://github.com/Pylons/pyramid_tm/pull/68 2.2 (2017-07-03) ^^^^^^^^^^^^^^^^ Backward Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~ - This is a backward-incompatible change for anyone using the ``tm.commit_veto`` hook. Anyone else is unaffected. The ``tm.commit_veto`` hook will now be consulted for any squashed exceptions instead of always aborting. Previously, if an exception was handled by an exception view, the transaction would always be aborted. Now, the ``commit_veto`` can inspect ``request.exception`` and the generated ``response`` to determine whether to commit or abort. The new behavior when using the ``pyramid_tm.default_commit_veto`` is that a squashed exception may be committed if either of the following conditions are true: - The response contains the ``x-tm`` header set to ``commit``. - The response's status code does not start with ``4`` or ``5``. In most cases the response would result in 4xx or 5xx exception and would be aborted - this behavior remains the same. However, if the squashed exception rendered a response that is 3xx or 2xx (such as raising ``pyramid.httpexceptions.HTTPFound``), then the transaction will be committed instead of aborted. See https://github.com/Pylons/pyramid_tm/pull/65 2.1 (2017-06-07) ^^^^^^^^^^^^^^^^ - On Pyramid >= 1.7 any errors raised from ``pyramid_tm`` invoking ``request.tm.abort`` and ``request.tm.commit`` will be caught and used to lookup and execute an exception view to return an error response. This exception view will be executed with an inactive transaction manager. See https://github.com/Pylons/pyramid_tm/pull/61 2.0 (2017-04-11) ^^^^^^^^^^^^^^^^ Major Features ~~~~~~~~~~~~~~ - The ``pyramid_tm`` tween has been moved **over** the ``EXCVIEW`` tween. This means the transaction is open during exception view execution. See https://github.com/Pylons/pyramid_tm/pull/55 - Added a ``pyramid_tm.is_tm_active`` and a ``tm_active`` view predicate which may be useful in exception views that require access to the database. See https://github.com/Pylons/pyramid_tm/pull/60 Backward Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~ - The ``tm.attempts`` setting has been removed and retry support has been moved into a new package named ``pyramid_retry``. If you want retry support then please look at that library for more information about installing and enabling it. See https://github.com/Pylons/pyramid_tm/pull/55 - The ``pyramid_tm`` tween has been moved **over** the ``EXCVIEW`` tween. If you have any hacks in your application that are opening a new transaction inside your exception views then it's likely you will want to remove them or re-evaluate when upgrading. See https://github.com/Pylons/pyramid_tm/pull/55 - Drop support for Pyramid < 1.5. Minor Features ~~~~~~~~~~~~~~ - Support for Python 3.6. 1.1.1 (2016-11-21) ^^^^^^^^^^^^^^^^^^ - ``pyramid_tm`` 1.1.0 failed to fix a unicode issue related to undecodable request paths. The placeholder message was not unicode. See https://github.com/Pylons/pyramid_tm/pull/52 - Include Changes in the main docs. 1.1.0 (2016-11-19) ^^^^^^^^^^^^^^^^^^ - Support ``transaction`` 2.x. - The transaction's request path and userid are now coerced to unicode by first decoding as ``utf-8`` and falling back to ``latin-1``. If the userid does not conform to these restrictions then set ``tm.annotate_user = no`` in your settings. See https://github.com/Pylons/pyramid_tm/pull/50 1.0.2 (2016-11-18) ^^^^^^^^^^^^^^^^^^ - Pin to ``transaction < 1.99`` as pyramid_tm is currently incompatible with the new 2.x release of transaction. See https://github.com/Pylons/pyramid_tm/issues/49 1.0.1 (2016-10-24) ^^^^^^^^^^^^^^^^^^ - Removes the ``AttributeError`` when ``request.tm`` is accessed outside the tween. It turns out this broke subrequests as well as ``pshell`` and ``pyramid.paster.bootstrapp`` CLI scripts, especially when using the global transaction manager which can be tracked outside of the tween. See https://github.com/Pylons/pyramid_tm/pull/48 1.0 (2016-09-12) ^^^^^^^^^^^^^^^^ - Drop Python 2.6, 3.2 and 3.3 support. - Add Python 3.5 support. - Subtle bugs can occur if you use the transaction manager during a request in which ``pyramid_tm`` is disabled via an ``activate_hook``. To combat these types of errors, attempting to access ``request.tm`` will now raise an ``AttributeError`` when ``pyramid_tm`` is inactive. See https://github.com/Pylons/pyramid_tm/pull/46 0.12.1 (2015-11-25) ^^^^^^^^^^^^^^^^^^^ - Fix compatibility with 1.2 and 1.3 again. This wasn't fully fixed in the 0.12 release as the tween was relying on request properties working (which they do not inside tweens in older versions). See https://github.com/Pylons/pyramid_tm/pull/39 0.12 (2015-05-20) ^^^^^^^^^^^^^^^^^ - Expose a ``tm.annotate_user`` option to avoid computing ``request.unauthenticated_userid`` on every request. See https://github.com/Pylons/pyramid_tm/pull/36 - Restore compatibility with Pyramid 1.2 and 1.3. 0.11 (2015-02-04) ^^^^^^^^^^^^^^^^^ - Add a hook to override creation of the transaction manager (the default remains the thread-local one accessed through ``transaction.manager``). See: https://github.com/Pylons/pyramid_tm/pull/31 0.10 (2015-01-06) ^^^^^^^^^^^^^^^^^ - Fix recording transactions with non-text, non-bytes userids. See: https://github.com/Pylons/pyramid_tm/issues/28 0.9 (2014-12-30) ^^^^^^^^^^^^^^^^ - Work around recording transaction userid containing unicode. See https://github.com/Pylons/pyramid_tm/pull/15, although the fix is different, to ensure Python3 compatibility. - Work around recording transaction notes containing unicode. https://github.com/Pylons/pyramid_tm/pull/25 0.8 (2014-11-12) ^^^^^^^^^^^^^^^^ - Add a new ``tm.activate_hook`` hook which can control when the transaction manager is active. For example, this may be useful in situations where the manager should be disabled for a particular URL. https://github.com/Pylons/pyramid_tm/pull/12 - Fix unit tests under Pyramid 1.5. - Fix a bug preventing retryable exceptions from actually being retried. https://github.com/Pylons/pyramid_tm/pull/8 - Don't call ``setUser`` on transaction if there is no user logged in. This could cause the username set on the transaction to be a strange string: " None". https://github.com/Pylons/pyramid_tm/pull/9 - Avoid crash when the ``path_info`` cannot be decoded from the request object. https://github.com/Pylons/pyramid_tm/pull/19 0.7 (2012-12-30) ^^^^^^^^^^^^^^^^ - Write unauthenticated userid and ``request.path_info`` as transaction metadata via ``t.setUser`` and ``t.note`` respectively during a commit. 0.6 (2012-12-26) ^^^^^^^^^^^^^^^^ - Disuse the confusing and bug-ridden generator-plus-context-manager "attempts" mechanism from the transaction package for retrying retryable exceptions (e.g. ZODB ConflictError). Use a simple while loop plus a counter and imperative logic instead. 0.5 (2012-06-26) ^^^^^^^^^^^^^^^^ Bug Fixes ~~~~~~~~~ - When a non-retryable exception was raised as the result of a call to ``transaction.manager.commit``, the exception was not reraised properly. Symptom: an unrecoverable exception such as ``Unsupported: Storing blobs in is not supported.`` would be swallowed inappropriately. 0.4 (2012-03-28) ^^^^^^^^^^^^^^^^ Bug Fixes ~~~~~~~~~ - Work around failure to retry ConflictError properly at commit time by the ``transaction`` 1.2.0 package. See https://mail.zope.org/pipermail/zodb-dev/2012-March/014603.html for details. Testing ~~~~~~~ - No longer tested under Python 2.5 by ``tox.ini`` (and therefore no longer tested under 2.5 by the Pylons Jenkins server). The package may still work under 2.5, but automated tests will no longer show breakage when it changes in ways that break 2.5 support. - Squash test deprecation warnings under Python 3.2. 0.3 (2011-09-27) ^^^^^^^^^^^^^^^^ Features ~~~~~~~~ - The transaction manager has been converted to a Pyramid 1.2 "tween" (instead of an event subscriber). It will be slotted directly "below" the exception view handler, meaning it will have a chance to handle exceptions before they are turned into responses. This means it's best to "raise HTTPFound(...)" instead of "return HTTPFound(...)" if you want an HTTP exception to abort the transaction. - The transaction manager will now retry retryable exceptions (such as a ZODB conflict error) if ``tm.attempts`` is configured to be more than the default of ``1``. See the ``Retrying`` section of the documentation. - Python 3.2 compatibility (requires Pyramid 1.3dev+). Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Incompatible with Pyramid < 1.2a1. Use ``pyramid_tm`` version 0.2 if you need compatibility with an older Pyramid installation. - The ``default_commit_veto`` commit veto callback is no longer configured into the system by default. Use ``tm.commit_veto = pyramid_tm.default_commit_veto`` in the deployment settings to add it. This is for parity with ``repoze.tm2``, which doesn't configure in a commit veto by default either. - The ``default_commit_veto`` no longer checks for the presence of the ``X-Tm-Abort`` header when attempting to figure out whether the transaction should be aborted (although it still checks for the ``X-Tm`` header). Use version 0.2 or a custom commit veto function if your application depends on the ``X-Tm-Abort`` header. - A commit veto is now called with two arguments: ``request`` and ``response``. The ``request`` is the webob request that caused the transaction manager to become active. The ``response`` is the response returned by the Pyramid application. This call signature is incompatible with older versions. The call signature of a ``pyramid_tm`` 0.2 and older commit veto accepted three arguments: ``environ``, ``status``, and ``headers``. If you're using a custom ``commit_veto`` function, you'll need to either convert your existing function to use the new calling convention or use a wrapper to make it compatible with the new calling convention. Here's a simple wrapper function (``bwcompat_commit_veto_wrapper``) that will allow you to use your existing custom commit veto function:: def bwcompat_commit_veto_wrapper(request, response): return my_custom_commit_veto(request.environ, response.status, response.headerlist) Deprecations ~~~~~~~~~~~~ - The ``pyramid_tm.commit_veto`` configuration setting is now canonically spelled as ``tm.commit_veto``. The older spelling will continue to work, but may raise a deprecation error when used. 0.2 (2011-07-18) ^^^^^^^^^^^^^^^^ - A new header ``X-Tm`` is now honored by the ``default_commit_veto`` commit veto hook. If this header exists in the headerlist, its value must be a string. If its value is ``commit``, the transaction will be committed regardless of the status code or the value of ``X-Tm-Abort``. If the value of the ``X-Tm`` header is ``abort`` (or any other string value except ``commit``), the transaction will be aborted, regardless of the status code or the value of ``X-Tm-Abort``. 0.1 (2011-02-23) ^^^^^^^^^^^^^^^^ - Initial release, based on repoze.tm2 pyramid_tm-2.6/CONTRIBUTORS.txt000066400000000000000000000115771471532340600162440ustar00rootroot00000000000000Pylons 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 by notifying 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 Repoze Public 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 Repoze Public License (see LICENSE.txt, aka "the RPL") or another license acceptable to Agendaless Consulting. Until Agendaless Consulting declares in writing an acceptable license other than the RPL, only the RPL 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. List of Contributors ==================== The below-signed are contributors to a code repository that is part of the project named "pyramid_tm". 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 ------------ - Rocky Burt, 2011/02/21 - Michael Merickel, 2011/07/05 - Tres Seaver, 2013/01/01 - Kridsada Thanabulpong, 2013/01/22 - Kamal Gill, 2013/10/01 - Truveris, Ryan P. Kelly, 2014/11/07 - Gavin Carothers, 2014/11/11 - Chris McDonough, 2014/11/12 - Chris Rossi, 2014/11/20 - Donald Stufft, 2015/02/02 - Nick Stenning, 2016/09/06 - Steve Piercy, 2016/11/19 - Sean Hammond, 2019/09/27 - Jon Betts, 2021/04/19 - Jonathan Vanasco, 2022/11/14 - Christian Zagrodnick, 2024/11/04 pyramid_tm-2.6/COPYRIGHT.txt000066400000000000000000000001611471532340600156420ustar00rootroot00000000000000Copyright (c) 2008-2011 Agendaless Consulting and Contributors. (http://www.agendaless.com), All Rights Reserved pyramid_tm-2.6/LICENSE.txt000066400000000000000000000032671471532340600153660ustar00rootroot00000000000000A copyright notice accompanies this license document that identifies the copyright holders. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions in source code must retain the accompanying copyright notice, this list of conditions, and the following disclaimer. 2. Redistributions in binary form must reproduce the accompanying copyright notice, this list of conditions, and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Names of the copyright holders must not be used to endorse or promote products derived from this software without prior written permission from the copyright holders. 4. If any files are modified, you must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. Disclaimer THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED 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 HOLDERS 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. pyramid_tm-2.6/MANIFEST.in000066400000000000000000000004701471532340600152720ustar00rootroot00000000000000graft src/pyramid_tm graft tests graft docs graft .github include README.rst include LICENSE.txt include CONTRIBUTORS.txt include COPYRIGHT.txt include CHANGES.rst include setup.cfg pyproject.toml include .coveragerc .flake8 include tox.ini rtd.txt .readthedocs.yaml recursive-exclude * __pycache__ *.py[cod] pyramid_tm-2.6/README.rst000066400000000000000000000014761471532340600152320ustar00rootroot00000000000000``pyramid_tm`` ============== .. image:: https://github.com/Pylons/pyramid_tm/actions/workflows/ci-tests.yml/badge.svg?branch=main :target: https://github.com/Pylons/pyramid_tm/actions/workflows/ci-tests.yml?query=branch%3Amain :alt: main CI Status .. image:: https://readthedocs.org/projects/pyramid-tm/badge/?version=latest :target: https://docs.pylonsproject.org/projects/pyramid-tm/en/latest/ :alt: Documentation Status ``pyramid_tm`` is a package which allows Pyramid requests to join the active transaction as provided by the `transaction `_ package. See `https://docs.pylonsproject.org/projects/pyramid_tm/en/latest/ `_ or ``docs/index.rst`` in this distribution for detailed documentation. pyramid_tm-2.6/docs/000077500000000000000000000000001471532340600144635ustar00rootroot00000000000000pyramid_tm-2.6/docs/.gitignore000066400000000000000000000000311471532340600164450ustar00rootroot00000000000000_build/ _themes/ .static pyramid_tm-2.6/docs/Makefile000066400000000000000000000055221471532340600161270ustar00rootroot00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = -W SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html web pickle htmlhelp latex changes linkcheck help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " pickle to make pickle files (usable by e.g. sphinx-web)" @echo " htmlhelp to make HTML files and a HTML help project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview over all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" clean: -rm -rf $(BUILDDIR)/* html: mkdir -p $(BUILDDIR)/html $(BUILDDIR)/doctrees $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." text: mkdir -p $(BUILDDIR)/text $(BUILDDIR)/doctrees $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/text." pickle: mkdir -p $(BUILDDIR)/pickle $(BUILDDIR)/doctrees $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files or run" @echo " sphinx-web $(BUILDDIR)/pickle" @echo "to start the sphinx-web server." web: pickle htmlhelp: mkdir -p $(BUILDDIR)/htmlhelp $(BUILDDIR)/doctrees $(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." latex: mkdir -p $(BUILDDIR)/latex $(BUILDDIR)/doctrees $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex cp _static/*.png $(BUILDDIR)/latex ./convert_images.sh cp _static/latex-warning.png $(BUILDDIR)/latex cp _static/latex-note.png $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: mkdir -p $(BUILDDIR)/changes $(BUILDDIR)/doctrees $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: mkdir -p $(BUILDDIR)/linkcheck $(BUILDDIR)/doctrees $(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." epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." pyramid_tm-2.6/docs/_static/000077500000000000000000000000001471532340600161115ustar00rootroot00000000000000pyramid_tm-2.6/docs/_static/logo_hi.gif000066400000000000000000000075241471532340600202300ustar00rootroot00000000000000GIF89a3BBBًzι<<>>ԾԾտѻԿLKIԶľBBA@@?|||??>AAAȼEEEHGEڷcccյųMMM˽͸̾vur@??YYYxCCCþihc:::ӽ!,3 H*\ȰÇ#JHŋ3jȱdžIrCȲ˅.ȜI^F͟?ѬIhE*XMJu! j%D. MjJ?_Ŗ@D|"݅t['Y EHG8._7CW"7)Tv'r֦+RcN+'n c ZMd#g׎9f _HrA\FSAUR`rMq`E"Wҥ&7ĄB5BU t fM?~O?5WGՂ|,ȢC4D54Xc8+pA , 9$ C- Cy  & E(q0 t$ ܴf(D.@>yXM4+ 7H‚JhTBE BZ0'@YN16tQbP X*:ab lTåuQf jJ dBDNC tP<P _PPRlpʴ ;D2$$aQCk72/Dx0(g& J#̐A:r AD!8(a 8žT4΃»@ 3(h.@B氽T@L;|B١sC@khQ:QęCt 0`=GJX-W|A !,@3zrasA rnqRh2[K\ Djئ<9E*Z8p@!p.n B x| ՐtS%=a,$`h %@A'6xB †) 6x;50bLPxF+90bA00)4l#QC-D&lp}PGvG<ڠ'cC!u!,._h ZN_a $ @Ҏ+P2R(ՇMosP/`qg.b Ed_ D 2,%vvm!^+P/yz"<S?yH€WPR C4;@`b9-@_0jj`!`\8lyxw WH0 `cPaQ 0$ W}GJ l   aCЍpqQ@PA dvyGP pYP E" (P{ 0@@ 1 F@d s @ 900) ٟ ;pyramid_tm-2.6/docs/api.rst000066400000000000000000000005161471532340600157700ustar00rootroot00000000000000.. _pyramid_tm_api: :mod:`pyramid_tm` API --------------------------- .. automodule:: pyramid_tm .. autofunction:: includeme .. autofunction:: is_tm_active .. autofunction:: default_commit_veto .. autofunction:: tm_tween_factory .. autofunction:: create_tm .. autofunction:: explicit_manager .. autoclass:: TMActivePredicate pyramid_tm-2.6/docs/changes.rst000066400000000000000000000000341471532340600166220ustar00rootroot00000000000000.. include:: ../CHANGES.rst pyramid_tm-2.6/docs/conf.py000066400000000000000000000145651471532340600157750ustar00rootroot00000000000000# -*- coding: utf-8 -*- # # pyramid_tm documentation build configuration file # # This file is execfile()d with the current directory set to its containing # dir. # # The contents of this file are pickled, so don't put values in the # namespace that aren't pickleable (module imports are okay, they're # removed automatically). # # All configuration values have a default value; values that are commented # out serve to show the default value. # If your extensions are in another directory, add it here. If the # directory is relative to the documentation root, use os.path.abspath to # make it absolute, like shown here. #sys.path.append(os.path.abspath('some/directory')) import datetime import pkg_resources import pylons_sphinx_themes html_theme_path = pylons_sphinx_themes.get_html_themes_path() html_theme = 'pyramid' html_theme_options = dict(github_url='https://github.com/Pylons/pyramid_tm') # General configuration # --------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', ] # Looks for pyramid's objects intersphinx_mapping = { 'pyramid': ('https://docs.pylonsproject.org/projects/pyramid/en/latest/', None)} # Add any paths that contain templates here, relative to this directory. templates_path = ['.templates'] # The suffix of source filenames. source_suffix = '.rst' # The main toctree document. master_doc = 'index' # General substitutions. thisyear = datetime.datetime.now().year copyright = '2011-%s, Agendaless Consulting ' % thisyear project = 'pyramid_tm' # The default replacements 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_tm').version # The full version, including alpha/beta/rc tags. release = version # 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 documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directories, that shouldn't be # searched for source files. #exclude_dirs = [] exclude_patterns = ['_themes/README.rst',] # 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' # Do not use smart quotes. smartquotes = False # Options for HTML output # ----------------------- # Add and use Pylons theme # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. # html_style = 'repoze.css' # 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 (within the static path) to place at the top of # the sidebar. # html_logo = '_static/logo_hi.gif' # 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_use_modindex = 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, the reST sources are included in the HTML build as # _sources/. #html_copy_source = 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 = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'atemplatedoc' # Options for LaTeX output # ------------------------ # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, document class [howto/manual]). latex_documents = [ ('index', 'pyramid_tm.tex', 'pyramid_tm Documentation', 'Repoze Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the # top of the title page. latex_logo = '_static/logo_hi.gif' # For "manual" documents, if this is true, then toplevel headings are # parts, not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True pyramid_tm-2.6/docs/glossary.rst000066400000000000000000000026541471532340600170670ustar00rootroot00000000000000.. _glossary: Glossary ======== .. glossary:: :sorted: Pyramid A `web framework `_. data manager The ``transaction`` package wraps data managers implemented for different transactional backends, such as SQLAlchemy (``zope.sqlalchemy``), but also many others. retryable A retryable exception is any exception that is recognized as retryable by an active :term:`data manager`. These errors usually inherit from ``transaction.interfaces.TransientError``. These errors are temporary and thus marked as retryable. For example, a serialization error in a database resulting from concurrent transactions. transaction A database transaction comprises a unit of work performed within a database management system. In the context of the Pyramid documentation, "transaction" is also the name of a `Python package `__ used by ``pyramid_tm``. dotted Python name A reference to a Python object by name using a string, in the form ``path.to.modulename:attributename``. Often used in Pyramid and Setuptools configurations. A variant is used in dotted names within configurator method arguments that name objects (such as the "add_view" method's "view" and "context" attributes): the colon (``:``) is not used; in its place is a dot. pyramid_tm-2.6/docs/index.rst000066400000000000000000000403731471532340600163330ustar00rootroot00000000000000pyramid_tm ========== .. _overview: Overview -------- ``pyramid_tm`` is a package which allows :term:`Pyramid` requests to join the active :term:`transaction` as provided by the Python `transaction `_ package. (See the `documentation for the transaction package `_ for an explanation of what "joining the active transaction" means). Installation ------------ Install using pip, e.g. (within a virtualenv):: $ pip install pyramid_tm Setup ----- Once ``pyramid_tm`` is installed, you must use the ``config.include`` mechanism to include it into your Pyramid project's configuration. In your Pyramid project's ``__init__.py``: .. code-block:: python :linenos: config = Configurator(.....) config.include('pyramid_tm') Or use the ``pyramid.includes`` configuration setting in your ``.ini`` file: .. code-block:: ini :linenos: [app:myapp] pyramid.includes = pyramid_tm After the package is included, whenever a new request enters the application, a new transaction is associated with that request. .. note:: When the ``repoze.tm`` or ``repoze.tm2`` middleware is in the WSGI pipeline, ``pyramid_tm`` becomes inactive. :term:`transaction` Usage ------------------------- At the beginning of a request a new :term:`transaction` is started using the ``request.tm.begin()`` function. Once the request has finished all of its works (ie views have finished running), a few checks are tested: 1) Did a transaction.doom() cause the transaction to become "doomed"? if so, ``request.tm.abort()``. 2) Did an exception occur in the underlying code? if so, ``request.tm.abort()`` 3) If the ``tm.commit_veto`` configuration setting was used, did the commit veto callback, called with the response generated by the application, return a result that evaluates to ``True``? if so, ``request.tm.abort()``. If none of these checks calls ``request.tm.abort()`` then the transaction is instead committed using ``request.tm.commit()``. By itself, this :term:`transaction` machinery doesn't do much. It is up to third-party code to *join* the active transaction to benefit. See `repoze.filesafe `_ for an example of how files creation can be committed or rolled back based on :term:`transaction` and the `pyramid_mailer `_ package to see how you can prevent emails from being sent until a transaction succeeds. ZODB database connections are automatically joined to the transaction, as well as SQLAlchemy connections which are configured with ``zope.sqlalchemy.register(session)`` from the `zope.sqlalchemy `_ package. Savepoints ---------- When using sessions / data managers joined to the transaction, it's important to synchronize changes across those managers. This means that it's usually incorrect to use your backend's session lifecycle functions directly such as ``sqlalchemy.orm.Session.begin_nested``. Instead, synchronize a savepoint across all joined data managers via ``sp = request.tm.savepoint()``. The savepoint can be rolled back via ``sp.rollback()``. For example: .. code-block:: python def my_view(request): sp = request.tm.savepoint() try: page = WikiPage() page.id = 5 # maybe the id 5 violates a unique constraint request.dbsession.add(page) request.dbsession.flush() except sqlalchemy.exc.IntegrityError: # page already exists! sp.rollback() # continue with or without the data added in the try-clause ... .. note:: Not every data manager supports savepoints and as such some changes may not be able to be rolled back. .. _error_handling: Error Handling -------------- ``pyramid_tm`` is positioned **OVER** the ``EXCVIEW`` tween. The implication of this is that the transaction may still be open and alive during the execution of your exception views. **This is not guaranteed**. If you write an exception view that expects an open transaction then you should declare your intent using the ``tm_active=True`` view predicate otherwise it may be executed later in the pipeline after the transaction has already been completed. For example: .. code-block:: python from pyramid.view import exception_view_config log = __import__('logging').getLogger(__name__) @exception_view_config(Exception, tm_active=True) def transactional_error_view(exc, request): # depending on your AuthenticationPolicy the authenticated # userid likely requires a lookup in your database which would # require an active transaction if request.authenticated_userid is not None: log.exception('authenticated user caused an exception') else: log.exception('unknown user caused an exception') response = request.response response.status_code = 500 return response @exception_view_config(Exception) def default_error_view(exc, request): log.exception('unknown user caused an exception') response = request.response response.status_code = 500 return response In the above example, ``transactional_error_view`` will be invoked only when an exception occurs during the ``pyramid_tm`` lifecycle. Otherwise, ``default_error_view`` will be invoked as a fallback. The transaction created and completed by ``pyramid_tm`` should be used for operations directly related to processing the request. Very often it is desirable to perform operations on the database and other backends in a failure scenario. This should be done using a separate transaction / connection, possibly in autocommit mode. **Do not** use ``request.tm`` and ``request.dbsession`` and such for these cases as the work added to that transaction is expected to be aborted upon any failures. Retries ------- ``pyramid_tm`` ships with support for `pyramid_retry `_ which is an execution policy that will retry requests when they fail with exceptions marked as retryable. By default, retrying is turned off. In order to turn it on you must update your app's configuration: .. code-block:: python from pyramid.config import Configurator def main(global_config, **settings): config = Configurator(settings=settings) config.include('pyramid_retry') config.include('pyramid_tm') Finally, ensure that your application's settings have ``retry.attempts`` set to a value greater than ``1``. When the transaction manager calls the downstream handler, if the handler raises a :term:`retryable` exception, ``pyramid_tm`` will mark the exception as retryable by ``pyramid_retry``. The execution policy will detect a retryable error and create a new copy of the request with new state. Retryable exceptions include ``ZODB.POSException.ConflictError``, and certain exceptions raised by various data managers, such as ``psycopg2.extensions.TransactionRollbackError``, ``cx_Oracle.DatabaseError`` where the exception's code is 8877. Any exception which inherits from ``transaction.interfaces.TransientError`` will be marked as retryable. Read more about retrying requests in the `pyramid_retry documentation `_. Custom Transaction Managers --------------------------- By default ``pyramid_tm`` will use the threadlocal ``transaction.manager`` to associate one transaction manager per thread. If you wish to override this and provide your own transaction manager you can create your own manager hook that will return the manager it should use. .. code-block:: python :linenos: import transaction def manager_hook(request): return transaction.TransactionManager(explicit=True) To enable this hook, add it as the ``tm.manager_hook`` setting in your app. .. code-block:: python :linenos: from pyramid.config import Configurator def app(global_conf, **settings): settings['tm.manager_hook'] = manager_hook config = Configurator(settings=settings) config.include('pyramid_tm') # ... This specific example, using an explicit mode non-threadlocal manager, is highly recommended and is shipped as :func:`pyramid_tm.explicit_manager`. Simply set ``tm.manager_hook = pyramid_tm.explicit_manager`` in your settings to enable it. The current transaction manager being used for any particular request can always be accessed on the request as ``request.tm`` so long as it is accessed while the ``pyramid_tm`` tween is active. If you try to access ``request.tm`` outside of the tween or during a request in which ``pyramid_tm`` was disabled, ``request.tm`` will raise an ``AttributeError``. .. note:: It is recommended to use a custom transaction manager with ``explicit=True``, as in the example above, instead of the threadlocal ``transaction.manager`` to give greater control over the transaction's lifecycle and to weed out potential bugs in your application. For example, you may have some parts of your app that access the manager after it has already been committed. This will open an implicit transaction that is never committed, and will even hang around until a subsequent request aborts the implicit transaction. Instead, if you set ``explicit=True``, any code affecting the manager outside of the lifecycle of the transaction will cause an error and will be noticed quickly. Adding an Activation Hook ------------------------- It may not always be desirable to have every request managed by the transaction manager automatically. It is possible to configure ``pyramid_tm`` with an "activate" hook. The callback function receives the request. It can then examine it and return ``False`` if the transaction manager should be disabled for that request. .. code-block:: python :linenos: def activate_hook(request): if request.path_info.startswith('/long-poll'): # Allow the long-poll class to manage its own connections to avoid # long-lived transactions. return False return True To enable this hook, add it as the ``tm.activate_hook`` setting in your app. .. code-block:: python :linenos: from pyramid.config import Configurator def app(global_conf, **settings): settings['tm.activate_hook'] = activate_hook config = Configurator(settings=settings) config.include('pyramid_tm') # ... Or via PasteDeploy: .. code-block:: ini :linenos: [app:myapp] tm.activate_hook = myapp.activate_hook In either configuration the value for ``tm.activate_hook`` is a :term:`dotted Python name`. Adding a Commit Veto Hook ------------------------- It is possible to configure ``pyramid_tm`` with a "commit veto" hook. The commit veto hook receives the request and the response. It can examine both of them, and return ``True`` if the transaction should be vetoed. If the transaction is vetoed, it will be aborted instead of committed. By default, ``pyramid_tm`` does not configure a commit veto into the system; you must do it explicitly. :mod:`pyramid_tm` contains a :func:`pyramid_tm.default_commit_veto` that is suitable for use when you want to abort when the response's status code indicates non-success or if you'd like to signal that the transaction should be aborted or committed using a response header. The default commit veto vetoes a commit if the status code starts with ``4`` or ``5`` or there is a ``X-Tm`` response header with a value that does not equal ``commit``. .. code-block:: python :linenos: def default_commit_veto(request, response): xtm = response.headers.get('x-tm') if xtm is not None: return xtm != 'commit' return response.status.startswith(('4', '5')) If you'd like to use this commit veto in your system, you can do it via Python: .. code-block:: python :linenos: from pyramid.config import Configurator def app(global_conf, **settings): settings['tm.commit_veto'] = 'pyramid_tm.default_commit_veto' config = Configurator(settings=settings) config.include('pyramid_tm') # ... Or via PasteDeploy: .. code-block:: ini :linenos: [app:myapp] tm.commit_veto = pyramid_tm.default_commit_veto If you'd like to use a different "commit veto" callback, create a function with the same signature (``request``, ``response``) and return value (``True`` or ``False``), then pass a ``tm.commit_veto`` key/value pair in your settings which points at the Python dotted name of this commit veto. Via Python: .. code-block:: python :linenos: from pyramid.config import Configurator def app(global_conf, settings): settings['tm.commit_veto'] = 'my.package.commit_veto' config = Configurator(settings=settings) config.include('pyramid_tm') Via PasteDeploy: .. code-block:: ini :linenos: [app:myapp] tm.commit_veto = my.package.commit_veto In the PasteDeploy example, the path is a :term:`dotted Python name`, where the dots separate module and package names, and the colon separates a module from its contents. In the above example, the code would be implemented as a "commit_veto" function which lives in the "package" submodule of the "my" package. View Predicates --------------- ``pyramid_tm`` registers a view predicate named ``tm_active`` which accepts a value of ``True`` or ``False``. This can be useful for declaring intent when defining exception views that require access to the transaction controlled by ``pyramid_tm``. For specific examples, see :ref:`error_handling`. If the request is manually completed via ``request.tm.abort()`` or ``request.tm.commit()``, this predicate may be incorrect depending on the specific transaction manager being used. After completing a transaction controlled by the transaction manager in explicit mode it is necessary to invoke ``request.tm.begin()`` to start a new one or any subsequent uses of the transaction manager will fail. Explicit Tween Configuration ---------------------------- Note that the transaction manager is a Pyramid "tween", and it can be used in the explicit tween list if its implicit position in the tween chain is incorrect (see the output of ``ptweens``):: [app:myapp] pyramid.tweens = someothertween pyramid_tm.tm_tween_factory pyramid.tweens.excview_tween_factory It usually belongs directly above the "pyramid.tweens.excview_tween_factory" entry in the `` ptweens`` output, and will attempt to sort there by default as the result of having ``config.include('pyramid_tm')`` invoked. Avoid Accessing the Authentication Policy ----------------------------------------- By default the tween will access :attr:`pyramid.request.Request.authenticated_userid` in order to annotate the transaction with information about the user. This can be turned off by setting the ini option ``tm.annotate_user = false``. Testing ------- You can partially disable or override ``pyramid_tm`` in your test suite. This can be helpful if you want to handle transactions externally - allowing you to rollback or keep them open across multiple requests. 1. Tell ``pyramid_tm`` that something else is handling transactions by setting ``tm.active`` in the WSGI environ. 2. Provide your own transaction manager to the app to override ``request.tm`` by setting ``tm.manager`` to your own object. .. code-block:: python :linenos: import pytest import transaction from webtest import TestApp @pytest.fixture def testapp(): app = ... tm = transaction.TransactionManager(explicit=True) tm.begin() tm.doom() # ensure no one can call tm.commit() manually testapp = TestApp(app, extra_environ={ 'tm.active': True, # disable pyramid_tm 'tm.manager': tm, # pass in our own tm for the app to use }) yield testapp tm.abort() More Information ---------------- .. toctree:: :maxdepth: 1 api.rst glossary.rst changes.rst Reporting Bugs / Development Versions ------------------------------------- Visit https://github.com/Pylons/pyramid_tm to download development or tagged versions. Visit https://github.com/Pylons/pyramid_tm/issues to report bugs. Indices and tables ------------------ * :ref:`glossary` * :ref:`genindex` * :ref:`modindex` * :ref:`search` pyramid_tm-2.6/pyproject.toml000066400000000000000000000012041471532340600164440ustar00rootroot00000000000000[build-system] requires = ["setuptools >= 41", "wheel"] build-backend = "setuptools.build_meta" [tool.black] line-length = 79 skip-string-normalization = true target-version = ['py37', 'py38', 'py39', 'py310'] exclude = ''' /( \.git | \.mypy_cache | \.tox | \.venv | \.pytest_cache | dist | build | docs )/ ''' # This next section only exists for people that have their editors # automatically call isort, black already sorts entries on its own when run. [tool.isort] profile = "black" line_length = 79 force_sort_within_sections = true sections = "FUTURE,THIRDPARTY,FIRSTPARTY,LOCALFOLDER" known_first_party = "pyramid_tm" pyramid_tm-2.6/rtd.txt000066400000000000000000000000131471532340600150570ustar00rootroot00000000000000-e .[docs] pyramid_tm-2.6/setup.cfg000066400000000000000000000034441471532340600153610ustar00rootroot00000000000000[metadata] name = pyramid_tm version = 2.6 description = A package which allows Pyramid requests to join the active transaction long_description = file: README.rst, CHANGES.rst long_description_content_type = text/x-rst keywords = wsgi pylons pyramid transaction license = BSD-derived (Repoze) license_file = LICENSE.txt classifiers = Development Status :: 6 - Mature Intended Audience :: Developers Programming Language :: Python Programming Language :: Python :: 3 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 Programming Language :: Python :: 3.13 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Operating System :: OS Independent Framework :: Pyramid License :: Repoze Public License url = https://github.com/Pylons/pyramid_tm project_urls = Documentation = https://docs.pylonsproject.org/projects/pyramid-tm/en/latest/index.html Changelog = https://docs.pylonsproject.org/projects/pyramid-tm/en/latest/changes.html Issue Tracker = https://github.com/Pylons/pyramid_tm/issues author = Rocky Burt, Chris McDonough author_email = pylons-discuss@googlegroups.com maintainer = Pylons Project maintainer_email = pylons-discuss@googlegroups.com [options] package_dir= =src packages = find: include_package_data = True python_requires = >=3.9 install_requires = pyramid >= 1.5 transaction >= 2.0 [options.packages.find] where = src [options.extras_require] testing = WebTest pytest pytest-cov coverage>=5.0 docs = Sphinx>=1.8.1 pylons-sphinx-themes>=1.0.9 [tool:pytest] python_files = test_*.py testpaths = tests addopts = -W always --cov pyramid_tm-2.6/setup.py000066400000000000000000000000461471532340600152450ustar00rootroot00000000000000from setuptools import setup setup() pyramid_tm-2.6/src/000077500000000000000000000000001471532340600143225ustar00rootroot00000000000000pyramid_tm-2.6/src/pyramid_tm/000077500000000000000000000000001471532340600164675ustar00rootroot00000000000000pyramid_tm-2.6/src/pyramid_tm/__init__.py000066400000000000000000000252121471532340600206020ustar00rootroot00000000000000from pyramid.exceptions import ConfigurationError, NotFound from pyramid.settings import asbool from pyramid.tweens import EXCVIEW from pyramid.util import DottedNameResolver import sys import transaction import warnings import zope.interface try: from pyramid_retry import IRetryableError except ImportError: # pragma: no cover IRetryableError = zope.interface.Interface try: from pyramid_retry import mark_error_retryable except ImportError: # pragma: no cover mark_error_retryable = lambda error: None mark_error_retryable(transaction.interfaces.TransientError) resolver = DottedNameResolver(None) def default_commit_veto(request, response): """ When used as a commit veto, the logic in this function will cause the transaction to be aborted if: - An ``X-Tm`` response header with the value ``abort`` (or any value other than ``commit``) exists. - The response status code starts with ``4`` or ``5``. Otherwise the transaction will be allowed to commit. """ xtm = response.headers.get('x-tm') if xtm is not None: return xtm != 'commit' return response.status.startswith(('4', '5')) class AbortWithResponse(Exception): """Abort the transaction but return a pre-baked response.""" def __init__(self, response): self.response = response def tm_tween_factory(handler, registry): settings = registry.settings maybe_resolve = lambda val: resolver.maybe_resolve(val) if val else None old_commit_veto = settings.get('pyramid_tm.commit_veto', None) commit_veto = settings.get('tm.commit_veto', old_commit_veto) activate_hook = settings.get('tm.activate_hook') commit_veto = maybe_resolve(commit_veto) activate_hook = maybe_resolve(activate_hook) annotate_user = asbool(settings.get('tm.annotate_user', True)) if 'tm.attempts' in settings: # pragma: no cover warnings.warn( 'pyramid_tm removed support for the "tm.attempts" ' 'setting in version 2.0. To re-enable retry support ' 'add pyramid_retry to your application.' ) # define a finish function that we'll call from every branch to # commit or abort the transaction - this can't be in a finally because # we only want the finisher to wrap commit/abort which occur in several # disparate branches below and we want to avoid catching errors from # non commit/abort related operations def _finish(request, finisher, response=None): # ensure the manager is inactive prior to invoking the finisher # such that when we handle any possible exceptions it is ready environ = request.environ if 'tm.active' in environ: del environ['tm.active'] if 'tm.manager' in environ: del environ['tm.manager'] try: finisher() # catch any errors that occur specifically during commit/abort # and attempt to render them to a response except Exception: exc_info = sys.exc_info() try: if hasattr(request, 'invoke_exception_view'): # pyramid >= 1.7 response = request.invoke_exception_view(exc_info) else: # pragma: no cover raise NotFound except NotFound: # since commit/abort has already been executed it's highly # likely we will not detect any backend-specific retryable # issues here unless they directly subclass TransientError # since the manager has cleared its list of data mangers at # this point maybe_tag_retryable(request, exc_info) raise exc_info[1] from None finally: del exc_info # avoid leak return response def tm_tween(request): environ = request.environ if ( # don't handle txn mgmt if repoze.tm is in the WSGI pipeline 'repoze.tm.active' in environ or # pyramid_tm should only be active once 'tm.active' in environ or # check activation hooks activate_hook is not None and not activate_hook(request) ): return handler(request) # grab a reference to the manager manager = request.tm # mark the environ as being managed by pyramid_tm environ['tm.active'] = True environ['tm.manager'] = manager t = manager.begin() try: # do not address the authentication policy until we are within # the transaction boundaries if annotate_user: userid = request.authenticated_userid if userid: t.user = str(userid) try: t.note(request.path_info) except UnicodeDecodeError: t.note("Unable to decode path as unicode") response = handler(request) if manager.isDoomed(): raise AbortWithResponse(response) if commit_veto is not None: if commit_veto(request, response): raise AbortWithResponse(response) # check for a squashed exception and handle it # this would happen if an exception view was invoked and # rendered an error response exc_info = getattr(request, 'exc_info', None) if exc_info is not None: maybe_tag_retryable(request, exc_info) if commit_veto is None: raise AbortWithResponse(response) return _finish(request, manager.commit, response) except AbortWithResponse as e: return _finish(request, manager.abort, e.response) # an unhandled exception was propagated - we should abort the # transaction and re-raise the original exception except Exception as exc: # try to tag the original exception as retryable before # aborting the transaction because after abort it may not # be possible to determine if the exception is retryable # because the bound data managers are cleared maybe_tag_retryable(request, sys.exc_info()) exc_response = _finish(request, manager.abort) if exc_response is not None: return exc_response raise exc from None return tm_tween def explicit_manager(request): """ Create a new ``transaction.TransactionManager`` in explicit mode. This is recommended transaction manager and will help to weed out errors caused by code that tweaks the transaction before it has begun or after it has ended. """ return transaction.TransactionManager(explicit=True) def maybe_tag_retryable(request, exc_info): exc = exc_info[1] txn = request.tm.get() if hasattr(txn, 'isRetryableError'): if txn.isRetryableError(exc): zope.interface.alsoProvides(exc, IRetryableError) # bw-compat transaction < 2.4 elif hasattr(request.tm, '_retryable'): # pragma: no cover if request.tm._retryable(*exc_info[:-1]): zope.interface.alsoProvides(exc, IRetryableError) def create_tm(request): manager = request.environ.get('tm.manager') if manager: return manager manager_hook = request.registry.settings.get('tm.manager_hook') if manager_hook: manager_hook = resolver.maybe_resolve(manager_hook) return manager_hook(request) return transaction.manager def is_tm_active(request): """ Return ``True`` if the ``request`` is currently being managed by the pyramid_tm tween. If ``False`` then it may be necessary to manage transactions yourself. .. note:: This does **not** indicate that there is a current transaction. For example, ``request.tm.get()`` may raise a ``NoTransaction`` error even though ``is_tm_active`` returns ``True``. This would be caused by user code that manually completed a transaction and did not begin a new one. """ return request.environ.get('tm.active', False) class TMActivePredicate(object): """ A :term:`view predicate` registered as ``tm_active``. 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_tm.is_tm_active`. """ def __init__(self, val, config): if not isinstance(val, bool): raise ConfigurationError( 'The "tm_active" view predicate value must be ' 'True or False.', ) self.val = val def text(self): return 'tm_active = %s' % (self.val,) phash = text def __call__(self, context, request): is_active = is_tm_active(request) return (self.val and is_active) or (not self.val and not is_active) def includeme(config): """ Set up an implicit 'tween' to do transaction management using the ``transaction`` package. The tween will be slotted between the Pyramid request ingress and the Pyramid exception view handler. For every request it handles, the tween will begin a transaction by calling ``request.tm.begin()``, and will then call the downstream handler (usually the main Pyramid application request handler) to obtain a response. When attempting to call the downstream handler: - If an exception is raised by downstream handler while attempting to obtain a response, the transaction will be rolled back (``request.tm.abort()`` will be called). - If no exception is raised by the downstream handler, but the transaction is doomed (``request.tm.doom()`` has been called), the transaction will be rolled back. - If the deployment configuration specifies a ``tm.commit_veto`` setting, and the transaction management tween receives a response from the downstream handler, the commit veto hook will be called. If it returns True, the transaction will be rolled back. If it returns ``False``, the transaction will be committed. - If none of the above conditions are true, the transaction will be committed (via ``request.tm.commit()``). """ config.add_tween('pyramid_tm.tm_tween_factory', over=EXCVIEW) config.add_request_method(create_tm, name='tm', reify=True) config.add_view_predicate('tm_active', TMActivePredicate) def ensure(): manager_hook = config.registry.settings.get("tm.manager_hook") if manager_hook is not None: manager_hook = resolver.maybe_resolve(manager_hook) config.registry.settings["tm.manager_hook"] = manager_hook config.action(None, ensure, order=10) pyramid_tm-2.6/tests/000077500000000000000000000000001471532340600146755ustar00rootroot00000000000000pyramid_tm-2.6/tests/__init__.py000066400000000000000000000006431471532340600170110ustar00rootroot00000000000000def veto_true(request, response): return True def veto_false(request, response): return False def activate_true(request): return True def activate_false(request): return False create_manager = None def dummy_tween_factory(handler, registry): def dummy_tween(request): dummy_handler = registry['dummy_handler'] return dummy_handler(handler, request) return dummy_tween pyramid_tm-2.6/tests/test_it.py000066400000000000000000000657701471532340600167410ustar00rootroot00000000000000import functools from pyramid import testing import transaction from transaction import TransactionManager import unittest import webtest from tests import activate_false, create_manager, dummy_tween_factory class TestDefaultCommitVeto(unittest.TestCase): def _callFUT(self, response, request=None): from pyramid_tm import default_commit_veto return default_commit_veto(request, response) def test_it_true_500(self): response = DummyResponse('500 Server Error') self.assertTrue(self._callFUT(response)) def test_it_true_503(self): response = DummyResponse('503 Service Unavailable') self.assertTrue(self._callFUT(response)) def test_it_true_400(self): response = DummyResponse('400 Bad Request') self.assertTrue(self._callFUT(response)) def test_it_true_411(self): response = DummyResponse('411 Length Required') self.assertTrue(self._callFUT(response)) def test_it_false_200(self): response = DummyResponse('200 OK') self.assertFalse(self._callFUT(response)) def test_it_false_201(self): response = DummyResponse('201 Created') self.assertFalse(self._callFUT(response)) def test_it_false_301(self): response = DummyResponse('301 Moved Permanently') self.assertFalse(self._callFUT(response)) def test_it_false_302(self): response = DummyResponse('302 Found') self.assertFalse(self._callFUT(response)) def test_it_false_x_tm_commit(self): response = DummyResponse('200 OK', {'x-tm': 'commit'}) self.assertFalse(self._callFUT(response)) def test_it_true_x_tm_abort(self): response = DummyResponse('200 OK', {'x-tm': 'abort'}) self.assertTrue(self._callFUT(response)) def test_it_true_x_tm_anythingelse(self): response = DummyResponse('200 OK', {'x-tm': ''}) self.assertTrue(self._callFUT(response)) class Test_tm_tween_factory(unittest.TestCase): def setUp(self): self.txn = DummyTransaction() self.request = DummyRequest() self.response = DummyResponse() self.config = testing.setUp(request=self.request) self.registry = self.config.registry self.settings = self.registry.settings def tearDown(self): testing.tearDown() def _callFUT(self, handler=None, registry=None, request=None, txn=None): if handler is None: def handler(request): return self.response if registry is None: registry = self.registry if request is None: request = self.request if txn is None: txn = self.txn request.tm = txn from pyramid_tm import tm_tween_factory factory = tm_tween_factory(handler, registry) return factory(request) def test_repoze_tm_active(self): request = DummyRequest() request.environ['repoze.tm.active'] = True result = self._callFUT(request=request) self.assertEqual(result, self.response) self.assertFalse(self.txn.began) def test_tm_active(self): request = DummyRequest() request.environ['tm.active'] = True result = self._callFUT(request=request) self.assertEqual(result, self.response) self.assertFalse(self.txn.began) def test_should_activate_true(self): self.settings.update({'tm.activate_hook': 'tests.activate_true'}) result = self._callFUT() self.assertEqual(result, self.response) self.assertTrue(self.txn.began) def test_should_activate_false(self): self.settings.update({'tm.activate_hook': 'tests.activate_false'}) result = self._callFUT() self.assertEqual(result, self.response) self.assertFalse(self.txn.began) def test_handler_exception(self): def handler(request): raise NotImplementedError self.assertRaises(NotImplementedError, self._callFUT, handler=handler) self.assertTrue(self.txn.began) self.assertTrue(self.txn.aborted) self.assertFalse(self.txn.committed) def test_handler_retryable_exception_defaults_to_1(self): from transaction.interfaces import TransientError class Conflict(TransientError): pass count = [] def handler(request, count=count): raise Conflict self.txn.retryable = True self.assertRaises(Conflict, self._callFUT, handler=handler) def test_handler_isdoomed(self): txn = DummyTransaction(True) self._callFUT(txn=txn) self.assertTrue(txn.began) self.assertTrue(txn.aborted) self.assertFalse(txn.committed) def test_handler_w_native_authenticated_userid(self): self.config.testing_securitypolicy(userid='phred') self._callFUT() self.assertEqual(self.txn.user, 'phred') def test_handler_w_utf8_authenticated_userid(self): USERID = b'phred/\xd1\x80\xd0\xb5\xd1\x81'.decode('utf-8') self.config.testing_securitypolicy(userid=USERID) self._callFUT() self.assertEqual(self.txn.user, 'phred/рес') def test_handler_w_latin1_authenticated_userid(self): USERID = b'\xc4\xd6\xdc' self.config.testing_securitypolicy(userid=USERID) self._callFUT() self.assertEqual(self.txn.user, r"b'\xc4\xd6\xdc'") def test_handler_w_integer_authenticated_userid(self): # See https://github.com/Pylons/pyramid_tm/issues/28 USERID = 1234 self.config.testing_securitypolicy(userid=USERID) self._callFUT() self.assertEqual(self.txn.user, '1234') def test_disables_user_annotation(self): self.config.testing_securitypolicy(userid="nope") self.settings['tm.annotate_user'] = 'false' result = self._callFUT() self.assertEqual(self.txn.user, None) def test_handler_notes(self): self._callFUT() self.assertEqual(self.txn._note, '/') self.assertEqual(self.txn.user, None) def test_handler_notes_unicode_decode_error(self): class DummierRequest(DummyRequest): def _get_path_info(self): b"\xc0".decode("utf-8") def _set_path_info(self, val): pass path_info = property(_get_path_info, _set_path_info) request = DummierRequest() self._callFUT(request=request) self.assertEqual(self.txn._note, 'Unable to decode path as unicode') self.assertEqual(self.txn.user, None) def test_handler_notes_unicode_path(self): class DummierRequest(DummyRequest): def _get_path_info(self): return b'collection/\xd1\x80\xd0\xb5\xd1\x81'.decode('utf-8') def _set_path_info(self, val): pass path_info = property(_get_path_info, _set_path_info) request = DummierRequest() self._callFUT(request=request) self.assertEqual(self.txn._note, 'collection/рес') self.assertEqual(self.txn.user, None) def test_handler_notes_native_str_path(self): class DummierRequest(DummyRequest): def _get_path_info(self): return 'some/resource' def _set_path_info(self, val): pass path_info = property(_get_path_info, _set_path_info) request = DummierRequest() self._callFUT(request=request) self.assertEqual(self.txn._note, 'some/resource') self.assertEqual(self.txn.user, None) def test_active_flag_set_during_handler(self): result = [] def handler(request): if 'tm.active' in request.environ: result.append('active') return self.response self._callFUT(handler=handler) self.assertEqual(result, ['active']) def test_active_flag_not_set_activate_false(self): self.settings.update({'tm.activate_hook': 'tests.activate_false'}) result = [] def handler(request): if 'tm.active' not in request.environ: result.append('not active') return self.response self._callFUT(handler=handler) self.assertEqual(result, ['not active']) def test_active_flag_unset_on_egress(self): self._callFUT() self.assertTrue('tm.active' not in self.request.environ) def test_active_flag_unset_on_egress_abort(self): txn = DummyTransaction(doomed=True) self._callFUT(txn=txn) self.assertTrue('tm.active' not in self.request.environ) def test_active_flag_unset_on_egress_exception(self): def handler(request): raise NotImplementedError try: self._callFUT(handler=handler) except NotImplementedError: pass self.assertTrue('tm.active' not in self.request.environ) def test_500_without_commit_veto(self): response = DummyResponse() response.status = '500 Bad Request' def handler(request): return response result = self._callFUT(handler=handler) self.assertEqual(result, response) self.assertTrue(self.txn.began) self.assertFalse(self.txn.aborted) self.assertTrue(self.txn.committed) def test_500_with_default_commit_veto(self): settings = self.registry.settings settings['tm.commit_veto'] = 'pyramid_tm.default_commit_veto' response = DummyResponse() response.status = '500 Bad Request' def handler(request): return response result = self._callFUT(handler=handler) self.assertEqual(result, response) self.assertTrue(self.txn.began) self.assertTrue(self.txn.aborted) self.assertFalse(self.txn.committed) def test_null_commit_veto(self): response = DummyResponse() response.status = '500 Bad Request' def handler(request): return response self.settings.update({'tm.commit_veto': None}) result = self._callFUT(handler=handler) self.assertEqual(result, response) self.assertTrue(self.txn.began) self.assertFalse(self.txn.aborted) self.assertTrue(self.txn.committed) def test_commit_veto_true(self): self.settings.update({'tm.commit_veto': 'tests.veto_true'}) result = self._callFUT() self.assertEqual(result, self.response) self.assertTrue(self.txn.began) self.assertTrue(self.txn.aborted) self.assertFalse(self.txn.committed) def test_commit_veto_false(self): self.settings.update({'tm.commit_veto': 'tests.veto_false'}) result = self._callFUT() self.assertEqual(result, self.response) self.assertTrue(self.txn.began) self.assertFalse(self.txn.aborted) self.assertTrue(self.txn.committed) def test_commitonly(self): result = self._callFUT() self.assertEqual(result, self.response) self.assertTrue(self.txn.began) self.assertFalse(self.txn.aborted) self.assertTrue(self.txn.committed) def test_commit_veto_alias(self): self.settings.update({'pyramid_tm.commit_veto': 'tests.veto_true'}) result = self._callFUT() self.assertEqual(result, self.response) self.assertTrue(self.txn.began) self.assertTrue(self.txn.aborted) self.assertFalse(self.txn.committed) class Test_create_tm(unittest.TestCase): def setUp(self): self.request = DummyRequest() self.request.registry = Dummy(settings={}) # Get rid of the request.tm attribute since it shouldn't be here yet. del self.request.tm def tearDown(self): testing.tearDown() def _callFUT(self, request=None): if request is None: request = self.request from pyramid_tm import create_tm return create_tm(request) def test_default_threadlocal(self): self.assertTrue(self._callFUT() is transaction.manager) def test_overridden_manager_hook(self): txn = DummyTransaction() self.request.registry.settings["tm.manager_hook"] = lambda r: txn self.assertTrue(self._callFUT() is txn) def test_overridden_manager_environ(self): tm = transaction.TransactionManager(explicit=True) self.request.environ['tm.manager'] = tm self.assertTrue(self._callFUT() is tm) class Test_includeme(unittest.TestCase): def test_it(self): from pyramid.tweens import EXCVIEW from pyramid_tm import TMActivePredicate, create_tm, includeme config = DummyConfig() includeme(config) self.assertEqual( config.tweens, [('pyramid_tm.tm_tween_factory', None, EXCVIEW)] ) self.assertEqual(config.request_methods, [(create_tm, 'tm', True)]) self.assertEqual( config.view_predicates, [('tm_active', TMActivePredicate)] ) self.assertEqual(len(config.actions), 1) self.assertEqual(config.actions[0][0], None) self.assertEqual(config.actions[0][2], 10) def test_invalid_dotted(self): from pyramid_tm import includeme config = DummyConfig() config.registry.settings["tm.manager_hook"] = "an.invalid.import" includeme(config) self.assertRaises(ImportError, config.actions[0][1]) def test_valid_dotted(self): from pyramid_tm import includeme config = DummyConfig() config.registry.settings["tm.manager_hook"] = "tests.create_manager" includeme(config) config.actions[0][1]() self.assertTrue( config.registry.settings["tm.manager_hook"] is create_manager ) def test_it_config(self): config = testing.setUp() try: config.include('pyramid_tm') finally: testing.tearDown() def skip_if_missing(module): # pragma: no cover def wrapper(fn): try: __import__(module) except ImportError: return @functools.wraps(fn) def wrapped(*args, **kwargs): return fn(*args, **kwargs) return wrapped return wrapper def skip_if_package_lt(pkg, version): # pragma: no cover import pkg_resources def wrapper(fn): dist = pkg_resources.get_distribution(pkg) if dist.parsed_version < pkg_resources.parse_version(version): return @functools.wraps(fn) def wrapped(*args, **kwargs): return fn(*args, **kwargs) return wrapped return wrapper class TestIntegration(unittest.TestCase): def setUp(self): self.config = testing.setUp(autocommit=False) self.config.include('pyramid_tm') def tearDown(self): testing.tearDown() def _makeApp(self): app = self.config.make_wsgi_app() return webtest.TestApp(app) def test_it(self): config = self.config dm = DummyDataManager() def view(request): dm.bind(request.tm) return 'ok' config.add_view(view, name='', renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'ok') self.assertEqual(dm.action, 'commit') @skip_if_missing('pyramid_retry') def test_transient_error_is_retried(self): from transaction.interfaces import TransientError config = self.config config.add_settings({'retry.attempts': 2}) config.include('pyramid_retry') class Conflict(TransientError): pass calls = [] def view(request): dm = DummyDataManager() dm.bind(request.tm) if len(calls) < 1: calls.append('fail') raise Conflict calls.append('ok') return 'ok' config.add_view(view, renderer='string') app = self._makeApp() result = app.get('/') self.assertEqual(calls, ['fail', 'ok']) self.assertEqual(result.body, b'ok') @skip_if_missing('pyramid_retry') def test_error_is_retried_with_commit_veto_and_error_view_and_retry_data_manager( self, ): class Conflict(Exception): """This is not a Transient error.""" class RetryDataManager(DummyDataManager): """The datamanager wants the Conflict to be retried.""" def should_retry(self, exception): return isinstance(exception, Conflict) def exc_view(request): return 'failure' config = self.config config.add_settings( { 'retry.attempts': 2, 'tm.commit_veto': lambda request, response: False, } ) config.include('pyramid_retry') config.add_view(exc_view, context=Exception, renderer='string') calls = [] def view(request): print(calls) dm = RetryDataManager() dm.bind(request.tm) if len(calls) < 1: calls.append('fail') raise Conflict calls.append('ok') return 'ok' config.add_view(view, renderer='string') app = self._makeApp() result = app.get('/') self.assertEqual(calls, ['fail', 'ok']) self.assertEqual(result.body, b'ok') def test_unhandled_error_aborts(self): config = self.config dm = DummyDataManager() def view(request): dm.bind(request.tm) raise ValueError config.add_view(view) app = self._makeApp() self.assertRaises(ValueError, app.get, '/') self.assertEqual(dm.action, 'abort') def test_handled_error_aborts(self): config = self.config dm = DummyDataManager() def view(request): dm.bind(request.tm) raise ValueError config.add_view(view) def exc_view(request): return 'failure' config.add_view(exc_view, context=ValueError, renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'failure') self.assertEqual(dm.action, 'abort') def test_handled_error_commits_with_veto(self): config = self.config dm = DummyDataManager() def view(request): dm.bind(request.tm) raise ValueError config.add_view(view) def exc_view(request): return 'failure' def commit_veto(request, response): return request.exception is None config.add_settings({'tm.commit_veto': commit_veto}) config.add_view(exc_view, context=ValueError, renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'failure') self.assertEqual(dm.action, 'commit') def test_explicit_manager_fails_before_tm(self): from transaction.interfaces import NoTransaction config = self.config config.add_settings({'tm.manager_hook': 'pyramid_tm.explicit_manager'}) config.add_tween( 'tests.dummy_tween_factory', over='pyramid_tm.tm_tween_factory' ) dm = DummyDataManager() def dummy_handler(handler, request): dm.bind(request.tm) config.registry['dummy_handler'] = dummy_handler config.add_view(lambda r: r.response) app = self._makeApp() self.assertRaises(NoTransaction, app.get, '/') def test_explicit_manager_fails_after_tm(self): from transaction.interfaces import NoTransaction config = self.config config.add_settings({'tm.manager_hook': 'pyramid_tm.explicit_manager'}) config.add_tween( 'tests.dummy_tween_factory', over='pyramid_tm.tm_tween_factory' ) dm = DummyDataManager() def dummy_handler(handler, request): handler(request) dm.bind(request.tm) config.registry['dummy_handler'] = dummy_handler config.add_view(lambda r: r.response) app = self._makeApp() self.assertRaises(NoTransaction, app.get, '/') def test_explicit_manager_works_in_view(self): config = self.config dm = DummyDataManager() def view(request): dm.bind(request.tm) return 'ok' config.add_view(view, renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'ok') self.assertEqual(dm.action, 'commit') def test_tm_active_predicate_is_True(self): config = self.config dm = DummyDataManager() def true_view(request): dm.bind(request.tm) return 'ok' def false_view(request): # pragma: no cover raise RuntimeError config.add_view(true_view, tm_active=True, renderer='string') config.add_view(false_view, tm_active=False, renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'ok') self.assertEqual(dm.action, 'commit') def test_tm_active_predicate_is_False(self): config = self.config config.add_settings({'tm.activate_hook': activate_false}) def true_view(request): # pragma: no cover raise RuntimeError def false_view(request): return 'ok' config.add_view(true_view, tm_active=True, renderer='string') config.add_view(false_view, tm_active=False, renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'ok') def test_tm_active_predicate_is_bool(self): from pyramid.exceptions import ConfigurationError config = self.config try: view = lambda r: 'ok' config.add_view(view, tm_active='yes', renderer='string') config.commit() except ConfigurationError: pass else: # pragma: no cover raise AssertionError @skip_if_package_lt('pyramid', '1.7') def test_excview_rendered_after_failed_commit(self): config = self.config tm = DummyTransaction(finish_with_exc=ValueError) config.add_settings({'tm.manager_hook': lambda r: tm}) config.add_view(lambda r: 'ok', renderer='string') def exc_view(request): return 'failure' config.add_view(exc_view, context=ValueError, renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'failure') @skip_if_package_lt('pyramid', '1.7') def test_excview_rendered_after_failed_abort(self): config = self.config tm = DummyTransaction(finish_with_exc=ValueError) config.add_settings({'tm.manager_hook': lambda r: tm}) config.add_settings({'tm.commit_veto': lambda req, resp: True}) config.add_view(lambda r: 'ok', renderer='string') def exc_view(request): return 'failure' config.add_view(exc_view, context=ValueError, renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'failure') @skip_if_package_lt('pyramid', '1.7') def test_excview_rendered_after_failed_abort_from_uncaught_exc(self): config = self.config tm = DummyTransaction(finish_with_exc=ValueError) config.add_settings({'tm.manager_hook': lambda r: tm}) def view(request): raise RuntimeError config.add_view(view) def exc_view(request): return 'failure' config.add_view(exc_view, context=ValueError, renderer='string') app = self._makeApp() resp = app.get('/') self.assertEqual(resp.body, b'failure') def test_failed_commit_reraises(self): config = self.config tm = DummyTransaction(finish_with_exc=ValueError) config.add_settings({'tm.manager_hook': lambda r: tm}) config.add_view(lambda r: 'ok', renderer='string') app = self._makeApp() self.assertRaises(ValueError, lambda: app.get('/')) def test_failed_abort_reraises(self): config = self.config tm = DummyTransaction(finish_with_exc=ValueError) config.add_settings({'tm.manager_hook': lambda r: tm}) config.add_settings({'tm.commit_veto': lambda req, resp: True}) config.add_view(lambda r: 'ok', renderer='string') app = self._makeApp() self.assertRaises(ValueError, lambda: app.get('/')) class Dummy(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) class DummyTransaction(TransactionManager): began = False committed = False aborted = False _resources = [] user = None def __init__(self, doomed=False, retryable=False, finish_with_exc=None): self.doomed = doomed self.began = 0 self.committed = 0 self.aborted = 0 self.retryable = retryable self.active = False self.finish_with_exc = finish_with_exc def isRetryableError(self, exc): return self._retryable(type(exc), exc) def _retryable(self, t, v): if self.active: return self.retryable def get(self): # pragma: no cover return self def isDoomed(self): return self.doomed def begin(self): self.began += 1 self.active = True return self def commit(self): self.committed += 1 if self.finish_with_exc: raise self.finish_with_exc def abort(self): self.active = False self.aborted += 1 if self.finish_with_exc: raise self.finish_with_exc def note(self, value): self._note = value class DummyDataManager(object): action = None def bind(self, tm): self.transaction_manager = tm tm.get().join(self) def abort(self, transaction): self.action = 'abort' def tpc_begin(self, transaction): pass def commit(self, transaction): self.action = 'commit' def tpc_vote(self, transaction): pass def tpc_finish(self, transaction): pass def tpc_abort(self, transaction): # pragma: no cover pass def sortKey(self): return 'dummy:%s' % id(self) class DummyRequest(testing.DummyRequest): def __init__(self, *args, **kwargs): self.tm = TransactionManager() super(DummyRequest, self).__init__(self, *args, **kwargs) def invoke_subrequest(self, request, use_tweens): # pragma: no cover pass class DummyResponse(object): def __init__(self, status='200 OK', headers=None): self.status = status if headers is None: headers = {} self.headers = headers class DummyConfig(object): def __init__(self): self.registry = Dummy(settings={}) self.tweens = [] self.request_methods = [] self.view_predicates = [] self.actions = [] def add_tween(self, x, under=None, over=None): self.tweens.append((x, under, over)) def add_request_method(self, x, name=None, reify=None): self.request_methods.append((x, name, reify)) def add_view_predicate(self, name, obj): self.view_predicates.append((name, obj)) def action(self, x, fun, order=None): self.actions.append((x, fun, order)) pyramid_tm-2.6/tox.ini000066400000000000000000000034401471532340600150470ustar00rootroot00000000000000[tox] envlist = lint, py39,py310,py311,py312,py313,pypy3, py39-pyramid15,py39-pyramid110, py313-pyramid20, docs, coverage isolated_build = True [testenv] deps = pyramid15: pyramid <= 1.5.99 pyramid16: pyramid <= 1.6.99 pyramid17: pyramid <= 1.7.99 pyramid18: pyramid <= 1.8.99 pyramid19: pyramid <= 1.9.99 pyramid19: pyramid_retry pyramid110: pyramid <= 1.10.99 pyramid110: pyramid_retry pyramid20: pyramid <= 2.0.99 pyramid20: pyramid_retry commands = python --version pytest {posargs:} extras = testing setenv = COVERAGE_FILE=.coverage.{envname} [testenv:coverage] skip_install = True commands = coverage combine coverage xml coverage report --fail-under=100 deps = coverage setenv = COVERAGE_FILE=.coverage depends = py310-pyramid20 [testenv:lint] skip_install = True commands = isort --check-only --df src/pyramid_tm tests black --check --diff . check-manifest # flake8 src/pyramid_tm/ tests # build sdist/wheel python -m build . twine check dist/* deps = black build check-manifest isort readme_renderer twine flake8 flake8-bugbear [testenv:docs] allowlist_externals = make commands = make -C docs html epub BUILDDIR={envdir} extras = docs [testenv:format] skip_install = true commands = isort src/pyramid_tm tests black . deps = black isort [testenv:build] skip_install = true commands = # clean up build/ and dist/ folders python -c 'import shutil; shutil.rmtree("build", ignore_errors=True)' # Make sure we aren't forgetting anything check-manifest # build sdist/wheel python -m build . # Verify all is well twine check dist/* deps = build check-manifest readme_renderer twine