Full Code of uralbash/sqlalchemy_mptt for AI

master 6bb11a2292b2 cached
53 files
236.2 KB
67.8k tokens
180 symbols
1 requests
Download .txt
Showing preview only (251K chars total). Download the full file or copy to clipboard to get everything.
Repository: uralbash/sqlalchemy_mptt
Branch: master
Commit: 6bb11a2292b2
Files: 53
Total size: 236.2 KB

Directory structure:
gitextract_78avwcyy/

├── .coveragerc
├── .editorconfig
├── .flake8
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql.yml
│       ├── publish.yml
│       └── run-tests.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── .rstcheck.cfg
├── CHANGES.rst
├── CHANGES_OLD.rst
├── CONTRIBUTORS.txt
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── RELEASING.rst
├── docs/
│   ├── CONTRIBUTING.rst
│   ├── Makefile
│   ├── conf.py
│   ├── crud.rst
│   ├── index.rst
│   ├── initialize.rst
│   ├── make.bat
│   ├── sqlalchemy_mptt.rst
│   └── tut_flask.rst
├── noxfile.py
├── pyproject.toml
├── requirements-doctest.txt
├── requirements-test.txt
├── requirements.txt
├── setup.py
├── sqlalchemy_mptt/
│   ├── __init__.py
│   ├── events.py
│   ├── mixins.py
│   ├── sqlalchemy_compat.py
│   └── tests/
│       ├── __init__.py
│       ├── cases/
│       │   ├── __init__.py
│       │   ├── edit_node.py
│       │   ├── get_node.py
│       │   ├── get_tree.py
│       │   ├── initialize.py
│       │   ├── integrity.py
│       │   └── move_node.py
│       ├── fixtures/
│       │   ├── tmp_tree.json
│       │   ├── tree.json
│       │   └── tree_3.json
│       ├── test_events.py
│       ├── test_inheritance.py
│       ├── test_mixins.py
│       └── test_stateful.py
└── test.sh

================================================
FILE CONTENTS
================================================

================================================
FILE: .coveragerc
================================================
[run]
relative_files = True
[report]
omit =
    */tests/*


================================================
FILE: .editorconfig
================================================
root = true

[*]
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .flake8
================================================
[flake8]
extend-exclude = .venv
statistics = True
count = True
show-source = True
# The GitHub editor is 127 chars wide
max-line-length = 127
# Ignore valid SQLAlchemy NULL query syntax
extend-ignore = E711


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: "pip" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/codeql.yml
================================================
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL Advanced"

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]
  schedule:
    - cron: '20 16 * * 3'

jobs:
  analyze:
    name: Analyze (${{ matrix.language }})
    # Runner size impacts CodeQL analysis time. To learn more, please see:
    #   - https://gh.io/recommended-hardware-resources-for-running-codeql
    #   - https://gh.io/supported-runners-and-hardware-resources
    #   - https://gh.io/using-larger-runners (GitHub.com only)
    # Consider using larger runners or machines with greater resources for possible analysis time improvements.
    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
    permissions:
      # required for all workflows
      security-events: write

      # required to fetch internal or private CodeQL packs
      packages: read

      # only required for workflows in private repositories
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        include:
        - language: actions
          build-mode: none
        - language: python
          build-mode: none
        # CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
        # Use `c-cpp` to analyze code written in C, C++ or both
        # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
        # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
        # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
        # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
        # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
        # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    # Add any setup steps before running the `github/codeql-action/init` action.
    # This includes steps like installing compilers or runtimes (`actions/setup-node`
    # or others). This is typically only required for manual builds.
    # - name: Setup runtime (example)
    #   uses: actions/setup-example@v1

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}
        build-mode: ${{ matrix.build-mode }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.

        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
        # queries: security-extended,security-and-quality

    # If the analyze step fails for one of the languages you are analyzing with
    # "We were unable to automatically build your code", modify the matrix above
    # to set the build mode to "manual" for that language. Then modify this step
    # to build your code.
    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
    - if: matrix.build-mode == 'manual'
      shell: bash
      run: |
        echo 'If you are using a "manual" build mode for one or more of the' \
          'languages you are analyzing, replace this with the commands to build' \
          'your code, for example:'
        echo '  make bootstrap'
        echo '  make release'
        exit 1

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3
      with:
        category: "/language:${{matrix.language}}"


================================================
FILE: .github/workflows/publish.yml
================================================
# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries

name: Upload Python Package

on:
  release:
    types: [published]

permissions:
  contents: read

jobs:
  release-build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: "3.x"

      - name: Build release distributions
        run: |
          # NOTE: put your own distribution build steps here.
          python -m pip install build
          python -m build

      - name: Upload distributions
        uses: actions/upload-artifact@v4
        with:
          name: release-dists
          path: dist/

  pypi-publish:
    runs-on: ubuntu-latest
    needs:
      - release-build
    permissions:
      # IMPORTANT: this permission is mandatory for trusted publishing
      id-token: write

    # Dedicated environments with protections for publishing are strongly recommended.
    # For more information, see: https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment#deployment-protection-rules
    environment:
      name: pypi
      # OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
      # url: https://pypi.org/p/YOURPROJECT
      #
      # ALTERNATIVE: if your GitHub Release name is the PyPI project version string
      # ALTERNATIVE: exactly, uncomment the following line instead:
      url: https://pypi.org/project/sqlalchemy_mptt/${{ github.event.release.name }}

    steps:
      - name: Retrieve release distributions
        uses: actions/download-artifact@v4
        with:
          name: release-dists
          path: dist/

      - name: Publish release distributions to PyPI
        uses: pypa/gh-action-pypi-publish@release/v1
        with:
          packages-dir: dist/


================================================
FILE: .github/workflows/run-tests.yml
================================================
name: Check code and run tests

on:
  push:
    branches: [ "master" ]
  pull_request:
    branches: [ "master" ]

permissions:
  contents: read

jobs:
  generate-jobs:
    name: Generate build matrix for tests
    runs-on: ubuntu-latest
    outputs:
      session: ${{ steps.set-matrix.outputs.session }}
    steps:
    - uses: actions/checkout@v4
    - uses: astral-sh/setup-uv@v6
    - name: Generate build matrix
      id: set-matrix
      shell: bash
      run: echo session=$(uv run noxfile.py --json -l | jq -c '[.[].session]') | tee --append $GITHUB_OUTPUT
  checks:
    name: Run ${{ matrix.session }}
    needs: [generate-jobs]
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        session: ${{ fromJson(needs.generate-jobs.outputs.session) }}
    steps:
    - uses: actions/checkout@v4
    - uses: astral-sh/setup-uv@v6
    - name: Run nox
      run: >
        uv run noxfile.py -s "${{ matrix.session }}"
        -- --pyargs sqlalchemy_mptt --cov-report xml
    - name: Upload coverage data
      if: ${{ startsWith(matrix.session, 'test(') }}
      uses: coverallsapp/github-action@v2
      with:
        flag-name: run-${{ join(matrix.*, '-') }}
        parallel: true
        debug: true

  coveralls_finish:
    name: Finalize coverage
    needs: checks
    runs-on: ubuntu-latest
    steps:
    - uses: coverallsapp/github-action@v2
      with:
        parallel-finished: true


================================================
FILE: .gitignore
================================================
uv.lock
.eggs
.env
*~
*.swo
*.swp
.settings
.project
.pydevproject
sqlalchemy_mptt/.coverage
sqlalchemy_mptt/TODO
*.pyc
sqlalchemy_mptt/cover
cover
.coverage
build
dist
*.egg-info
example
TODO
TODO.txt
nohup.out
.cache
.tox
__pycache__
_build
.ropeproject
_themes
.aider.*
.vscode/
coverage.xml
.hypothesis/
.nox
.pytest_cache
.venv


================================================
FILE: .pre-commit-config.yaml
================================================
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
        exclude: '^.*\.svg$'
    -   id: check-yaml
    -   id: check-toml
    -   id: check-added-large-files
-   repo: https://github.com/pycqa/flake8
    rev: '7.3.0'
    hooks:
    -   id: flake8
-   repo: https://github.com/pappasam/toml-sort
    rev: 'v0.24.2'
    hooks:
    -   id: toml-sort
-   repo: https://github.com/pycqa/isort
    rev: '7.0.0'
    hooks:
    -   id: isort
        name: isort (python)


================================================
FILE: .readthedocs.yaml
================================================
# Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details

# Required
version: 2

# Set the OS, Python version and other tools you might need
build:
  os: ubuntu-22.04
  tools:
    python: "3.12"
    # You can also specify other tool versions:
    # nodejs: "20"
    # rust: "1.70"
    # golang: "1.20"

# Build documentation in the "docs/" directory with Sphinx
sphinx:
  configuration: docs/conf.py
  # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs
  # builder: "dirhtml"
  # Fail on all warnings to avoid broken references
  # fail_on_warning: true

# Optionally build your docs in additional formats such as PDF and ePub
# formats:
#   - pdf
#   - epub

# Optional but recommended, declare the Python requirements required
# to build your documentation
# See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
#python:
#  install:
#    - requirements: requirements.txt
python:
  install:
    - method: pip
      path: .


================================================
FILE: .rstcheck.cfg
================================================
[rstcheck]
ignore_directives=automodule,autofunction,autoclass,code-block
ignore_roles=mod,py:mod


================================================
FILE: CHANGES.rst
================================================
Versions releases 0.2.x & above
###############################

0.6.0 (2025-11-29)
==================

see issues #109, #111, #112 & #113

- Add support for SQLAlchemy 2.0.
- Add official support for Python 3.12, 3.13 and 3.14.
- Remove examples of defunct features from the documentation.

0.5.0 (2025-11-18)
==================

see issues #104 & #110

- Add support for SQLAlchemy 1.4.
- Drop official support for PyPy.
- Simplify memory management by using ``weakref.WeakSet`` instead of rolling our own
  weak reference set.
- Unify ``after_flush_postexec`` execution path for CPython & PyPy.
- Simplify ``get_siblings``.
- Run doctest on all code snippets in the documentation.
- Fix some of the incorrect documentation snippets.

0.4.0 (2025-05-30)
==================

see issue #97

- Support for Python 3.10 and 3.11
- Dropped support for Python 3.7

0.3.0 (2025-05-10)
==================

see issues #63, #87, #89 & #90

- Support for joined-table inheritance
- Restrict to Python & PyPy 3.7 - 3.9
- Restrict to SQLA 1.0 - 1.3
- Fixes race condition with garbage collection on PyPy versions

0.2.5 (2019-07-23)
==================

see issue #64

- Added similar `django_mptt` methods `get_siblings` and `get_children`

0.2.4 (2018-12-14)
==================

see PR #61

- Allow to specify ordering of path_to_root

0.2.3 (2018-06-03)
==================

see issue #57

- Fix rebuild tree
- Added support node's identifier start from 0

0.2.2 (2017-10-05)
==================

see issue #56

- Added custom default root level. Support Django style level=0

0.2.1 (2016-01-23)
==================

see PR #51

- fix of index columns names

0.2.0 (2015-11-13)
==================

see PR #50

- Changed ``parent_id`` to dynamically match the type of the primary_key
- exposed drilldown_tree's logic and path_to_root's logic as both instance and
  class level method


================================================
FILE: CHANGES_OLD.rst
================================================
Versions releases 0.1.x
#######################

0.1.9 (2015-09-24)
==================

- add option ``remove`` to ``sqlalchemy.events.TreesManager.register_mapper``

0.1.8 (2015-09-14)
==================

- add method ``_base_query_obj``

0.1.7 (2015-08-18)
==================

- add method ``path_to_root`` (see #46)
- add data integrity tests

0.1.6 (2015-07-03)
==================

- fix bug with ``get_tree`` when no rows in database.

0.1.5 (2015-06-25)
==================

- Add drilldown_tree method (see #41)
- Add custom ``query`` atribute to ``get_tree`` method

0.1.4 (2015-06-19)
==================

- delete method ``get_pk_with_class_name``

Bug Fixes
---------

- fix ``_get_tree_table`` function for inheritance models

0.1.3 (2015-06-17)
==================

- Add test for swap trees
- rename ``get_pk`` method to ``get_pk_name``
- rename ``get_db_pk`` method to ``get_pk_column``
- rename ``get_class_pk`` method to ``get_pk_with_class_name``

Bug Fixes
---------

- Fix order of elements in tree

0.1.2 (2015-04-22)
==================

Bug Fixes
---------

- Fix MANIFEST.in file

Deprecation
-----------

- Delete ``BaseNestedSets.register_tree`` method
- Delete ``BaseNestedSets.get_tree_recursively`` method

0.1.1 (2015-04-21)
==================

Features
--------

- Add test for rst docs and migrate on new itcase_sphinx_theme (#40)

Bug Fixes
---------

- Remove recursion from BaseNestedSets.get_tree method (#39)

0.1.0 (2014-11-18)
==================

Bug Fixes
---------

- Fix concurrency issue with multiple session (#36)
- Flushing the session now expire the instance and it's children (#33)

0.0.9 (2014-10-09)
==================

- Add MANIFEST.in
- New docs
- fixes in setup.py

0.0.8 (2014-08-15)
==================

- Add CONTRIBUTORS.txt

Features
--------

- Automatically register tree classes enhancement (#28)
- Added support polymorphic tree models (#24)

Bug Fixes
---------

- Fix expire left/right attributes of parent somewhen after the `before_insert` event (#30)
- Fix tree_id is incorrectly set to an existing tree if no parent is set (#23)
- Fix package is not installable if sqlalchemy is not (yet) installed (#22)

0.0.7 (2014-08-04)
==================

- Add LICENSE.txt

Bug Fixes
---------

- fix get_db_pk function


0.0.6 (2014-07-31)
==================

Bug Fixes
---------

-  Allow the primary key to not be named "id" #20. See https://github.com/uralbash/sqlalchemy_mptt/issues/20


================================================
FILE: CONTRIBUTORS.txt
================================================
Contributors
------------

- Dmitry Svintsov (uralbash), 2014/04/16
- Jonathan Stoppani, 2014/08/11
- Fayaz Yusuf Khan, 2014/10/10
- Greg Klupar, 2015/11/13
- Jiri Kuncar, 2016/01/20
- Sylvain Thénault (sthenault), 2018/06/03
- Musharraf (mush42), 2019/02/12
- Tim Gates (timgates42), 2020/03/27


================================================
FILE: LICENSE.txt
================================================
The MIT License (MIT)

Copyright (c) 2013 uralbash

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.


================================================
FILE: MANIFEST.in
================================================
include requirements.txt
include requirements-test.txt
include README.rst CHANGES.rst


================================================
FILE: README.rst
================================================
|PyPI Version| |PyPI Downloads| |PyPI Python Versions|
|Build Status| |Coverage Status|

Library for implementing Modified Preorder Tree Traversal with your
SQLAlchemy Models and working with trees of Model instances, like
django-mptt. Docs http://sqlalchemy-mptt.readthedocs.io/

.. image:: https://cdn.rawgit.com/uralbash/sqlalchemy_mptt/master/docs/img/2_sqlalchemy_mptt_traversal.svg
   :alt: Nested sets traversal
   :width: 800px

The nested set model is a particular technique for representing nested
sets (also known as trees or hierarchies) in relational databases.

Installing
----------

Install from github:

.. code-block:: bash

    pip install git+http://github.com/uralbash/sqlalchemy_mptt.git

PyPi:

.. code-block:: bash

    pip install sqlalchemy_mptt

Source:

.. code-block:: bash

    pip install -e .

Usage
-----

Add mixin to model

.. code-block:: python

    from sqlalchemy import Column, Integer, Boolean
    from sqlalchemy.ext.declarative import declarative_base

    from sqlalchemy_mptt.mixins import BaseNestedSets

    Base = declarative_base()


    class Tree(Base, BaseNestedSets):
        __tablename__ = "tree"

        id = Column(Integer, primary_key=True)
        visible = Column(Boolean)

        def __repr__(self):
            return "<Node (%s)>" % self.id

Now you can add, move and delete obj!

Insert node
-----------

.. code-block:: python

    node = Tree(parent_id=6)
    session.add(node)

::

            level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

            level     Insert node with parent_id == 6
            1                    1(1)24
                    _______________|_________________
                   |               |                 |
            2    2(2)5           6(4)13           14(7)23
                   |           ____|____          ___|____
                   |          |         |        |        |
            3    3(3)4      7(5)8    9(6)12  15(8)18   19(10)22
                                       |        |         |
            4                      10(23)11  16(9)17  20(11)21

Delete node
-----------

.. code:: python

    node = session.query(Tree).filter(Tree.id == 4).one()
    session.delete(node)

::

            level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

            level         Delete node == 4
            1                    1(1)16
                    _______________|_____
                   |                     |
            2    2(2)5                 6(7)15
                   |                     ^
            3    3(3)4            7(8)10   11(10)14
                                    |          |
            4                     8(9)9    12(11)13

Update node
-----------

.. code:: python

    node = session.query(Tree).filter(Tree.id == 8).one()
    node.parent_id = 5
    session.add(node)

::

            level           Nested sets example
                1                    1(1)22
                        _______________|___________________
                       |               |                   |
                2    2(2)5           6(4)11             12(7)21
                       |               ^                   ^
                3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                      |          |
                4                                  14(9)15   18(11)19

            level               Move 8 - > 5
                1                     1(1)22
                         _______________|__________________
                        |               |                  |
                2     2(2)5           6(4)15            16(7)21
                        |               ^                  |
                3     3(3)4      7(5)12   13(6)14      17(10)20
                                   |                        |
                4                8(8)11                18(11)19
                                   |
                5                9(9)10

Move node (support multitree)
-----------------------------

.. figure:: https://cdn.rawgit.com/uralbash/sqlalchemy_mptt/master/docs/img/3_sqlalchemy_mptt_multitree.svg
   :alt: Nested sets multitree
   :width: 600px

   Nested sets multitree

Move inside

.. code:: python

    node = session.query(Tree).filter(Tree.id == 4).one()
    node.move_inside("15")

::

                     4 -> 15
            level           Nested sets tree1
            1                    1(1)16
                    _______________|_____________________
                   |                                     |
            2    2(2)5                                 6(7)15
                   |                                     ^
            3    3(3)4                            7(8)10   11(10)14
                                                    |          |
            4                                     8(9)9    12(11)13

            level           Nested sets tree2
            1                     1(12)28
                     ________________|_______________________
                    |                |                       |
            2    2(13)5            6(15)17                18(18)27
                   |                 ^                        ^
            3    3(14)4    7(4)12 13(16)14  15(17)16  19(19)22  23(21)26
                             ^                            |         |
            4          8(5)9  10(6)11                 20(20)21  24(22)25

Move after

.. code:: python

    node = session.query(Tree).filter(Tree.id == 8).one()
    node.move_after("5")

::

           level           Nested sets example
                1                    1(1)22
                        _______________|___________________
                       |               |                   |
                2    2(2)5           6(4)11             12(7)21
                       |               ^                   ^
                3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                      |          |
                4                                  14(9)15   18(11)19

            level               Move 8 after 5
                1                     1(1)22
                         _______________|__________________
                        |               |                  |
                2     2(2)5           6(4)15            16(7)21
                        |               ^                  |
                3     3(3)4    7(5)8  9(8)12  13(6)14   17(10)20
                                        |                  |
                4                    10(9)11            18(11)19

Move to top level

.. code:: python

    node = session.query(Tree).filter(Tree.id == 15).one()
    node.move_after("1")

::

            level           tree_id = 1
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

            level           tree_id = 2
            1                     1(15)6
                                     ^
            2                 2(16)3   4(17)5

            level           tree_id = 3
            1                    1(12)16
                     _______________|
                    |               |
            2    2(13)5          6(18)15
                    |               ^
            3    3(14)4     7(19)10   11(21)14
                               |          |
            4               8(20)9    12(22)13

Support and Development
=======================

To report bugs, use the `issue tracker
<https://github.com/uralbash/sqlalchemy_mptt/issues>`_.

We welcome any contribution: suggestions, ideas, commits with new
futures, bug fixes, refactoring, docs, tests, translations, etc...

If you have any questions:

* Use the `Discussion board <https://github.com/uralbash/sqlalchemy_mptt/discussions>`_
* Contact the maintainer via email: fayaz.yusuf.khan@gmail.com
* Contact the author via email: sacrud@uralbash.ru or #sacrud IRC channel |IRC Freenode|

Refer the detailed contribution guide in the `docs <https://sqlalchemy-mptt.readthedocs.io/CONTRIBUTING.html>`_
for more information on setting up the development environment, running tests, and contributing to the project.

License
=======

The project is licensed under the MIT license.

.. |PyPI Version| image:: https://img.shields.io/pypi/v/sqlalchemy_mptt
   :alt: PyPI - Version
.. |PyPI Downloads| image:: https://img.shields.io/pypi/dm/sqlalchemy_mptt
   :alt: PyPI - Downloads
.. |PyPI Python Versions| image:: https://img.shields.io/pypi/pyversions/sqlalchemy_mptt
   :alt: PyPI - Python Version
.. |Build Status| image:: https://github.com/uralbash/sqlalchemy_mptt/actions/workflows/run-tests.yml/badge.svg?branch=master
   :target: https://github.com/uralbash/sqlalchemy_mptt/actions/workflows/run-tests.yml
.. |Coverage Status| image:: https://coveralls.io/repos/uralbash/sqlalchemy_mptt/badge.png
   :target: https://coveralls.io/r/uralbash/sqlalchemy_mptt
.. |IRC Freenode| image:: https://img.shields.io/badge/irc-freenode-blue.svg
   :target: https://webchat.freenode.net/?channels=sacrud


================================================
FILE: RELEASING.rst
================================================
Releasing
=========

1. Merge all intended and verified pull requests into the ``master`` branch.
2. Create a local build and test:
    - Run ``uv run noxfile.py -s build`` to create a source distribution and a wheel.
    - Install both artifacts in a fresh virtual environment to ensure they work correctly.
3. Bump the version number in ``setup.py``. (May be included in the pull request.)
4. Update the changelog in ``CHANGES.rst``.
5. Add new contributors to the ``CONTRIBUTORS.rst`` file.
6. Update the release date in ``CHANGES.rst``.
7. Ensure the latest build passes on GitHub Actions.
8. Rebuild the documentation and check that it looks correct.
9. Create a new release on GitHub:
   - Use the version number as the tag.
   - Include the changelog in the release notes.
10. Ensure the release is published.
11. Test the release by installing it in a fresh virtual environment.


================================================
FILE: docs/CONTRIBUTING.rst
================================================
Contribution Guidelines
=======================

All types of contributions are welcome: suggestions, ideas, commits
with new features, bug fixes, refactoring, docs, tests, translations, etc...

If you have any questions:

* Use the `Discussion board <https://github.com/uralbash/sqlalchemy_mptt/discussions>`_
* Contact the maintainer via email: fayaz.yusuf.khan@gmail.com
* Contact the author via email: sacrud@uralbash.ru or #sacrud IRC channel |IRC Freenode|

Development Setup
-----------------

To set up the development environment, you can run:

.. code-block:: bash

    # Install uv
    $ pip install uv
    # Run the noxfile.py script
    $ uv run noxfile.py -s dev

By default, this will create a virtual environment with Python 3.8 and install all the required dependencies.
If you need to setup the development environment with a specific Python version, you can run:

.. code-block:: bash

    $ uv run noxfile.py -s dev -P 3.10

Running Tests
-------------

To run the tests and linters, you can use the following command:

.. code-block:: bash

    $ uv run noxfile.py

For futher details, refer to the ``noxfile.py`` script.

Building Documentation
----------------------

The documentation on `ReadtheDocs <https://app.readthedocs.org/projects/sqlalchemy-mptt/>`_ is manually built from the master branch.
To build the documentation locally, you can run:

.. code-block:: bash

    $ uv tool install sphinx --with-editable . --with-requirements requirements-doctest.txt
    $ cd docs
    $ make html

For futher details, refer to the ``docs/Makefile``.

.. |IRC Freenode| image:: https://img.shields.io/badge/irc-freenode-blue.svg
   :target: https://webchat.freenode.net/?channels=sacrud


================================================
FILE: docs/Makefile
================================================
# 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 <target>' where <target> 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/sqlalchemy_mptt.qhcp"
	@echo "To view the help file:"
	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/sqlalchemy_mptt.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/sqlalchemy_mptt"
	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/sqlalchemy_mptt"
	@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."


================================================
FILE: docs/conf.py
================================================
# -*- coding: utf-8 -*-
#
# sqlalchemy_mptt documentation build configuration file, created by
# sphinx-quickstart on Wed Jun 25 14:00:12 2014.
#
# 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.
from datetime import date
import os
import sys

from docutils.parsers.rst import directives
from sphinx.directives.code import CodeBlock

directives.register_directive('no-code-block', CodeBlock)

sys.path.insert(0, os.path.abspath("."))
sys.path.insert(0, os.path.abspath("../"))

# -- 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.viewcode',
    'sphinx.ext.mathjax',
    'sphinx.ext.doctest',
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix of source filenames.
source_suffix = '.rst'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = 'sqlalchemy_mptt'
copyright = '{}, uralbash'.format(date.today().year)

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'

# -- Options for HTML output ----------------------------------------------

# 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']

# Output file base name for HTML help builder.
htmlhelp_basename = 'sqlalchemy_mpttdoc'

html_theme_options = {
    'github_button': True,
    'github_user': 'uralbash',
    'github_repo': 'sqlalchemy_mptt',
}

# -- Options for doctest extension ------------------------------------------
doctest_global_setup = """
from sqlalchemy import create_engine, Column, Integer, Boolean
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import Session

from sqlalchemy_mptt import tree_manager
from sqlalchemy_mptt.mixins import BaseNestedSets
"""
doctest_global_cleanup = """
try:
    session.flush()
except NameError:
    pass
"""


================================================
FILE: docs/crud.rst
================================================
Usage
=====

INSERT
------

Insert node with parent_id==6

.. testsetup::

    Base = declarative_base()
    engine = create_engine("sqlite:///:memory:")
    session = Session(engine)

    class Tree(Base, BaseNestedSets):
        __tablename__ = "tree"

        id = Column(Integer, primary_key=True)
        visible = Column(Boolean)

        def __repr__(self):
            return "<Node (%s)>" % self.id

    Base.metadata.create_all(engine)
    tree_manager.register_events(remove=True)
    instances = [
        Tree(id=1, parent_id=None),
        Tree(id=2, parent_id=1),
        Tree(id=3, parent_id=2),
        Tree(id=4, parent_id=1),
        Tree(id=5, parent_id=4),
        Tree(id=6, parent_id=4),
        Tree(id=7, parent_id=1),
        Tree(id=8, parent_id=7),
        Tree(id=9, parent_id=8),
        Tree(id=10, parent_id=7),
        Tree(id=11, parent_id=10)
    ]
    for instance in instances:
        instance.left = 0
        instance.right = 0
        instance.visible = True
    session.add_all(instances)
    session.flush()
    tree_manager.register_events()
    Tree.rebuild_tree(session, tree_id=None)


.. testcode::

    node = Tree(parent_id=6)
    session.add(node)

Tree state before insert

.. code::

    level           Before INSERT
    1                    1(1)22
            _______________|___________________
           |               |                   |
    2    2(2)5           6(4)11             12(7)21
           |               ^                   ^
    3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                          |          |
    4                                  14(9)15   18(11)19

After insert

.. code::

    level           After INSERT
    1                    1(1)24
            _______________|_________________
           |               |                 |
    2    2(2)5           6(4)13           14(7)23
           |           ____|___          ____|____
           |          |        |        |         |
    3    3(3)4      7(5)8    9(6)12  15(8)18   19(10)22
                               |        |         |
    4                      10(23)11  16(9)17   20(11)21

UPDATE
------

Set parent_id=5 for node with id==8

.. testcode::

    node = session.query(Tree).filter(Tree.id == 8).one()
    node.parent_id = 5
    session.add(node)

Tree state before update

.. code::

    level           Before UPDATE
    1                    1(1)22
            _______________|___________________
           |               |                   |
    2    2(2)5           6(4)11             12(7)21
           |               ^                   ^
    3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                          |          |
    4                                  14(9)15   18(11)19

After update

.. code::

    level               Move 8 - > 5
        1                     1(1)22
                 _______________|__________________
                |               |                  |
        2     2(2)5           6(4)15            16(7)21
                |               ^                  |
        3     3(3)4      7(5)12   13(6)14      17(10)20
                           |                       |
        4                8(8)11                18(11)19
                           |
        5                9(9)10

DELETE
------

Delete node with id==4

.. testcode::

    node = session.query(Tree).filter(Tree.id == 4).one()
    session.delete(node)

Tree state before delete

.. code::

    level           Before DELETE
    1                    1(1)22
            _______________|___________________
           |               |                   |
    2    2(2)5           6(4)11             12(7)21
           |               ^                   ^
    3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                          |          |
    4                                  14(9)15   18(11)19

After delete

.. code::

    level         Delete node == 4
    1                    1(1)16
            _______________|_____
           |                     |
    2    2(2)5                 6(7)15
           |                     ^
    3    3(3)4            7(8)10   11(10)14
                            |          |
    4                     8(9)9    12(11)13

For more example see :mod:`sqlalchemy_mptt.tests.TestTree`


================================================
FILE: docs/index.rst
================================================
.. sqlalchemy_mptt documentation master file, created by
   sphinx-quickstart on Wed Jun 25 14:00:12 2014.
   You can adapt this file completely to your liking, but it should at least
   contain the root `toctree` directive.

sqlalchemy_mptt
===============

.. image:: _static/mptt_insert.jpg
    :alt: MPTT (nested sets) INSERT

Library for implementing Modified Preorder Tree Traversal with your
`SQLAlchemy` Models and working with trees of Model instances, like
`django-mptt`.
The nested set model is a particular technique for representing nested
sets (also known as trees or hierarchies) in relational databases.

Where used
----------

* ps_tree_
* pyramid_pages_
* your project ...

Manual
------

.. toctree::

    initialize.rst
    crud.rst

API:
----

.. toctree::
   :maxdepth: 2

   sqlalchemy_mptt

Tutorial
--------

.. toctree::

    tut_flask.rst

A lot of examples and logic in
:py:mod:`sqlalchemy_mptt.tests.cases`

Support and Development
=======================

.. toctree::

    CONTRIBUTING.rst

Changelog
=========

.. toctree::
    :maxdepth: 1

    CHANGES.rst
    CHANGES_OLD.rst

Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

.. _ps_tree: https://github.com/sacrud/ps_tree
.. _pyramid_pages: https://github.com/uralbash/pyramid_pages


================================================
FILE: docs/initialize.rst
================================================
Setup
=====

Create model with MPTT mixin:

.. testcode::

    from sqlalchemy import Column, Integer, Boolean
    from sqlalchemy.ext.declarative import declarative_base

    from sqlalchemy_mptt.mixins import BaseNestedSets

    Base = declarative_base()


    class Tree(Base, BaseNestedSets):
        __tablename__ = "tree"

        id = Column(Integer, primary_key=True)
        visible = Column(Boolean)  # you custom field

        def __repr__(self):
            return "<Node (%s)>" % self.id



Session factory wrapper
-----------------------

For the automatic tree maintainance triggered after session flush to work
correctly, wrap the Session factory with :mod:`sqlalchemy_mptt.mptt_sessionmaker`

.. testcode::

    from sqlalchemy import create_engine
    from sqlalchemy.orm import sessionmaker
    from sqlalchemy_mptt import mptt_sessionmaker

    engine = create_engine('sqlite:///:memory:')
    Session = mptt_sessionmaker(sessionmaker(bind=engine))

Using session factory wrapper with flask_sqlalchemy
---------------------------------------------------

If you use Flask and SQLAlchemy, you probably use also flask_sqlalchemy
extension for integration. In that case the Session creation is not directly
accessible. The following allows you to use the wrapper:

.. testcode::

    from sqlalchemy_mptt import mptt_sessionmaker
    from flask_sqlalchemy import SQLAlchemy

    # instead of creating db object directly
    db = SQLAlchemy()

    # subclass the db manager and insert the wrapper at session creation
    class CustomSQLAlchemy(SQLAlchemy):
        """A custom SQLAlchemy manager, to have control on session creation"""

        def create_session(self, options):
            """Override the original session factory creation"""
            Session = super().create_session(options)
            # Use wrapper from sqlalchemy_mptt that manage tree tables
            return mptt_sessionmaker(Session)

    # then
    db = CustomSQLAlchemy()


Events
------

The tree manager automatically registers events. But you can do it manually:

.. testcode::

   from sqlalchemy_mptt import tree_manager

   tree_manager.register_events()  # register events before_insert,
                                   # before_update and before_delete

Or disable events if it required:

.. testcode::

   from sqlalchemy_mptt import tree_manager

   tree_manager.register_events(remove=True)  # remove events before_insert,
                                              # before_update and before_delete

Data structure
--------------

Fill table with records, for example, as shown in the picture

.. image:: img/2_sqlalchemy_mptt_traversal.svg
    :width: 500px
    :alt: SQLAlchemy MPTT (nested sets)

Represented data of tree like dict

.. testcode::

    tree = (
        {'id':  '1',                  'parent_id': None},

        {'id':  '2', 'visible': True, 'parent_id':  '1'},
        {'id':  '3', 'visible': True, 'parent_id':  '2'},

        {'id':  '4', 'visible': True, 'parent_id':  '1'},
        {'id':  '5', 'visible': True, 'parent_id':  '4'},
        {'id':  '6', 'visible': True, 'parent_id':  '4'},

        {'id':  '7', 'visible': True, 'parent_id':  '1'},
        {'id':  '8', 'visible': True, 'parent_id':  '7'},
        {'id':  '9',                  'parent_id':  '8'},
        {'id': '10',                  'parent_id':  '7'},
        {'id': '11',                  'parent_id': '10'},
    )

Initializing a tree with data
-----------------------------

When you add nodes to the table, the tree manager subsequently updates the
level, left and right attributes in the reset of the table. This is done very
quickly if the tree already exists in the database, but for initializing the
tree, it might become a big overhead. In this case, it is recommended to
deactivate automatic tree management, fill in the data, reactivate automatic
tree management and finally call manually a rebuild of the tree once at the end:

.. testcode::
    :hide:

    # This is some more setup code.
    from flask import Flask

    class MyModelTree(db.Model, BaseNestedSets):
        __tablename__ = "my_model_tree"

        id = db.Column(db.Integer, primary_key=True)
        visible = db.Column(db.Boolean)  # you custom field

        def __repr__(self):
            return "<Node (%s)>" % self.id

    app = Flask('test')
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    db.init_app(app)
    app.app_context().push()
    db.create_all()
    items = [MyModelTree(**data) for data in tree]

.. testcode::

    from sqlalchemy_mptt import tree_manager

    ...

    tree_manager.register_events(remove=True) # Disable MPTT events

    # Fill tree
    for item in items:
        item.left = 0
        item.right = 0
        item.tree_id = 1
        db.session.add(item)
    db.session.commit()

    ...

    tree_manager.register_events() # enabled MPTT events back
    MyModelTree.rebuild_tree(db.session, 1) # rebuild lft, rgt value automatically

After an initial table with tree you can use mptt features.


================================================
FILE: docs/make.bat
================================================
@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 ^<target^>` where ^<target^> 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\sqlalchemy_mptt.qhcp
	echo.To view the help file:
	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\sqlalchemy_mptt.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


================================================
FILE: docs/sqlalchemy_mptt.rst
================================================
:mod:`sqlalchemy_mptt` package
==============================

Events
------

Base events
~~~~~~~~~~~

.. automodule:: sqlalchemy_mptt.events

    .. autofunction:: mptt_before_insert
    .. autofunction:: mptt_before_delete
    .. autofunction:: mptt_before_update
    .. autoclass:: TreesManager

Hidden method
~~~~~~~~~~~~~

    .. autofunction:: _insert_subtree

Mixins
------

.. automodule:: sqlalchemy_mptt.mixins

    .. autoclass:: BaseNestedSets
        :members:

        .. automethod:: tree_id
        .. attribute:: parent_id
        .. attribute:: parent
        .. automethod:: left
        .. automethod:: right
        .. automethod:: level

Tests
-----

.. automodule:: sqlalchemy_mptt.tests.test_events
    :members:
    :undoc-members:
    :show-inheritance:

.. automodule:: sqlalchemy_mptt.tests.test_inheritance
    :members:
    :undoc-members:
    :show-inheritance:

.. automodule:: sqlalchemy_mptt.tests.test_mixins
    :members:
    :undoc-members:
    :show-inheritance:

Cases tests
~~~~~~~~~~~

.. automodule:: sqlalchemy_mptt.tests.cases.edit_node
    :members:
    :undoc-members:
    :show-inheritance:

.. automodule:: sqlalchemy_mptt.tests.cases.get_node
    :members:
    :undoc-members:
    :show-inheritance:

.. automodule:: sqlalchemy_mptt.tests.cases.get_tree
    :members:
    :undoc-members:
    :show-inheritance:

.. automodule:: sqlalchemy_mptt.tests.cases.initialize
    :members:
    :undoc-members:
    :show-inheritance:

.. automodule:: sqlalchemy_mptt.tests.cases.integrity
    :members:
    :undoc-members:
    :show-inheritance:

.. automodule:: sqlalchemy_mptt.tests.cases.move_node
    :members:
    :undoc-members:
    :show-inheritance:


================================================
FILE: docs/tut_flask.rst
================================================
Usage with Flask-SQLAlchemy
===========================

Initialize Flask app and sqlalchemy

.. testsetup::

    __name__ = "__main__"

.. testcode::

    from pprint import pprint
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy

    from sqlalchemy_mptt.mixins import BaseNestedSets

    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///:memory:'
    db = SQLAlchemy(app)

Make models.

.. testcode::

    class Category(db.Model, BaseNestedSets):
        __tablename__ = 'categories'
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(400), index=True, unique=True)
        items = db.relationship("Product", backref='item', lazy='dynamic')

        def __repr__(self):
            return '<Category {}>'.format(self.name)


    class Product(db.Model):
        __tablename__ = 'products'
        id = db.Column(db.Integer, primary_key=True)
        category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
        name = db.Column(db.String(475), index=True)

Represent data of tree in table
-------------------------------

Add data to table with tree.

.. testcode::
    :hide:

    # This is some more setup code.
    app.app_context().push()

.. testcode::

    db.session.add(Category(name="root"))  # root node
    db.session.add_all(  # first branch of tree
        [
            Category(name="foo", parent_id=1),
            Category(name="bar", parent_id=2),
            Category(name="baz", parent_id=3),
        ]
    )
    db.session.add_all(  # second branch of tree
        [
            Category(name="foo1", parent_id=1),
            Category(name="bar1", parent_id=5),
            Category(name="baz1", parent_id=5),
        ]
    )

    db.drop_all()
    db.create_all()
    db.session.commit()

The database entries are added:

.. code-block:: text

    "id"  "name"  "lft"  "rgt"  "level"  "parent_id"  "tree_id"
    1     "root"  1      14     1        1
    2     "foo"   2      7      2        1            1
    3     "bar"   3      6      3        2            1
    4     "baz"   4      5      4        3            1
    5     "foo1"  8      13     2        1            1
    6     "bar1"  9      10     3        5            1
    7     "baz1"  11     12     3        5            1


``Lft`` of root element every time :math:`1`.

:math:`root_{lft} = 1`

``Rgt`` of root element always equal 2 * quantity of tree nodes.

:math:`root_{rgt} = 2 * | P |`

:math:`root_{rgt} = 2 * 7 = 14`

The tree that displays the records in the database is represented schematically
below:

.. code-block:: text

   level
     1                  1(root)14
                            |
                   ---------------------
                   |                   |
     2          2(foo)7             8(foo1)13
                   |               /         \
     3          3(bar)6        9(bar1)10   11(baz1)12
                   |
     4          4(baz)5

Drilldown
---------

Drilldown tree for a given node.

A drilldown tree consists of a node’s ancestors, itself and its immediate
children. For example, a drilldown tree for a ``foo1`` category might look
something like:

.. code-block:: text

   Drilldown for foo1 node

   level
     1                  1(root)14
                            |
                   ---------------------
                   |         ----------|---------------
     2          2(foo)7      |      8(foo1)13         |
                   |         |     /         \        |
     3          3(bar)6      | 9(bar1)10   11(baz1)12 |
                   |         --------------------------
     4          4(baz)5

.. testcode::

    categories = Category.query.all()

    for item in categories:
        print(item)
        pprint(item.drilldown_tree())
        print()

.. testoutput::
   :options: +NORMALIZE_WHITESPACE

    <Category root>
    [{'children': [{'children': [{'children': [{'node': <Category baz>}],
                                  'node': <Category bar>}],
                    'node': <Category foo>},
                   {'children': [{'node': <Category bar1>},
                                 {'node': <Category baz1>}],
                    'node': <Category foo1>}],
      'node': <Category root>}]

    <Category foo>
    [{'children': [{'children': [{'node': <Category baz>}],
                    'node': <Category bar>}],
      'node': <Category foo>}]

    <Category bar>
    [{'children': [{'node': <Category baz>}], 'node': <Category bar>}]

    <Category baz>
    [{'node': <Category baz>}]

    <Category foo1>
    [{'children': [{'node': <Category bar1>}, {'node': <Category baz1>}],
      'node': <Category foo1>}]

    <Category bar1>
    [{'node': <Category bar1>}]

    <Category baz1>
    [{'node': <Category baz1>}]

Represent it to JSON format:

.. testcode::

   def cat_to_json(item):
       return {
           'id': item.id,
           'name': item.name
       }

   for item in categories:
       pprint(item.drilldown_tree(json=True, json_fields=cat_to_json))
       print()

.. testoutput::
    :options: +NORMALIZE_WHITESPACE

    [{'children': [{'children': [{'children': [{'id': 4,
                                                'label': '<Category baz>',
                                                'name': 'baz'}],
                                  'id': 3,
                                  'label': '<Category bar>',
                                  'name': 'bar'}],
                    'id': 2,
                    'label': '<Category foo>',
                    'name': 'foo'},
                   {'children': [{'id': 6,
                                  'label': '<Category bar1>',
                                  'name': 'bar1'},
                                 {'id': 7,
                                  'label': '<Category baz1>',
                                  'name': 'baz1'}],
                    'id': 5,
                    'label': '<Category foo1>',
                    'name': 'foo1'}],
      'id': 1,
      'label': '<Category root>',
      'name': 'root'}]

    [{'children': [{'children': [{'id': 4,
                                  'label': '<Category baz>',
                                  'name': 'baz'}],
                    'id': 3,
                    'label': '<Category bar>',
                    'name': 'bar'}],
      'id': 2,
      'label': '<Category foo>',
      'name': 'foo'}]

    [{'children': [{'id': 4, 'label': '<Category baz>', 'name': 'baz'}],
      'id': 3,
      'label': '<Category bar>',
      'name': 'bar'}]

    [{'id': 4, 'label': '<Category baz>', 'name': 'baz'}]

    [{'children': [{'id': 6, 'label': '<Category bar1>', 'name': 'bar1'},
                   {'id': 7, 'label': '<Category baz1>', 'name': 'baz1'}],
      'id': 5,
      'label': '<Category foo1>',
      'name': 'foo1'}]

    [{'id': 6, 'label': '<Category bar1>', 'name': 'bar1'}]

    [{'id': 7, 'label': '<Category baz1>', 'name': 'baz1'}]

Path to root
------------

Returns a list containing the ancestors and the node itself in tree order.

.. code-block:: text

   Path to root of bar node

   level      ---------------------
     1        |         1(root)14 |
              |             |     |
              |    ---------------|-----
              |    |    -----------    |
     2        | 2(foo)7 |           8(foo1)13
              |    |    |          /         \
     3        | 3(bar)6 |      9(bar1)10   11(baz1)12
              -----|-----
     4          4(baz)5

.. testcode::

   for item in categories:
       print(item)
       pprint(item.path_to_root().all())
       print()

.. testoutput::
   :options: +NORMALIZE_WHITESPACE

    <Category root>
    [<Category root>]

    <Category foo>
    [<Category foo>, <Category root>]

    <Category bar>
    [<Category bar>, <Category foo>, <Category root>]

    <Category baz>
    [<Category baz>, <Category bar>, <Category foo>, <Category root>]

    <Category foo1>
    [<Category foo1>, <Category root>]

    <Category bar1>
    [<Category bar1>, <Category foo1>, <Category root>]

    <Category baz1>
    [<Category baz1>, <Category foo1>, <Category root>]

Full code
---------

.. testcode::

    from pprint import pprint
    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy

    from sqlalchemy_mptt.mixins import BaseNestedSets

    app = Flask(__name__)
    app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db'
    db = SQLAlchemy(app)


    class Category(db.Model, BaseNestedSets):
        __tablename__ = 'categories'
        id = db.Column(db.Integer, primary_key=True)
        name = db.Column(db.String(400), index=True, unique=True)
        items = db.relationship("Product", backref='item', lazy='dynamic')

        def __repr__(self):
            return '<Category {}>'.format(self.name)


    class Product(db.Model):
        __tablename__ = 'products'
        id = db.Column(db.Integer, primary_key=True)
        category_id = db.Column(db.Integer, db.ForeignKey('categories.id'))
        name = db.Column(db.String(475), index=True)

    app.app_context().push()
    db.session.add(Category(name="root"))  # root node
    db.session.add_all(  # first branch of tree
        [
            Category(name="foo", parent_id=1),
            Category(name="bar", parent_id=2),
            Category(name="baz", parent_id=3),
        ]
    )
    db.session.add_all(  # second branch of tree
        [
            Category(name="foo1", parent_id=1),
            Category(name="bar1", parent_id=5),
            Category(name="baz1", parent_id=5),
        ]
    )

    '''
    "id"  "name"  "lft"  "rgt"  "level"  "parent_id"  "tree_id"
    1     "root"  1      14     1        1
    2     "foo"   2      7      2        1            1
    3     "bar"   3      6      3        2            1
    4     "baz"   4      5      4        3            1
    5     "foo1"  8      13     2        1            1
    6     "bar1"  9      10     3        5            1
    7     "baz1"  11     12     3        5            1

    root lft everytime = 1
    root rgt = qty_nodes * 2

    level
      1                  1(root)14
                             |
                    ---------------------
                    |                   |
      2          2(foo)7             8(foo1)13
                    |               /         \
      3          3(bar)6        9(bar1)10   11(baz1)12
                    |
      4          4(baz)5
    '''

    db.drop_all()
    db.create_all()
    db.session.commit()

    categories = Category.query.all()

    for item in categories:
        print(item)
        pprint(item.drilldown_tree())
        print()

    '''
    <Category root>
    [{'children': [{'children': [{'children': [{'node': <Category baz>}],
                                  'node': <Category bar>}],
                    'node': <Category foo>},
                   {'children': [{'node': <Category bar1>},
                                 {'node': <Category baz1>}],
                    'node': <Category foo1>}],
      'node': <Category root>}]

    <Category foo>
    [{'children': [{'children': [{'node': <Category baz>}],
                    'node': <Category bar>}],
      'node': <Category foo>}]

    <Category bar>
    [{'children': [{'node': <Category baz>}], 'node': <Category bar>}]

    <Category baz>
    [{'node': <Category baz>}]

    <Category foo1>
    [{'children': [{'node': <Category bar1>}, {'node': <Category baz1>}],
      'node': <Category foo1>}]

    <Category bar1>
    [{'node': <Category bar1>}]

    <Category baz1>
    [{'node': <Category baz1>}]
    '''

    for item in categories:
        print(item)
        pprint(item.path_to_root().all())
        print()

    '''
    <Category root>
    [<Category root>]

    <Category foo>
    [<Category foo>, <Category root>]

    <Category bar>
    [<Category bar>, <Category foo>, <Category root>]

    <Category baz>
    [<Category baz>, <Category bar>, <Category foo>, <Category root>]

    <Category foo1>
    [<Category foo1>, <Category root>]

    <Category bar1>
    [<Category bar1>, <Category foo1>, <Category root>]

    <Category baz1>
    [<Category baz1>, <Category foo1>, <Category root>]
    '''

.. testoutput::
    :options: +NORMALIZE_WHITESPACE
    :hide:

     <Category root>
     [{'children': [{'children': [{'children': [{'node': <Category baz>}],
                                     'node': <Category bar>}],
                      'node': <Category foo>},
                     {'children': [{'node': <Category bar1>},
                                    {'node': <Category baz1>}],
                      'node': <Category foo1>}],
        'node': <Category root>}]

     <Category foo>
     [{'children': [{'children': [{'node': <Category baz>}],
                      'node': <Category bar>}],
        'node': <Category foo>}]

     <Category bar>
     [{'children': [{'node': <Category baz>}], 'node': <Category bar>}]

     <Category baz>
     [{'node': <Category baz>}]

     <Category foo1>
     [{'children': [{'node': <Category bar1>}, {'node': <Category baz1>}],
        'node': <Category foo1>}]

     <Category bar1>
     [{'node': <Category bar1>}]

     <Category baz1>
     [{'node': <Category baz1>}]

     <Category root>
     [<Category root>]

     <Category foo>
     [<Category foo>, <Category root>]

     <Category bar>
     [<Category bar>, <Category foo>, <Category root>]

     <Category baz>
     [<Category baz>, <Category bar>, <Category foo>, <Category root>]

     <Category foo1>
     [<Category foo1>, <Category root>]

     <Category bar1>
     [<Category bar1>, <Category foo1>, <Category root>]

     <Category baz1>
     [<Category baz1>, <Category foo1>, <Category root>]


================================================
FILE: noxfile.py
================================================
# -*- coding: utf-8 -*-
#
# Copyright (c) 2025 Fayaz Yusuf Khan <fayaz.yusuf.khan@gmail.com>
#
# Distributed under terms of the MIT license.
#
# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "nox",
#     "nox-uv",
#     "requests",
# ]
# ///
""" Entry point script for testing, linting, and development of the package.

    This project uses Nox to create isolated environments.

    Requirements:
    - uv

    Usage:

      Run all tests and linting:
        $ uv run noxfile.py
      Run tests for a specific SQLAlchemy version:
        $ uv run noxfile.py -t sqla12
      Run tests for a specific Python version:
        $ uv run noxfile.py -s test -p 3.X

      Set up a development environment with the default Python version (3.8):
        $ uv run noxfile.py -s dev
      Set up a development environment with a specific Python version:
        $ uv run noxfile.py -s dev -P 3.X
"""
import sys
from itertools import groupby

import nox
import requests
from packaging.requirements import Requirement
from packaging.version import Version

# Python versions supported and tested against: 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, 3.14
PYTHON_MINOR_VERSION_MIN = 8
PYTHON_MINOR_VERSION_MAX = 14

nox.options.default_venv_backend = "uv"


@nox.session()
def lint(session):
    """Run flake8."""
    session.install("flake8")
    # stop the linter if there are Python syntax errors or undefined names
    session.run("flake8", "--select=E9,F63,F7,F82", "--show-source")
    # exit-zero treats all errors as warnings
    session.run("flake8", "--exit-zero", "--max-complexity=10")


def parametrize_test_versions():
    """Parametrize the session with all supported Python & SQLAlchemy versions."""
    print("Requesting all SQLAlchemy versions from PyPI...", file=sys.stderr)
    response = requests.get("https://pypi.org/pypi/SQLAlchemy/json")
    print("Preparing test version candidates...", file=sys.stderr)
    response.raise_for_status()
    data = response.json()
    all_major_and_minor_sqlalchemy_versions = [
        Version(f"{major}.{minor}")
        for (major, minor), _ in groupby(
            sorted(Version(version) for version in data["releases"].keys()),
            key=lambda v: (v.major, v.minor)
        )
    ]

    with open("requirements.txt", "r") as f:
        requirement = Requirement(f.read().strip())
    filtered_sqlalchemy_versions = [
        version for version in all_major_and_minor_sqlalchemy_versions
        if version in requirement.specifier
    ]

    return [
        nox.param(
            f"3.{python_minor}", str(sqlalchemy_version),
            tags=[f"sqla{sqlalchemy_version.major}{sqlalchemy_version.minor}"]
        )
        for python_minor in range(PYTHON_MINOR_VERSION_MIN, PYTHON_MINOR_VERSION_MAX + 1)
        for sqlalchemy_version in filtered_sqlalchemy_versions
        # SQLA 1.1 or below doesn't seem to support Python 3.10+
        # SQLA 1.2 doesn't seem to support Python 3.14+
        if ((sqlalchemy_version >= Version("1.2") or python_minor <= 9)
            and (sqlalchemy_version >= Version("1.3") or python_minor <= 13))]


PARAMETRIZED_TEST_VERSIONS = parametrize_test_versions()


def install_dependencies(session, session_name, sqlalchemy_version):
    """Install dependencies for the given session."""
    session.install(
        "-r", f"requirements-{session_name}.txt",
        f"sqlalchemy~={sqlalchemy_version}.0",
        "-e", "."
    )


@nox.session()
@nox.parametrize("python,sqlalchemy", PARAMETRIZED_TEST_VERSIONS)
def test(session, sqlalchemy):
    """Run tests with pytest.

    You can pass arguments to pytest using the `--` option.

        $ uv run noxfile.py -s test -- sqlalchemy_mptt/tests/test_events.py

    If no arguments are provided, it defaults to running all tests in the package.

    For running tests for a specific SQLAlchemy version, use the tags option:

        $ uv run noxfile.py -s test -t sqla12

    For fine-grained control over running the tests, refer the nox documentation: https://nox.thea.codes/en/stable/usage.html
    """
    install_dependencies(session, "test", sqlalchemy)
    pytest_args = session.posargs or ["--pyargs", "sqlalchemy_mptt"]
    session.run("pytest", *pytest_args, env={"SQLALCHEMY_WARN_20": "1"})


@nox.session()
@nox.parametrize("python,sqlalchemy", PARAMETRIZED_TEST_VERSIONS[-1:])
def doctest(session, sqlalchemy):
    """Run doctests in the documentation."""
    install_dependencies(session, "doctest", sqlalchemy)
    session.run("sphinx-build", "-b", "doctest", "docs", "docs/_build")


@nox.session(default=False)
def dev(session):
    """Set up a development environment.
    This will create a virtual environment and install the package in editable mode in .venv.

    To use a specific Python version, use the -P option:
    $ uv run noxfile.py -s dev -P 3.X
    """
    session.run("uv", "venv", "--python", session.python or f"3.{PYTHON_MINOR_VERSION_MIN}", "--seed")
    session.run(".venv/bin/pip", "install", "-r", "requirements-test.txt", external=True)
    session.run(".venv/bin/pip", "install", "-e", ".", external=True)


@nox.session(default=False)
def build(session):
    """Build the package."""
    session.install("build")
    session.run("python", "-m", "build")


if __name__ == "__main__":
    nox.main()


================================================
FILE: pyproject.toml
================================================
[tool.black]
line-length = 79
include = '\.pyi?$'
exclude = '''
/(
    \.git
  | \.hg
  | \.mypy_cache
  | \.tox
  | \.venv
  | _build
  | buck-out
  | build
  | dist
)/
'''

[tool.pytest.ini_options]
filterwarnings = [
  "error:::sqlalchemy_mptt"
]
addopts = "--cov sqlalchemy_mptt --cov-report term-missing:skip-covered"


================================================
FILE: requirements-doctest.txt
================================================
flask-sqlalchemy
sphinx


================================================
FILE: requirements-test.txt
================================================
hypothesis
pytest
pytest-cov


================================================
FILE: requirements.txt
================================================
SQLAlchemy>=1.0.0,<3.0


================================================
FILE: setup.py
================================================
import os

from setuptools import setup

this = os.path.dirname(os.path.realpath(__file__))


def read(name):
    with open(os.path.join(this, name)) as f:
        return f.read()


setup(
    name="sqlalchemy_mptt",
    version="0.6.0",
    url="http://github.com/uralbash/sqlalchemy_mptt/",
    author="Svintsov Dmitry",
    author_email="sacrud@uralbash.ru",
    maintainer="Fayaz Khan",
    maintainer_email="fayaz.yusuf.khan@gmail.com",
    packages=["sqlalchemy_mptt"],
    include_package_data=True,
    zip_safe=False,
    license="MIT",
    description=(
        "SQLAlchemy mixins for implementing tree-like models"
        " using Modified Pre-order Tree Traversal (MPTT) / Nested Sets"),
    long_description=read("README.rst") + "\n" + read("CHANGES.rst"),
    install_requires=read("requirements.txt"),
    classifiers=[
        "Development Status :: 5 - Production/Stable",
        "Environment :: Console",
        "Environment :: Web Environment",
        "Intended Audience :: Developers",
        "Natural Language :: English",
        "Operating System :: OS Independent",
        "Programming Language :: Python",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.8",
        "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 :: 3.14",
        "Framework :: Pyramid",
        "Framework :: Flask",
        "Topic :: Internet",
        "Topic :: Database",
        "License :: OSI Approved :: MIT License",
    ],
)


================================================
FILE: sqlalchemy_mptt/__init__.py
================================================
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright (c) 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
from .events import TreesManager
from .mixins import BaseNestedSets

__mixins__ = [BaseNestedSets]
__all__ = ['BaseNestedSets', 'mptt_sessionmaker']

tree_manager = TreesManager(BaseNestedSets)
tree_manager.register_events()
mptt_sessionmaker = tree_manager.register_factory


================================================
FILE: sqlalchemy_mptt/events.py
================================================
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
# Copyright (c) 2025 Fayaz Yusuf Khan <fayaz.yusuf.khan@gmail.com>
#
# Distributed under terms of the MIT license.

"""
SQLAlchemy events extension
"""
# standard library
import weakref

# SQLAlchemy
from sqlalchemy import and_, event, inspection
from sqlalchemy.orm import object_session
from sqlalchemy.sql import func
from sqlalchemy.orm.base import NO_VALUE

from sqlalchemy_mptt.sqlalchemy_compat import compat_layer


def _insert_subtree(
        table,
        connection,
        node_size,
        node_pos_left,
        node_pos_right,
        parent_pos_left,
        parent_pos_right,
        subtree,
        parent_tree_id,
        parent_level,
        node_level,
        left_sibling,
        table_pk
):
    # step 1: rebuild inserted subtree
    delta_lft = left_sibling['lft'] + 1
    if not left_sibling['is_parent']:
        delta_lft = left_sibling['rgt'] + 1
    delta_rgt = delta_lft + node_size - 1

    connection.execute(
        table.update()
        .where(table_pk.in_(subtree))
        .values(
            lft=table.c.lft - node_pos_left + delta_lft,
            rgt=table.c.rgt - node_pos_right + delta_rgt,
            level=table.c.level - node_level + parent_level + 1,
            tree_id=parent_tree_id
        )
    )

    # step 2: update key of right side
    connection.execute(
        table.update()
        .where(table.c.rgt > delta_lft - 1)
        .where(table_pk.notin_(subtree))
        .where(table.c.tree_id == parent_tree_id)
        .values(
            rgt=table.c.rgt + node_size,
            lft=compat_layer.case(
                (table.c.lft > left_sibling['lft'], table.c.lft + node_size),
                else_=table.c.lft
            )
        )
    )


def _get_tree_table(mapper):
    for table in mapper.tables:
        if all(key in table.c for key in ['level', 'lft', 'rgt', 'parent_id']):
            return table


def mptt_before_insert(mapper, connection, instance):
    """ Based on example
    https://bitbucket.org/zzzeek/sqlalchemy/src/73095b353124/examples/nested_sets/nested_sets.py?at=master
    """
    table = _get_tree_table(mapper)
    db_pk = instance.get_pk_column()
    table_pk = getattr(table.c, db_pk.name)

    if instance.parent_id is None:
        instance.left = 1
        instance.right = 2
        instance.level = instance.get_default_level()
        tree_id = connection.scalar(
            compat_layer.select(
                func.max(table.c.tree_id) + 1
            )
        ) or 1
        instance.tree_id = tree_id
    else:
        (parent_pos_left,
         parent_pos_right,
         parent_tree_id,
         parent_level) = connection.execute(
            compat_layer.select(
                table.c.lft,
                table.c.rgt,
                table.c.tree_id,
                table.c.level
            ).where(
                table_pk == instance.parent_id
            )
        ).fetchone()

        # Update key of right side
        connection.execute(
            table.update()
            .where(table.c.rgt >= parent_pos_right)
            .where(table.c.tree_id == parent_tree_id)
            .values(
                lft=compat_layer.case(
                    (table.c.lft > parent_pos_right, table.c.lft + 2),
                    else_=table.c.lft
                ),
                rgt=compat_layer.case(
                    (table.c.rgt >= parent_pos_right, table.c.rgt + 2),
                    else_=table.c.rgt
                )
            )
        )

        instance.level = parent_level + 1
        instance.tree_id = parent_tree_id
        instance.left = parent_pos_right
        instance.right = parent_pos_right + 1


def mptt_before_delete(mapper, connection, instance, delete=True):
    table = _get_tree_table(mapper)
    tree_id = instance.tree_id
    pk = getattr(instance, instance.get_pk_name())
    db_pk = instance.get_pk_column()
    table_pk = getattr(table.c, db_pk.name)
    lft, rgt = connection.execute(
        compat_layer.select(
            table.c.lft,
            table.c.rgt
        ).where(
            table_pk == pk
        )
    ).fetchone()
    delta = rgt - lft + 1

    if delete:
        mapper.base_mapper.confirm_deleted_rows = False
        connection.execute(
            table.delete().where(
                table_pk == pk
            )
        )

    if instance.parent_id is not None or not delete:
        """ Update key of current tree

            UPDATE tree
            SET left_id = CASE
                    WHEN left_id > $leftId THEN left_id - $delta
                    ELSE left_id
                END,
                right_id = CASE
                    WHEN right_id >= $rightId THEN right_id - $delta
                    ELSE right_id
                END
        """
        connection.execute(
            table.update()
            .where(table.c.rgt > rgt)
            .where(table.c.tree_id == tree_id)
            .values(
                lft=compat_layer.case(
                    (table.c.lft > lft, table.c.lft - delta),
                    else_=table.c.lft
                ),
                rgt=compat_layer.case(
                    (table.c.rgt >= rgt, table.c.rgt - delta),
                    else_=table.c.rgt
                )
            )
        )


def mptt_before_update(mapper, connection, instance):
    """ Based on this example:
        http://stackoverflow.com/questions/889527/move-node-in-nested-set
    """
    node_id = getattr(instance, instance.get_pk_name())
    table = _get_tree_table(mapper)
    db_pk = instance.get_pk_column()
    default_level = instance.get_default_level()
    table_pk = getattr(table.c, db_pk.name)
    mptt_move_inside = None
    left_sibling = None
    left_sibling_tree_id = None

    if hasattr(instance, 'mptt_move_inside'):
        mptt_move_inside = instance.mptt_move_inside

    if hasattr(instance, 'mptt_move_before'):
        (
            right_sibling_left,
            right_sibling_right,
            right_sibling_parent,
            right_sibling_level,
            right_sibling_tree_id
        ) = connection.execute(
            compat_layer.select(
                table.c.lft,
                table.c.rgt,
                table.c.parent_id,
                table.c.level,
                table.c.tree_id
            ).where(
                table_pk == instance.mptt_move_before
            )
        ).fetchone()
        current_lvl_nodes = connection.execute(
            compat_layer.select(
                table.c.lft,
                table.c.rgt,
                table.c.parent_id,
                table.c.tree_id
            ).where(
                and_(
                    table.c.level == right_sibling_level,
                    table.c.tree_id == right_sibling_tree_id,
                    table.c.lft < right_sibling_left
                )
            )
        ).fetchall()
        if current_lvl_nodes:
            (
                left_sibling_left,
                left_sibling_right,
                left_sibling_parent,
                left_sibling_tree_id
            ) = current_lvl_nodes[-1]
            instance.parent_id = left_sibling_parent
            left_sibling = {
                'lft': left_sibling_left,
                'rgt': left_sibling_right,
                'is_parent': False
            }
        # if move_before to top level
        elif not right_sibling_parent:
            left_sibling_tree_id = right_sibling_tree_id - 1

    # if placed after a particular node
    if hasattr(instance, 'mptt_move_after'):
        (
            left_sibling_left,
            left_sibling_right,
            left_sibling_parent,
            left_sibling_tree_id
        ) = connection.execute(
            compat_layer.select(
                table.c.lft,
                table.c.rgt,
                table.c.parent_id,
                table.c.tree_id
            ).where(
                table_pk == instance.mptt_move_after
            )
        ).fetchone()
        instance.parent_id = left_sibling_parent
        left_sibling = {
            'lft': left_sibling_left,
            'rgt': left_sibling_right,
            'is_parent': False
        }

    """ Get subtree from node

        SELECT id, name, level FROM my_tree
        WHERE left_key >= $left_key AND right_key <= $right_key
        ORDER BY left_key
    """
    subtree = connection.execute(
        compat_layer.select(table_pk)
        .where(
            and_(
                table.c.lft >= instance.left,
                table.c.rgt <= instance.right,
                table.c.tree_id == instance.tree_id
            )
        ).order_by(
            table.c.lft
        )
    ).fetchall()
    subtree = [x[0] for x in subtree]

    """ step 0: Initialize parameters.

        Put there left and right position of moving node
    """
    (
        node_pos_left,
        node_pos_right,
        node_tree_id,
        node_parent_id,
        node_level
    ) = connection.execute(
        compat_layer.select(
            table.c.lft,
            table.c.rgt,
            table.c.tree_id,
            table.c.parent_id,
            table.c.level
        ).where(
            table_pk == node_id
        )
    ).fetchone()

    # if instance just update w/o move
    # XXX why this str() around parent_id comparison?
    if not left_sibling \
            and str(node_parent_id) == str(instance.parent_id) \
            and not mptt_move_inside:
        if left_sibling_tree_id is None:
            return

    # fix tree shorting
    if instance.parent_id is not None:
        (
            parent_id,
            parent_pos_right,
            parent_pos_left,
            parent_tree_id,
            parent_level
        ) = connection.execute(
            compat_layer.select(
                table_pk,
                table.c.rgt,
                table.c.lft,
                table.c.tree_id,
                table.c.level
            ).where(
                table_pk == instance.parent_id
            )
        ).fetchone()
        if node_parent_id is None and node_tree_id == parent_tree_id:
            instance.parent_id = None
            return

    # delete from old tree
    mptt_before_delete(mapper, connection, instance, False)

    if instance.parent_id is not None:
        """ Put there right position of new parent node (there moving node
            should be moved)
        """
        (
            parent_id,
            parent_pos_right,
            parent_pos_left,
            parent_tree_id,
            parent_level
        ) = connection.execute(
            compat_layer.select(
                table_pk,
                table.c.rgt,
                table.c.lft,
                table.c.tree_id,
                table.c.level
            ).where(
                table_pk == instance.parent_id
            )
        ).fetchone()
        # 'size' of moving node (including all it's sub nodes)
        node_size = node_pos_right - node_pos_left + 1

        # left sibling node
        if not left_sibling:
            left_sibling = {
                'lft': parent_pos_left,
                'rgt': parent_pos_right,
                'is_parent': True
            }

        # insert subtree in exist tree
        instance.tree_id = parent_tree_id
        _insert_subtree(
            table,
            connection,
            node_size,
            node_pos_left,
            node_pos_right,
            parent_pos_left,
            parent_pos_right,
            subtree,
            parent_tree_id,
            parent_level,
            node_level,
            left_sibling,
            table_pk
        )
    else:
        # if insert after
        if left_sibling_tree_id or left_sibling_tree_id == 0:
            tree_id = left_sibling_tree_id + 1
            connection.execute(
                table.update()
                .where(table.c.tree_id > left_sibling_tree_id)
                .values(
                    tree_id=table.c.tree_id + 1
                )
            )
        # if just insert
        else:
            tree_id = connection.scalar(
                compat_layer.select(
                    func.max(table.c.tree_id) + 1
                )
            )

        connection.execute(
            table.update()
            .where(table_pk.in_(subtree))
            .values(
                lft=table.c.lft - node_pos_left + 1,
                rgt=table.c.rgt - node_pos_left + 1,
                level=table.c.level - node_level + default_level,
                tree_id=tree_id
            )
        )


class _WeakDefaultDict(weakref.WeakKeyDictionary):
    """A weak reference dictionary that returns a new `WeakSet` as a default
    value for missing keys."""

    def __getitem__(self, key):
        try:
            return super(_WeakDefaultDict, self).__getitem__(key)
        except KeyError:
            self[key] = value = weakref.WeakSet()
            return value


class TreesManager(object):
    """
    Manages events dispatching for all subclasses of a given class.
    """
    def __init__(self, base_class):
        self.base_class = base_class
        self.classes = set()
        self.instances = _WeakDefaultDict()

    def register_events(self, remove=False):
        for e, h in (
            ('before_insert', self.before_insert),
            ('before_update', self.before_update),
            ('before_delete', self.before_delete),
        ):
            is_event_exist = event.contains(self.base_class, e, h)
            if remove and is_event_exist:
                event.remove(self.base_class, e, h)
            elif not is_event_exist:
                event.listen(self.base_class, e, h, propagate=True)
        return self

    def register_factory(self, sessionmaker):
        """
        Registers this TreesManager instance to respond on
        `after_flush_postexec` events on the given session or session factory.
        This method returns the original argument, so that it can be used by
        wrapping an already existing instance:

        .. code-block:: python
            :linenos:

            from sqlalchemy import create_engine
            from sqlalchemy.orm import sessionmaker, mapper
            from sqlalchemy_mptt.mixins import BaseNestedSets

            engine = create_engine('...')

            trees_manager = TreesManager(BaseNestedSets)
            trees_manager.register_mapper(mapper)

            Session = tree_manager.register_factory(
                sessionmaker(bind=engine)
            )

        A reference to this method, bound to a default instance of this class
        and already registered to a mapper, is importable directly from
        `sqlalchemy_mptt`:

        .. code-block:: python
            :linenos:

            from sqlalchemy import create_engine
            from sqlalchemy.orm import sessionmaker
            from sqlalchemy_mptt import mptt_sessionmaker

            engine = create_engine('...')
            Session = mptt_sessionmaker(sessionmaker(bind=engine))
        """
        event.listen(sessionmaker, 'after_flush_postexec',
                     self.after_flush_postexec)
        return sessionmaker

    def before_insert(self, mapper, connection, instance):
        session = object_session(instance)
        self.instances[session].add(instance)
        mptt_before_insert(mapper, connection, instance)

    def before_update(self, mapper, connection, instance):
        session = object_session(instance)
        self.instances[session].add(instance)
        mptt_before_update(mapper, connection, instance)

    def before_delete(self, mapper, connection, instance):
        session = object_session(instance)
        self.instances[session].discard(instance)
        mptt_before_delete(mapper, connection, instance)

    def after_flush_postexec(self, session, context):
        """
        Event listener to recursively expire `left` and `right` attributes the
        parents of all modified instances part of this flush.
        """
        instances = self.instances[session]
        while True:
            try:
                instance = instances.pop()
            except KeyError:
                break
            if instance not in session:
                continue
            parent = self.get_parent_value(instance)

            while parent != NO_VALUE and parent is not None:
                instances.discard(parent)
                session.expire(parent, ['left', 'right', 'tree_id', 'level'])
                parent = self.get_parent_value(parent)
            else:
                session.expire(instance, ['left', 'right', 'tree_id', 'level'])
                self.expire_session_for_children(session, instance)

    @staticmethod
    def get_parent_value(instance):
        return inspection.inspect(instance).attrs.parent.loaded_value

    @staticmethod
    def expire_session_for_children(session, instance):
        children = instance.children

        def expire_recursively(node):
            children = node.children
            for item in children:
                session.expire(item, ['left', 'right', 'tree_id', 'level'])
                expire_recursively(item)

        if children != NO_VALUE and children is not None:
            for item in children:
                session.expire(item, ['left', 'right', 'tree_id', 'level'])
                expire_recursively(item)


================================================
FILE: sqlalchemy_mptt/mixins.py
================================================
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
# Copyright © 2016 Jiri Kuncar <jiri.kuncar@gmail.com>
#
# Distributed under terms of the MIT license.

"""
SQLAlchemy nested sets mixin

.. testsetup::

    engine = create_engine('sqlite:///:memory:')
    session = Session(bind=engine)

"""
# SQLAlchemy
from sqlalchemy import Column, Integer, ForeignKey, asc, desc
from sqlalchemy.orm import backref, relationship, object_session
from sqlalchemy.ext.hybrid import hybrid_method
from sqlalchemy.orm.session import Session
from sqlalchemy.ext.declarative import declared_attr

# local
from .events import _get_tree_table


class BaseNestedSets(object):
    """ Base mixin for MPTT model.

    Example:

    .. testcode::

        from sqlalchemy import Boolean, Column, create_engine, Integer
        from sqlalchemy.ext.declarative import declarative_base
        from sqlalchemy.orm import sessionmaker

        from sqlalchemy_mptt.mixins import BaseNestedSets

        Base = declarative_base()


        class Tree(Base, BaseNestedSets):
            __tablename__ = "tree"

            id = Column(Integer, primary_key=True)
            visible = Column(Boolean)

            def __repr__(self):
                return "<Node (%s)>" % self.id

    .. testcode::
        :hide:

        # This is some more setup code.
        Base.metadata.create_all(engine)
        node = Tree()
        session.add(node)
        session.flush()
        node7 = Tree(parent=node)
        session.add(node7)
        session.flush()
        node8 = Tree(parent=node7)
        session.add(node8)
        session.flush()
        node10 = Tree(parent=node7)
        session.add(node10)
        session.flush()
        node11 = Tree(parent=node10)
        session.add(node11)
    """

    @classmethod
    def __declare_first__(cls):
        cls.__mapper__.batch = False

    @classmethod
    def get_default_level(cls):
        """
        Compatibility with Django MPTT: level value for root node.
        See https://github.com/uralbash/sqlalchemy_mptt/issues/56
        """
        return getattr(cls, "sqlalchemy_mptt_default_level", 1)

    @classmethod
    def get_pk_name(cls):
        return getattr(cls, "sqlalchemy_mptt_pk_name", "id")

    @classmethod
    def get_pk_column(cls):
        return getattr(cls, cls.get_pk_name())

    def get_pk_value(self):
        return getattr(self, self.get_pk_name())

    @declared_attr
    def tree_id(cls):
        return Column("tree_id", Integer)

    @declared_attr
    def parent_id(cls):
        pk = cls.get_pk_column()
        if not pk.name:
            pk.name = cls.get_pk_name()

        return Column(
            "parent_id",
            pk.type,
            ForeignKey(
                "{}.{}".format(cls.__tablename__, pk.name), ondelete="CASCADE"
            ),
        )

    @declared_attr
    def parent(self):
        return relationship(
            self,
            order_by=lambda: self.left,
            foreign_keys=[self.parent_id],
            remote_side="{}.{}".format(self.__name__, self.get_pk_name()),
            backref=backref(
                "children",
                cascade="all,delete",
                order_by=lambda: (self.tree_id, self.left),
            ),
        )

    @declared_attr
    def left(cls):
        return Column("lft", Integer, nullable=False, index=True)

    @declared_attr
    def right(cls):
        return Column("rgt", Integer, nullable=False, index=True)

    @declared_attr
    def level(cls):
        return Column("level", Integer, nullable=False, default=0, index=True)

    @hybrid_method
    def is_ancestor_of(self, other, inclusive=False):
        """ class or instance level method which returns True if self is
        ancestor (closer to root) of other else False. Optional flag
        `inclusive` on whether or not to treat self as ancestor of self.

        For example see:

        * :mod:`sqlalchemy_mptt.tests.cases.integrity.test_hierarchy_structure`
        """
        if inclusive:
            return (
                (self.tree_id == other.tree_id)
                & (self.left <= other.left)
                & (other.right <= self.right)
            )
        return (
            (self.tree_id == other.tree_id)
            & (self.left < other.left)
            & (other.right < self.right)
        )

    @hybrid_method
    def is_descendant_of(self, other, inclusive=False):
        """ class or instance level method which returns True if self is
        descendant (farther from root) of other else False.  Optional flag
        `inclusive` on whether or not to treat self as descendant of self.

        For example see:

        * :mod:`sqlalchemy_mptt.tests.cases.integrity.test_hierarchy_structure`
        """
        return other.is_ancestor_of(self, inclusive)

    def move_inside(self, parent_id):
        """ Moving one node of tree inside another

        For example see:

        * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_inside_function`
        * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_inside_to_the_same_parent_function`
        """  # noqa
        session = Session.object_session(self)
        self.parent_id = parent_id
        self.mptt_move_inside = parent_id
        session.add(self)

    def move_after(self, node_id):
        """ Moving one node of tree after another

        For example see :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_after_function`
        """  # noqa
        session = Session.object_session(self)
        self.parent_id = self.parent_id
        self.mptt_move_after = node_id
        session.add(self)

    def move_before(self, node_id):
        """ Moving one node of tree before another

        For example see:

        * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_before_function`
        * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_before_to_other_tree`
        * :mod:`sqlalchemy_mptt.tests.cases.move_node.test_move_before_to_top_level`
        """  # noqa
        session = Session.object_session(self)
        table = _get_tree_table(self.__mapper__)
        pk = getattr(table.c, self.get_pk_column().name)
        node = session.query(table).filter(pk == node_id).one()
        self.parent_id = node.parent_id
        self.mptt_move_before = node_id
        session.add(self)

    def leftsibling_in_level(self):
        """ Node to the left of the current node at the same level

        For example see
        :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_leftsibling_in_level`
        """  # noqa
        table = _get_tree_table(self.__mapper__)
        session = Session.object_session(self)
        current_lvl_nodes = (
            session.query(table)
            .filter_by(level=self.level)
            .filter_by(tree_id=self.tree_id)
            .filter(table.c.lft < self.left)
            .order_by(table.c.lft)
            .all()
        )
        if current_lvl_nodes:
            return current_lvl_nodes[-1]
        return None

    @classmethod
    def _node_to_dict(cls, node, json, json_fields):
        """ Helper method for ``get_tree``.
        """
        if json:
            pk_name = node.get_pk_name()
            # jqTree or jsTree format
            result = {"id": getattr(node, pk_name), "label": node.__repr__()}
            if json_fields:
                result.update(json_fields(node))
        else:
            result = {"node": node}
        return result

    @classmethod
    def _base_query(cls, session=None):
        return session.query(cls)

    def _base_query_obj(self, session=None):
        if not session:
            session = object_session(self)
        return self._base_query(session)

    @classmethod
    def _base_order(cls, query, order=asc):
        return (
            query.order_by(order(cls.tree_id))
            .order_by(order(cls.level))
            .order_by(order(cls.left))
        )

    @classmethod
    def get_tree(cls, session=None, json=False, json_fields=None, query=None):
        """ This method generate tree of current node table in dict or json
        format. You can make custom query with attribute ``query``. By default
        it return all nodes in table.

        Args:
            session (:mod:`sqlalchemy.orm.session.Session`): SQLAlchemy session

        Kwargs:
            json (bool): if True return JSON jqTree format
            json_fields (function): append custom fields in JSON
            query (function): it takes :class:`sqlalchemy.orm.query.Query`
            object as an argument, and returns in a modified form

                .. testcode::

                    def query(nodes):
                        return nodes.filter(node.__class__.tree_id.is_(node.tree_id))

                    node.get_tree(session=session, json=True, query=query)

        Example:

        * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_get_tree`
        * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_get_json_tree`
        * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_get_json_tree_with_custom_field`
        """  # noqa
        tree = []
        nodes_of_level = {}

        # handle custom query
        nodes = cls._base_query(session)
        if query:
            nodes = query(nodes)
        nodes = cls._base_order(nodes).all()

        # search minimal level of nodes.
        min_level = min([node.level for node in nodes] or [None])

        def get_node_id(node):
            return getattr(node, node.get_pk_name())

        for node in nodes:
            result = cls._node_to_dict(node, json, json_fields)
            parent_id = node.parent_id
            if node.level != min_level:  # for children
                # Find parent in the tree
                if parent_id not in nodes_of_level.keys():
                    continue
                if "children" not in nodes_of_level[parent_id]:
                    nodes_of_level[parent_id]["children"] = []
                # Append node to parent
                nl = nodes_of_level[parent_id]["children"]
                nl.append(result)
                nodes_of_level[get_node_id(node)] = nl[-1]
            else:  # for top level nodes
                tree.append(result)
                nodes_of_level[get_node_id(node)] = tree[-1]
        return tree

    def _drilldown_query(self, nodes=None):
        table = self.__class__
        if not nodes:
            nodes = self._base_query_obj()
        return nodes.filter(self.is_ancestor_of(table, inclusive=True))

    def drilldown_tree(self, session=None, json=False, json_fields=None):
        """ This method generate a branch from a tree, beginning with current
        node.

        For example:

            .. testcode::

                node7.drilldown_tree()

            .. code::

                level           Nested sets example
                1                    1(1)22       ---------------------
                        _______________|_________|_________            |
                       |               |         |         |           |
                2    2(2)5           6(4)11      |      12(7)21        |
                       |               ^         |         ^           |
                3    3(3)4       7(5)8   9(6)10  | 13(8)16   17(10)20  |
                                                 |    |          |     |
                4                                | 14(9)15   18(11)19  |
                                                 |                     |
                                                  ---------------------

        Example in tests:

            * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_drilldown_tree`
        """
        if not session:
            session = object_session(self)
        return self.get_tree(
            session,
            json=json,
            json_fields=json_fields,
            query=self._drilldown_query,
        )

    def path_to_root(self, session=None, order=desc):
        r"""Generate path from a leaf or intermediate node to the root.

        For example:

            .. testcode::

                node11.path_to_root()

            .. code::

                level           Nested sets example

                                 -----------------------------------------
                1               |    1(1)22                               |
                        ________|______|_____________________             |
                       |        |      |                     |            |
                       |         ------+---------            |            |
                2    2(2)5           6(4)11      | --     12(7)21         |
                       |               ^             |   /      \         |
                3    3(3)4       7(5)8   9(6)10      ---/----    \        |
                                                    13(8)16 |  17(10)20   |
                                                       |    |     |       |
                4                                   14(9)15 | 18(11)19    |
                                                            |             |
                                                             -------------
        """
        table = self.__class__
        query = self._base_query_obj(session=session)
        query = query.filter(table.is_ancestor_of(self, inclusive=True))
        return self._base_order(query, order=order)

    def get_siblings(self, include_self=False, session=None):
        r"""
        * https://github.com/uralbash/sqlalchemy_mptt/issues/64
        * https://django-mptt.readthedocs.io/en/latest/models.html#get-siblings-include-self-false

        Creates a query containing siblings of this model
        instance. Root nodes are considered to be siblings of other root
        nodes.

        For example:

            .. testcode::

                node10.get_siblings() #-> [Node(8)]

            Only one node is sibling of node10

            .. code::

                level           Nested sets example

                1                   1(1)22
                        ______________|____________________
                       |              |                    |
                       |              |                    |
                2    2(2)5          6(4)11              12(7)21
                       |              ^                /       \            |
                3    3(3)4      7(5)8   9(6)10        /         \           |
                                                   13(8)16   17(10)20       |
                                                      |         |           |
                4                                  14(9)15   18(11)19       |


        """
        table = self.__class__
        query = self._base_query_obj(session=session)
        query = query.filter(table.parent_id == self.parent_id)
        if not include_self:
            query = query.filter(self.get_pk_column() != self.get_pk_value())
        return query

    def get_children(self, session=None):
        r"""
        * https://github.com/uralbash/sqlalchemy_mptt/issues/64
        * https://github.com/django-mptt/django-mptt/blob/fd76a816e05feb5fb0fc23126d33e514460a0ead/mptt/models.py#L563

        Returns a query containing the immediate children of this
        model instance, in tree order.

        For example:

            .. testcode::

                node7.get_children() #-> [Node(8), Node(10)]

            .. code::

                level           Nested sets example

                1                   1(1)22
                        ______________|____________________
                       |              |                    |
                       |              |                    |
                2    2(2)5          6(4)11              12(7)21
                       |              ^                /       \             |
                3    3(3)4      7(5)8   9(6)10        /         \            |
                                                   13(8)16   17(10)20        |
                                                      |         |            |
                4                                  14(9)15   18(11)19        |


        """
        table = self.__class__
        query = self._base_query_obj(session=session)
        query = query.filter(table.parent_id == self.get_pk_value())
        return query

    @classmethod
    def rebuild_tree(cls, session, tree_id):
        """ This method rebuild tree.

        Args:
            session (:mod:`sqlalchemy.orm.session.Session`): SQLAlchemy session
            tree_id (int or str): id of tree

        Example:

        * :mod:`sqlalchemy_mptt.tests.cases.get_tree.test_rebuild`
        """
        session.query(cls).filter_by(tree_id=tree_id).update(
            {cls.left: 0, cls.right: 0, cls.level: 0}
        )
        top = (
            session.query(cls)
            .filter_by(parent_id=None)
            .filter_by(tree_id=tree_id)
            .one()
        )
        top.left = left = 1
        top.right = right = 2
        top.level = level = cls.get_default_level()

        def recursive(children, left, right, level):
            level = level + 1
            for i, node in enumerate(children):
                same_level_right = children[i - 1].right
                left = left + 1

                if i > 0:
                    left = left + 1
                if same_level_right:
                    left = same_level_right + 1

                right = left + 1
                node.left = left
                node.right = right
                parent = node.parent

                j = 0
                while parent:
                    parent.right = right + 1 + j
                    parent = parent.parent
                    j += 1

                node.level = level
                recursive(node.children, left, right, level)

        recursive(top.children, left, right, level)

    @classmethod
    def rebuild(cls, session, tree_id=None):
        """ This function rebuild tree.

        Args:
            session (:mod:`sqlalchemy.orm.session.Session`): SQLAlchemy session

        Kwargs:
            tree_id (int or str): id of tree, default None

        Example:

        * :mod:`sqlalchemy_mptt.tests.TestTree.test_rebuild`
        """

        trees = session.query(cls).filter_by(parent_id=None)
        if tree_id:
            trees = trees.filter_by(tree_id=tree_id)
        for tree in trees:
            cls.rebuild_tree(session, tree.tree_id)


================================================
FILE: sqlalchemy_mptt/sqlalchemy_compat.py
================================================
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright (c) 2025 Fayaz Yusuf Khan <fayaz.yusuf.khan@gmail.com>
# Distributed under terms of the MIT license.
"""Compatibility layer for SQLAlchemy versions."""
import sqlalchemy as sa


class LegacySQLAlchemyAPI:
    """A class to provide compatibility for legacy SQLAlchemy versions (1.0 - 1.3)."""

    @staticmethod
    def declarative_base(*args, **kwargs):
        from sqlalchemy.ext.declarative import declarative_base
        return declarative_base(*args, **kwargs)

    @staticmethod
    def select(*args, **kwargs):
        return sa.select(args, **kwargs)

    @staticmethod
    def case(*args, **kwargs):
        return sa.case(args, **kwargs)

    @staticmethod
    def get(session, model, id):
        return session.query(model).get(id)


class ModernSQLAlchemyAPI:
    """A class to provide compatibility for modern SQLAlchemy versions (1.4+)."""

    @staticmethod
    def declarative_base(*args, **kwargs):
        from sqlalchemy.orm import declarative_base
        return declarative_base(*args, **kwargs)

    @staticmethod
    def select(*args, **kwargs):
        return sa.select(*args, **kwargs)

    @staticmethod
    def case(*args, **kwargs):
        return sa.case(*args, **kwargs)

    @staticmethod
    def get(session, model, id):
        return session.get(model, id)


if sa.__version__ < '1.4':
    compat_layer = LegacySQLAlchemyAPI()
else:
    compat_layer = ModernSQLAlchemyAPI()


================================================
FILE: sqlalchemy_mptt/tests/__init__.py
================================================
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
""" Base mptt tree

.. code::

    level           Nested sets tree1
        1                    1(1)22
                _______________|___________________
               |               |                   |
        2    2(2)5           6(4)11             12(7)21
               |               ^                   ^
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                              |          |
        4                                  14(9)15   18(11)19

        level           Nested sets tree2
        1                    1(12)22
                _______________|___________________
               |               |                   |
        2    2(13)5         6(15)11             12(18)21
               |               ^                    ^
        3    3(14)4     7(16)8   9(17)10   13(19)16   17(21)20
                                               |          |
        4                                  14(20)15   18(22)19

"""
# standard library
import contextlib
import json
import os
import sys
import typing
import unittest

# SQLAlchemy
import sqlalchemy as sa
from sqlalchemy import create_engine, event
from sqlalchemy.orm import sessionmaker

from sqlalchemy_mptt import mptt_sessionmaker
from sqlalchemy_mptt.sqlalchemy_compat import compat_layer

from .cases.edit_node import Changes
from .cases.get_node import GetNodes
from .cases.get_tree import Tree
from .cases.initialize import Initialize
from .cases.integrity import DataIntegrity
from .cases.move_node import MoveAfter, MoveBefore, MoveInside

BaseType = unittest.TestCase if typing.TYPE_CHECKING else object


def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[]):
    """
    Decorator to mark tests that are expected to fail on specific versions of
    SQLAlchemy and/or Python.

    If a parameter is not provided, it is assumed that the failure is expected on all versions.
    If more than one parameter is provided, it is assumed that the failure is expected on all combinations of those parameters.
    """
    def decorator(test_method):
        if sqlalchemy_versions:
            if not any(sa.__version__.startswith(v) for v in sqlalchemy_versions):
                return test_method
        if python_versions:
            if not any(sys.version.startswith(v) for v in python_versions):
                return test_method
        # If we reach here, it means the test is expected to fail
        return unittest.expectedFailure(test_method)
    return decorator


class DatabaseSetupMixin(BaseType):
    base: compat_layer.declarative_base()  # type: ignore

    def setUp(self):
        with contextlib.suppress(AttributeError):
            super().setUp()
        self.engine: sa.engine.Engine = create_engine("sqlite:///:memory:")
        Session = mptt_sessionmaker(sessionmaker(bind=self.engine))
        self.session = Session()
        self.base.metadata.create_all(self.engine)

    def tearDown(self):
        with contextlib.suppress(AttributeError):
            super().tearDown()
        self.session.close()
        self.engine.dispose()


class Fixtures(object):
    def __init__(self, session):
        self.session = session

    def add(self, model, fixtures):
        here = os.path.dirname(os.path.realpath(__file__))
        with open(os.path.join(here, fixtures)) as file:
            fixtures = json.loads(file.read())
        for fixture in fixtures:
            if hasattr(model, "sqlalchemy_mptt_pk_name"):
                fixture[model.sqlalchemy_mptt_pk_name] = fixture.pop("id")
            self.session.add(model(**fixture))
            self.session.flush()


class TreeTestingMixin(
    Initialize,
    Changes,
    MoveAfter,
    DataIntegrity,
    MoveBefore,
    MoveInside,
    Tree,
    GetNodes,
    DatabaseSetupMixin
):
    base = None
    model = None

    def catch_queries(self, conn, cursor, statement, *args):
        self.stmts.append(statement)

    def start_query_counter(self):
        self.stmts = []
        event.listen(
            self.session.bind.engine, "before_cursor_execute", self.catch_queries
        )

    def stop_query_counter(self):
        event.remove(
            self.session.bind.engine, "before_cursor_execute", self.catch_queries
        )

    def setUp(self):
        super().setUp()
        self.fixture = Fixtures(self.session)
        self.fixture.add(
            self.model, os.path.join("fixtures", getattr(self, "fixtures", "tree.json"))
        )

        self.result = self.session.query(
            self.model.get_pk_column(),
            self.model.left,
            self.model.right,
            self.model.level,
            self.model.parent_id,
            self.model.tree_id,
        )

    def test_session_expire_for_move_after_to_new_tree(self):
        """
        https://github.com/uralbash/sqlalchemy_mptt/issues/33
        """
        node = (
            self.session.query(self.model).filter(self.model.get_pk_column() == 4).one()
        )
        children = (
            self.session.query(self.model)
            .filter(self.model.get_pk_column().in_((5, 6)))
            .all()
        )
        node.move_after("1")
        self.session.flush()

        _level = node.get_default_level()
        self.assertEqual(node.tree_id, 2)
        self.assertEqual(node.level, _level)
        self.assertEqual(node.parent_id, None)

        self.assertEqual(children[0].tree_id, 2)
        self.assertEqual(children[0].parent_id, 4)
        self.assertEqual(children[0].level, _level + 1)

        self.assertEqual(children[1].tree_id, 2)
        self.assertEqual(children[1].parent_id, 4)
        self.assertEqual(children[1].level, _level + 1)


================================================
FILE: sqlalchemy_mptt/tests/cases/__init__.py
================================================


================================================
FILE: sqlalchemy_mptt/tests/cases/edit_node.py
================================================
class Changes(object):

    def test_update_wo_move(self):
        """ Update node w/o move
        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19
        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 4).one()
        node.visible = True
        self.session.add(node)
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,   6, 11, _level + 1,  1, 1),
                (5,   7,  8, _level + 2,  4, 1),
                (6,   9, 10, _level + 2,  4, 1),
                (7,  12, 21, _level + 1,  1, 1),
                (8,  13, 16, _level + 2,  7, 1),
                (9,  14, 15, _level + 3,  8, 1),
                (10, 17, 20, _level + 2,  7, 1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ],
            self.result.all())  # flake8: noqa

    def test_update_wo_move_like_sacrud_save(self):
        """ Just change attr from node w/o move
        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19
        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 4).one()
        node.parent_id = '1'
        node.visible = True
        self.session.add(node)
        _level = node.get_default_level()
        #                 id lft rgt lvl parent tree
        self.assertEqual([(1,   1, 22, _level + 0, None, 1),
                          (2,   2,  5, _level + 1,  1, 1),
                          (3,   3,  4, _level + 2,  2, 1),
                          (4,   6, 11, _level + 1,  1, 1),
                          (5,   7,  8, _level + 2,  4, 1),
                          (6,   9, 10, _level + 2,  4, 1),
                          (7,  12, 21, _level + 1,  1, 1),
                          (8,  13, 16, _level + 2,  7, 1),
                          (9,  14, 15, _level + 3,  8, 1),
                          (10, 17, 20, _level + 2,  7, 1),
                          (11, 18, 19, _level + 3, 10, 1),

                          (12,  1, 22, _level + 0, None, 2),
                          (13,  2,  5, _level + 1, 12, 2),
                          (14,  3,  4, _level + 2, 13, 2),
                          (15,  6, 11, _level + 1, 12, 2),
                          (16,  7,  8, _level + 2, 15, 2),
                          (17,  9, 10, _level + 2, 15, 2),
                          (18, 12, 21, _level + 1, 12, 2),
                          (19, 13, 16, _level + 2, 18, 2),
                          (20, 14, 15, _level + 3, 19, 2),
                          (21, 17, 20, _level + 2, 18, 2),
                          (22, 18, 19, _level + 3, 21, 2)], self.result.all())

    def test_insert_node(self):
        """ Insert node with parent==6
        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19
            level     Insert node with parent_id == 6
            1                    1(1)24
                    _______________|_________________
                   |               |                 |
            2    2(2)5           6(4)13           14(7)23
                   |           ____|____          ___|____
                   |          |         |        |        |
            3    3(3)4      7(5)8    9(6)12  15(8)18   19(10)22
                                       |        |         |
            4                      10(23)11  16(9)17  20(11)21
        """
        node = self.model(parent_id=6)
        self.session.add(node)
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 24, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,   6, 13, _level + 1,  1, 1),
                (5,   7,  8, _level + 2,  4, 1),
                (6,   9, 12, _level + 2,  4, 1),
                (7,  14, 23, _level + 1,  1, 1),
                (8,  15, 18, _level + 2,  7, 1),
                (9,  16, 17, _level + 3,  8, 1),
                (10, 19, 22, _level + 2,  7, 1),
                (11, 20, 21, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2),

                (23, 10, 11, _level + 3, 6, 1)
            ],
            self.result.all())

    def test_insert_node_near_subtree(self):
        """ Insert node with parent==4
        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19
            level     Insert node with parent_id == 4
            1                    1(1)24
                    _______________|_____________________
                   |               |                     |
            2    2(2)5           6(4)13               14(7)23
                   |         ______|________           __|______
                   |        |      |        |         |         |
            3    3(3)4    7(5)8  9(6)10  11(23)12  15(8)18   19(10)22
                                                      |         |
            4                                      16(9)17   20(11)21
        """
        node = self.model(parent_id=4)
        self.session.add(node)
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 24, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,   6, 13, _level + 1,  1, 1),
                (5,   7,  8, _level + 2,  4, 1),
                (6,   9, 10, _level + 2,  4, 1),
                (7,  14, 23, _level + 1,  1, 1),
                (8,  15, 18, _level + 2,  7, 1),
                (9,  16, 17, _level + 3,  8, 1),
                (10, 19, 22, _level + 2,  7, 1),
                (11, 20, 21, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2),

                (23, 11, 12, _level + 2,  4, 1)
            ],
            self.result.all())

    def test_insert_after_node(self):
        pass

    def test_delete_node(self):
        """ Delete node(4)
        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Test delete node
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19
            level         Delete node == 4
            1                    1(1)16
                    _______________|_____
                   |                     |
            2    2(2)5                 6(7)15
                   |                     ^
            3    3(3)4            7(8)10   11(10)14
                                    |          |
            4                     8(9)9    12(11)13
        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 4).one()
        self.session.delete(node)
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 16, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (7,   6, 15, _level + 1,  1, 1),
                (8,   7, 10, _level + 2,  7, 1),
                (9,   8,  9, _level + 3,  8, 1),
                (10, 11, 14, _level + 2,  7, 1),
                (11, 12, 13, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ],
            self.result.all())

    def test_update_node(self):
        """ Set parent_id==5 for node(8)
        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Test update node
                1                    1(1)22
                        _______________|___________________
                       |               |                   |
                2    2(2)5           6(4)11             12(7)21
                       |               ^                   ^
                3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                      |          |
                4                                  14(9)15   18(11)19
            level               Move 8 - > 5
                1                     1(1)22
                         _______________|__________________
                        |               |                  |
                2     2(2)5           6(4)15            16(7)21
                        |               ^                  |
                3     3(3)4      7(5)12   13(6)14      17(10)20
                                   |                        |
                4                8(8)11                18(11)19
                                   |
                5                9(9)10
        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 8).one()
        node.parent_id = 5
        self.session.add(node)
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1, 1, 22,   _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,   6, 15, _level + 1,  1, 1),
                (5,   7, 12, _level + 2,  4, 1),
                (6,  13, 14, _level + 2,  4, 1),
                (7,  16, 21, _level + 1,  1, 1),
                (8,   8, 11, _level + 3,  5, 1),
                (9,   9, 10, _level + 4,  8, 1),
                (10, 17, 20, _level + 2,  7, 1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ],
            self.result.all())

        """ level               Move 8 - > 5
                1                     1(1)22
                         _______________|__________________
                        |               |                  |
                2     2(2)5           6(4)15            16(7)21
                        |               ^                  |
                3     3(3)4      7(5)12   13(6)14      17(10)20
                                   |                        |
                4                8(8)11                18(11)19
                                   |
                5                9(9)10
            level               Move 4 - > 2
                1                     1(1)22
                                ________|_____________
                               |                      |
                2            2(2)15                16(7)21
                           ____|_____                 |
                          |          |                |
                3       3(4)12    13(3)14         17(10)20
                          ^                           |
                4  4(5)9    10(6)11               18(11)19
                     |
                5  5(8)8
                     |
                6  6(9)7
        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 4).one()
        node.parent_id = 2
        self.session.add(node)
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2, 15, _level + 1,  1, 1),
                (3,  13, 14, _level + 2,  2, 1),
                (4,   3, 12, _level + 2,  2, 1),
                (5,   4,  9, _level + 3,  4, 1),
                (6,  10, 11, _level + 3,  4, 1),
                (7,  16, 21, _level + 1,  1, 1),
                (8,   5,  8, _level + 4,  5, 1),
                (9,   6,  7, _level + 5,  8, 1),
                (10, 17, 20, _level + 2,  7, 1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ],
            self.result.all())

        """ level               Move 4 - > 2
                1                     1(1)22
                                ________|_____________
                               |                      |
                2            2(2)15                16(7)21
                           ____|_____                 |
                          |          |                |
                3       3(4)12    13(3)14         17(10)20
                          ^                           |
                4   4(5)9   10(6)11               18(11)19
                      |
                5   5(8)8
                      |
                6   6(9)7
            level               Move 8 - > 10
                1                     1(1)22
                                ________|_____________
                               |                      |
                2            2(2)11                12(7)21
                         ______|_____                 |
                        |            |                |
                3     3(4)8        9(3)10         13(10)20
                      __|____                        _|______
                     |       |                      |        |
                4  4(5)5   6(6)7                 14(8)17  18(11)19
                                                    |
                5                                15(9)16
        """

        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 8).one()
        node.parent_id = 10
        self.session.add(node)
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2, 11, _level + 1,  1, 1),
                (3,   9, 10, _level + 2,  2, 1),
                (4,   3,  8, _level + 2,  2, 1),
                (5,   4,  5, _level + 3,  4, 1),
                (6,   6,  7, _level + 3,  4, 1),
                (7,  12, 21, _level + 1,  1, 1),
                (8,  14, 17, _level + 3, 10, 1),
                (9,  15, 16, _level + 4,  8, 1),
                (10, 13, 20, _level + 2,  7, 1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ],
            self.result.all())

    def test_rebuild(self):
        """ Rebuild tree with tree_id==1

        .. code::

            level      Nested sets w/o left & right (or broken left & right)
                1                     (1)
                        _______________|___________________
                       |               |                   |
                2     (2)             (4)                 (7)
                       |               ^                   ^
                3     (3)          (5)   (6)           (8)   (10)
                                                        |      |
                4                                      (9)   (11)


                level           Nested sets after rebuild
                1                    1(1)22
                        _______________|___________________
                       |               |                   |
                2    2(2)5           6(4)11             12(7)21
                       |               ^                   ^
                3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                      |          |
                4                                  14(9)15   18(11)19
        """

        self.session.query(self.model).update({
            self.model.left: 0,
            self.model.right: 0,
            self.model.level: 0
        })
        self.model.rebuild(self.session, 1)
        _level = self.model.get_default_level()
        self.assertEqual(
            self.result.all(),
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2,  5, _level + 1, 1,  1),
                (3,   3,  4, _level + 2, 2,  1),
                (4,   6, 11, _level + 1, 1,  1),
                (5,   7,  8, _level + 2, 4,  1),
                (6,   9, 10, _level + 2, 4,  1),
                (7,  12, 21, _level + 1, 1,  1),
                (8,  13, 16, _level + 2, 7,  1),
                (9,  14, 15, _level + 3, 8,  1),
                (10, 17, 20, _level + 2, 7,  1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  0,  0, 0, None, 2),
                (13,  0,  0, 0, 12, 2),
                (14,  0,  0, 0, 13, 2),
                (15,  0,  0, 0, 12, 2),
                (16,  0,  0, 0, 15, 2),
                (17,  0,  0, 0, 15, 2),
                (18,  0,  0, 0, 12, 2),
                (19,  0,  0, 0, 18, 2),
                (20,  0,  0, 0, 19, 2),
                (21,  0,  0, 0, 18, 2),
                (22,  0,  0, 0, 21, 2)
            ]
        )

        self.model.rebuild(self.session)
        self.assertEqual(
            self.result.all(),
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2,  5, _level + 1, 1,  1),
                (3,   3,  4, _level + 2, 2,  1),
                (4,   6, 11, _level + 1, 1,  1),
                (5,   7,  8, _level + 2, 4,  1),
                (6,   9, 10, _level + 2, 4,  1),
                (7,  12, 21, _level + 1, 1,  1),
                (8,  13, 16, _level + 2, 7,  1),
                (9,  14, 15, _level + 3, 8,  1),
                (10, 17, 20, _level + 2, 7,  1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ]
        )


================================================
FILE: sqlalchemy_mptt/tests/cases/get_node.py
================================================
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.


class GetNodes(object):
    def test_get_siblings(self):
        """
        Get siblings of node

        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Nested sets example
                1                    1(1)22                              (12)
                        _______________|___________________
                       |               |                   |
                2    2(2)5           6(4)11             12(7)21
                       |               ^                   ^
                3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                      |          |
                4                                  14(9)15   18(11)19

        """
        node10 = (
            self.session.query(self.model)
            .filter(self.model.get_pk_column() == 10)
            .one()
        )
        points = (
            self.session.query(self.model).filter(self.model.get_pk_column() == 8).all()
        )
        self.assertEqual(points, node10.get_siblings().all())  # flake8: noqa

        node9 = (
            self.session.query(self.model).filter(self.model.get_pk_column() == 9).one()
        )
        self.assertEqual([], node9.get_siblings().all())  # flake8: noqa

        node1 = (
            self.session.query(self.model).filter(self.model.get_pk_column() == 1).one()
        )
        points = (
            self.session.query(self.model).filter(self.model.get_pk_column() == 12).all()
        )
        self.assertEqual(points, node1.get_siblings().all())

    def test_get_children(self):
        """
        Get children of node

        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Nested sets example
                1                    1(1)22
                        _______________|___________________
                       |               |                   |
                2    2(2)5           6(4)11             12(7)21
                       |               ^                   ^
                3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                      |          |
                4                                  14(9)15   18(11)19

        """
        node7 = (
            self.session.query(self.model).filter(self.model.get_pk_column() == 7).one()
        )
        points = self.session.query(self.model).filter(self.model.parent_id == 7).all()
        self.assertEqual(points, node7.get_children().all())  # flake8: noqa

        node9 = (
            self.session.query(self.model).filter(self.model.get_pk_column() == 9).one()
        )
        self.assertEqual([], node9.get_children().all())  # flake8: noqa


================================================
FILE: sqlalchemy_mptt/tests/cases/get_tree.py
================================================
from sqlalchemy import asc


def get_obj(session, model, id):
    return session.query(model).filter(model.get_pk_column() == id).one()


class Tree(object):

    def test_get_empty_tree(self):
        """
            No rows in database.
        """
        self.session.query(self.model).delete()
        self.session.flush()
        tree = self.model.get_tree(self.session)
        self.assertEqual(tree, [])

    def test_get_empty_tree_with_custom_query(self):
        """
            No rows with id < 0.
        """
        query = lambda x: x.filter(self.model.get_pk_column() < 0)  # noqa
        tree = self.model.get_tree(self.session, query=query)
        self.assertEqual(tree, [])

    def test_get_tree(self):
        """.. note::

            See [source] for full example

        Return tree as list of dict

        .. code::

            tree = Tree.get_tree(self.session)
        """
        tree = self.model.get_tree(self.session)

        def go(id):
            return get_obj(self.session, self.model, id)

        reference_tree = [
            {'node': go(1),
             'children':
             [{'node': go(2),
               'children': [{'node': go(3)}]},
              {'node': go(4),
               'children': [{'node': go(5)},
                            {'node': go(6)}]},
              {'node': go(7),
               'children':
               [{'node': go(8), 'children': [{'node': go(9)}]},
                {'node': go(10), 'children': [{'node': go(11)}]}]}]},
            {'node': go(12),
             'children': [{'node': go(13), 'children': [{'node': go(14)}]},
                          {'node': go(15), 'children': [{'node': go(16)},
                                                        {'node': go(17)}]},
                          {'node': go(18),
                           'children': [{'node': go(19),
                                         'children': [{'node': go(20)}]},
                                        {'node': go(21),
                                         'children': [{'node': go(22)}]}]}]}]

        self.assertEqual(tree, reference_tree)

    def test_get_tree_count_query(self):
        """
        Count num of queries to the database.
        See https://github.com/uralbash/sqlalchemy_mptt/issues/39
        """
        # from datetime import datetime
        self.session.commit()

        # Get tree by for cycle
        self.start_query_counter()
        self.assertEqual(0, len(self.stmts))
        # startTime = datetime.now()
        self.model.get_tree(self.session)
        # delta = datetime.now() - startTime
        # print("Get tree: {!s:>26}".format(delta))
        self.assertEqual(1, len(self.stmts))
        self.stop_query_counter()

    def test_get_json_tree(self):
        """.. note::

            See [source] for full example

        Return tree as JSON of jqTree format

        .. code::

            tree = Tree.get_tree(self.session, json=True)
        """
        reference_tree = [
            {'children': [{'children': [{'id': 3, 'label': '<Node (3)>'}],
                           'id': 2, 'label': '<Node (2)>'},
                          {'children': [{'id': 5, 'label': '<Node (5)>'},
                                        {'id': 6, 'label': '<Node (6)>'}],
                           'id': 4, 'label': '<Node (4)>'},
                          {'children':
                           [{'children': [{'id': 9, 'label': '<Node (9)>'}],
                             'id': 8, 'label': '<Node (8)>'},
                            {'children': [{'id': 11, 'label': '<Node (11)>'}],
                             'id': 10, 'label': '<Node (10)>'}],
                           'id': 7, 'label': '<Node (7)>'}], 'id': 1,
             'label': '<Node (1)>'},
            {'children': [{'children': [{'id': 14, 'label': '<Node (14)>'}],
                           'id': 13, 'label': '<Node (13)>'},
                          {'children': [{'id': 16, 'label': '<Node (16)>'},
                                        {'id': 17, 'label': '<Node (17)>'}],
                           'id': 15, 'label': '<Node (15)>'},
                          {'children': [{'children':
                                         [{'id': 20, 'label': '<Node (20)>'}],
                                         'id': 19, 'label': '<Node (19)>'},
                                        {'children':
                                         [{'id': 22, 'label': '<Node (22)>'}],
                                         'id': 21, 'label': '<Node (21)>'}],
                           'id': 18, 'label': '<Node (18)>'}],
             'id': 12, 'label': '<Node (12)>'}]

        tree = self.model.get_tree(self.session, json=True)
        self.assertEqual(tree, reference_tree)

    def test_get_json_tree_with_custom_field(self):
        """.. note::

            See [source] for full example

        Return tree as JSON of jqTree format with additional field

        .. code-block:: python
            :linenos:

            def fields(node):
                return {'visible': node.visible}

            tree = Tree.get_tree(self.session, json=True, json_fields=fields)
        """
        self.maxDiff = None

        def fields(node):
            return {'visible': node.visible}

        reference_tree = [
            {'visible': None, 'children':
             [{'visible': True, 'children':
               [{'visible': True, 'id': 3, 'label': '<Node (3)>'}],
               'id': 2, 'label': '<Node (2)>'},
              {'visible': True, 'children':
               [{'visible': True, 'id': 5, 'label': '<Node (5)>'},
                {'visible': True, 'id': 6, 'label': '<Node (6)>'}],
               'id': 4, 'label': '<Node (4)>'},
              {'visible': True, 'children':
               [{'visible': True, 'children':
                 [{'visible': None, 'id': 9, 'label': '<Node (9)>'}],
                 'id': 8, 'label': '<Node (8)>'},
                {'visible': None, 'children':
                 [{'visible': None, 'id': 11, 'label': '<Node (11)>'}],
                 'id': 10, 'label': '<Node (10)>'}],
               'id': 7, 'label': '<Node (7)>'}],
             'id': 1, 'label': '<Node (1)>'},
            {'visible': None, 'children':
             [{'visible': None, 'children':
               [{'visible': None, 'id': 14, 'label': '<Node (14)>'}],
               'id': 13, 'label': '<Node (13)>'},
              {'visible': None, 'children':
               [{'visible': None, 'id': 16, 'label': '<Node (16)>'},
                {'visible': None, 'id': 17, 'label': '<Node (17)>'}],
               'id': 15, 'label': '<Node (15)>'},
              {'visible': None, 'children':
               [{'visible': None, 'children':
                 [{'visible': None, 'id': 20, 'label': '<Node (20)>'}],
                 'id': 19, 'label': '<Node (19)>'},
                {'visible': None, 'children':
                 [{'visible': None, 'id': 22, 'label': '<Node (22)>'}],
                 'id': 21, 'label': '<Node (21)>'}],
               'id': 18, 'label': '<Node (18)>'}],
             'id': 12, 'label': '<Node (12)>'}]

        tree = self.model.get_tree(self.session, json=True, json_fields=fields)
        self.assertEqual(tree, reference_tree)

    def test_leftsibling_in_level(self):
        """ Node to the left of the current node at the same level

        .. code::

            level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

            level1 = [1]
            level2 = [2, 4, 7]
            level3 = [3, 5, 6, 8, 10]
            level4 = [9, 11]

            leftsibling_in_level_of_node_3 = None
            leftsibling_in_level_of_node_5 = 3
            leftsibling_in_level_of_node_6 = 5
            leftsibling_in_level_of_node_8 = 6
            leftsibling_in_level_of_node_11 = 9
        """
        q = self.session.query(self.model)
        node3 = q.filter(self.model.get_pk_column() == 3).one()
        node5 = q.filter(self.model.get_pk_column() == 5).one()
        node6 = q.filter(self.model.get_pk_column() == 6).one()
        node8 = q.filter(self.model.get_pk_column() == 8).one()
        node10 = q.filter(self.model.get_pk_column() == 10).one()

        pk_name = self.model.get_pk_name()
        pk_column_name = self.model.get_pk_column().name

        self.assertEqual(
            getattr(node10.leftsibling_in_level(), pk_column_name),
            getattr(node8, pk_name)
        )

        self.assertEqual(
            getattr(node8.leftsibling_in_level(), pk_column_name),
            getattr(node6, pk_name)
        )
        self.assertEqual(
            getattr(node6.leftsibling_in_level(), pk_column_name),
            getattr(node5, pk_name)
        )
        self.assertEqual(node3.leftsibling_in_level(), None)

    def test_drilldown_tree(self):
        """
        .. code::

            level           Nested sets example
            1                    1(1)22       ---------------------
                    _______________|_________|_________            |
                   |               |         |         |           |
            2    2(2)5           6(4)11      |      12(7)21        |
                   |               ^         |         ^           |
            3    3(3)4       7(5)8   9(6)10  | 13(8)16   17(10)20  |
                                             |    |          |     |
            4                                | 14(9)15   18(11)19  |
                                             |                     |
                                              ---------------------
        """
        def go(id):
            return get_obj(self.session, self.model, id)

        node = go(7)
        tree = node.drilldown_tree(self.session)
        reference_tree = [
            {'node': go(7),
             'children': [
                 {'node': go(8),
                  'children': [
                      {'node': go(9)}]},
                 {'node': go(10),
                  'children': [
                      {'node': go(11)}]}]
             }
        ]
        self.assertEqual(tree, reference_tree)

    def test_drilldown_tree_without_session(self):
        def go(id):
            return get_obj(self.session, self.model, id)
        node = go(7)
        tree = node.drilldown_tree()
        reference_tree = [
            {'node': go(7),
             'children': [
                 {'node': go(8),
                  'children': [
                      {'node': go(9)}]},
                 {'node': go(10),
                  'children': [
                      {'node': go(11)}]}]
             }
        ]
        self.assertEqual(tree, reference_tree)

    def test_path_to_root(self):
        r"""Generate path from a leaf or intermediate node to the root.

        For example:

            node11.path_to_root()

            .. code::

                level           Nested sets example

                                 -----------------------------------------
                1               |    1(1)22                               |
                        ________|______|_____________________             |
                       |        |      |                     |            |
                       |          -----+---------            |            |
                2    2(2)5           6(4)11      | --     12(7)21         |
                       |               ^             |    /     \         |
                3    3(3)4       7(5)8   9(6)10      ---/----    \        |
                                                    13(8)16 |  17(10)20   |
                                                       |    |     |       |
                4                                   14(9)15 | 18(11)19    |
                                                            |             |
                                                             -------------
        """
        def go(id):
            return get_obj(self.session, self.model, id)

        node11 = go(11)
        node8 = go(8)
        node6 = go(6)
        node1 = go(1)
        path_11_to_root = node11.path_to_root(self.session).all()
        path_8_to_root = node8.path_to_root(self.session).all()
        path_6_to_root = node6.path_to_root(self.session).all()
        path_1_to_root = node1.path_to_root(self.session).all()
        self.assertEqual(path_11_to_root, [go(11), go(10), go(7), go(1)])
        self.assertEqual(path_8_to_root, [go(8), go(7), go(1)])
        self.assertEqual(path_6_to_root, [go(6), go(4), go(1)])
        self.assertEqual(path_1_to_root, [go(1)])

        asc_path_11_to_root = node11.path_to_root(self.session, order=asc).all()
        self.assertEqual(asc_path_11_to_root, [go(1), go(7), go(10), go(11)])


================================================
FILE: sqlalchemy_mptt/tests/cases/initialize.py
================================================
from sqlalchemy.exc import IntegrityError


class Initialize(object):

    def test_tree_orm_initialize(self):
        pk_name = self.model.get_pk_name()
        t0 = self.model(**{pk_name: 30})
        t1 = self.model(**{pk_name: 31, 'parent': t0})
        t2 = self.model(**{pk_name: 32, 'parent': t1})
        t3 = self.model(**{pk_name: 33, 'parent': t1})

        self.session.add(t0)
        self.session.flush()

        self.assertEqual(t0.left, 1)
        self.assertEqual(t0.right, 8)

        self.assertEqual(t1.left, 2)
        self.assertEqual(t1.right, 7)

        self.assertEqual(t2.left, 3)
        self.assertEqual(t2.right, 4)

        self.assertEqual(t3.left, 5)
        self.assertEqual(t3.right, 6)

        t0 = self.model(**{pk_name: 40})
        t1 = self.model(**{pk_name: 41, 'parent': t0})
        t2 = self.model(**{pk_name: 42, 'parent': t1})
        t3 = self.model(**{pk_name: 43, 'parent': t2})
        t4 = self.model(**{pk_name: 44, 'parent': t3})
        t5 = self.model(**{pk_name: 45, 'parent': t4})

        self.session.add(t3)
        self.session.flush()

        self.assertEqual(t0.left, 1)
        self.assertEqual(t0.right, 12)

        self.assertEqual(t1.left, 2)
        self.assertEqual(t1.right, 11)

        self.assertEqual(t2.left, 3)
        self.assertEqual(t2.right, 10)

        self.assertEqual(t3.left, 4)
        self.assertEqual(t3.right, 9)

        self.assertEqual(t4.left, 5)
        self.assertEqual(t4.right, 8)

        self.assertEqual(t5.left, 6)
        self.assertEqual(t5.right, 7)

    def test_flush_with_transient_nodes_present(self):
        """
        https://github.com/uralbash/sqlalchemy_mptt/issues/34
        """
        pk_name = self.model.get_pk_name()
        transient_node = self.model(**{pk_name: 1, 'parent': None})
        self.session.add(transient_node)
        try:
            self.session.flush()
        except IntegrityError:
            pass
        self.session.rollback()
        self.session.add(self.model(**{pk_name: 46, 'parent': None}))
        self.session.flush()

    def test_tree_initialize(self):
        """ Initial state of the trees

        .. code::

            level               Tree 1
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19


            level               Tree 2
            1                    1(12)22
                    _______________|___________________
                   |               |                   |
            2    2(13)5         6(15)11              12(18)21
                   |               ^                    ^
            3    3(14)4     7(16)8   9(17)10   13(19)16   17(21)20
                                                   |          |
            4                                  14(20)15   18(22)19

        """
        _level = self.model.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,   6, 11, _level + 1,  1, 1),
                (5,   7,  8, _level + 2,  4, 1),
                (6,   9, 10, _level + 2,  4, 1),
                (7,  12, 21, _level + 1,  1, 1),
                (8,  13, 16, _level + 2,  7, 1),
                (9,  14, 15, _level + 3,  8, 1),
                (10, 17, 20, _level + 2,  7, 1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ],
            self.result.all())  # flake8: noqa


================================================
FILE: sqlalchemy_mptt/tests/cases/integrity.py
================================================
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
from sqlalchemy.sql import func


class DataIntegrity(object):

    def test_left_is_always_less_than_right(self):
        """ The left key is always less than the right.

        The following example should return an empty result.

        .. code-block:: sql

            SELECT id FROM tree WHERE left >= right
        """
        table = self.model
        nop = self.session.query(table).filter(table.left >= table.right).all()
        self.assertEqual(nop, [])

    def test_lowest_left_is_always_1(self):
        """ The lowest left key is always 1.

        The following example should return 1.

        .. code-block:: sql

            SELECT MIN(left) FROM tree
        """
        table = self.model
        one = self.session.query(func.min(table.left)).scalar()
        self.assertEqual(one, 1)

    def test_greatest_right_is_always_double_number_of_nodes(self):
        """ The greatest right key is always double the number of nodes.

        The following example should match COUNT(id) * 2 equal MAX(right).

        .. code-block:: sql

            SELECT COUNT(id), MAX(right) FROM tree
        """
        table = self.model
        result = self.session.query(
            func.count(table.get_pk_name()),
            func.max(table.right)).group_by(table.tree_id).all()
        for tree in result:
            self.assertEqual(tree[0] * 2, tree[1])

    def test_right_minus_left_always_odd(self):
        """ Difference between left and right keys are always an odd number.

        The following example should return an empty result.

        .. code-block:: sql

            SELECT MOD((right - left) / 2) AS modulo
            FROM tree WHERE modulo = 0
        """
        table = self.model
        modulo = (table.right - table.left) % 2
        nop = self.session.query(table).filter(modulo == 0).all()
        self.assertEqual(nop, [])

    def test_level_odd_when_left_odd_and_vice_versa(self):
        """ If the node number is odd then the left key is always an odd
        number, and the same goes for the even numbers.

        The following example should return an empty result.

        .. code-block:: sql

            SELECT id, MOD((left - level + 2) / 2) AS modulo FROM tree
            WHERE modulo = 1
        """
        table = self.model
        level_delta = pow(0, table.get_default_level() % 2)
        modulo = (table.left - table.level + level_delta + 2) % 2
        nop = self.session.query(table).filter(modulo == 1).all()
        self.assertEqual(nop, [])

    def test_left_and_right_always_unique_number(self):
        """ left and right always is unique.
        """
        table = self.model
        left = self.session.query(table.left)
        right = self.session.query(table.right)
        keys = [x[0] for x in left.union(right)]
        self.assertEqual(len(keys), len(set(keys)))

    def test_hierarchy_structure(self):
        """ Nodes with left < self and right > self are considered ancestors,
        while nodes with left > self and right < self are considered
        descendants
        """
        table = self.model
        pivot = self.session.query(table).filter(
            table.right - table.left != 1
        ).filter(table.parent_id != None).first()  # noqa

        # Exclusive Tests
        ancestors = self.session.query(table).filter(
            table.is_ancestor_of(pivot)
        ).all()
        for ancestor in ancestors:
            self.assertTrue(ancestor.is_ancestor_of(pivot))
        self.assertNotIn(pivot, ancestors)

        descendants = self.session.query(table).filter(
            table.is_descendant_of(pivot)
        ).all()
        for descendant in descendants:
            self.assertTrue(descendant.is_descendant_of(pivot))
        self.assertNotIn(pivot, descendants)

        self.assertEqual(set(), set(ancestors).intersection(set(descendants)))

        # Inclusive Tests - because sometimes inclusivity is nice, like with
        # self joins
        ancestors = self.session.query(table).filter(
            table.is_ancestor_of(pivot, inclusive=True)
        ).all()
        for ancestor in ancestors:
            self.assertTrue(ancestor.is_ancestor_of(pivot, inclusive=True))
        self.assertIn(pivot, ancestors)

        descendants = self.session.query(table).filter(
            table.is_descendant_of(pivot, inclusive=True)
        ).all()
        for descendant in descendants:
            self.assertTrue(descendant.is_descendant_of(pivot, inclusive=True))
        self.assertIn(pivot, descendants)

        self.assertEqual(
            set([pivot]),
            set(ancestors).intersection(set(descendants))
        )


================================================
FILE: sqlalchemy_mptt/tests/cases/move_node.py
================================================
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.
import os


class MoveBefore(object):

    def test_move_before_to_top_level(self):
        """ For example move node(4) before node(1)

        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Nested sets example
                1                    1(1)22
                        _______________|___________________
                       |               |                   |
                2    2(2)5           6(4)11             12(7)21
                       |               ^                   ^
                3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                      |          |
                4                                  14(9)15   18(11)19

            level            move 4 before 1
                1         1(4)6              1(1)16
                            ^           _______|_______
                      2(5)3   4(6)5    |               |
                2                    2(2)5           6(7)15
                                       |               ^
                3                    3(3)4      7(8)10   11(10)14
                                                  |          |
                4                               8(9)9    12(11)13

        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 4).one()
        node.move_before(1)
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 16, _level + 0, None, 2),
                (2,   2,  5, _level + 1,  1, 2),
                (3,   3,  4, _level + 2,  2, 2),

                (4,   1,  6, _level + 0,  None, 1),
                (5,   2,  3, _level + 1,  4, 1),
                (6,   4,  5, _level + 1,  4, 1),

                (7,   6, 15, _level + 1,  1, 2),
                (8,   7, 10, _level + 2,  7, 2),
                (9,   8,  9, _level + 3,  8, 2),
                (10, 11, 14, _level + 2,  7, 2),
                (11, 12, 13, _level + 3, 10, 2),

                (12,  1, 22, _level + 0, None, 3),
                (13,  2,  5, _level + 1, 12, 3),
                (14,  3,  4, _level + 2, 13, 3),
                (15,  6, 11, _level + 1, 12, 3),
                (16,  7,  8, _level + 2, 15, 3),
                (17,  9, 10, _level + 2, 15, 3),
                (18, 12, 21, _level + 1, 12, 3),
                (19, 13, 16, _level + 2, 18, 3),
                (20, 14, 15, _level + 3, 19, 3),
                (21, 17, 20, _level + 2, 18, 3),
                (22, 18, 19, _level + 3, 21, 3)
            ],
            self.result.all())  # flake8: noqa

    def test_move_one_tree_before_another(self):
        """ For example move node(12) before node(1)

        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

                                        <--------------------------------
                                                                        |
            level           Nested sets tree1                           |
                1                    1(1)22                             |
                        _______________|___________________             |
                       |               |                   |            |
                2    2(2)5           6(4)11             12(7)21         |
                       |               ^                   ^            |
                3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20   |
                                                      |          |      |
                4                                  14(9)15   18(11)19   |
                                                                        |
                level           Nested sets tree2                       |
                1                    1(12)22 ----------------------------
                        _______________|___________________
                       |               |                   |
                2    2(13)5         6(15)11             12(18)21
                       |               ^                    ^
                3    3(14)4     7(16)8   9(17)10   13(19)16   17(21)20
                                                       |          |
                4                                  14(20)15   18(22)19

        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 12).one()
        node.move_before("1")
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 2),
                (2,   2,  5, _level + 1,  1, 2),
                (3,   3,  4, _level + 2,  2, 2),
                (4,   6, 11, _level + 1,  1, 2),
                (5,   7,  8, _level + 2,  4, 2),
                (6,   9, 10, _level + 2,  4, 2),
                (7,  12, 21, _level + 1,  1, 2),
                (8,  13, 16, _level + 2,  7, 2),
                (9,  14, 15, _level + 3,  8, 2),
                (10, 17, 20, _level + 2,  7, 2),
                (11, 18, 19, _level + 3, 10, 2),

                (12,  1, 22, _level + 0, None, 1),
                (13,  2,  5, _level + 1, 12, 1),
                (14,  3,  4, _level + 2, 13, 1),
                (15,  6, 11, _level + 1, 12, 1),
                (16,  7,  8, _level + 2, 15, 1),
                (17,  9, 10, _level + 2, 15, 1),
                (18, 12, 21, _level + 1, 12, 1),
                (19, 13, 16, _level + 2, 18, 1),
                (20, 14, 15, _level + 3, 19, 1),
                (21, 17, 20, _level + 2, 18, 1),
                (22, 18, 19, _level + 3, 21, 1)
            ],
            self.result.all())

    def test_move_before_function(self):
        """ For example move node(8) before node(4)

        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

            level           move 8 before 4
            1                    1(1)22
                    _______________|___________________
                   |        |            |             |
            2    2(2)5    6(8)9       10(4)15       16(7)21
                   |        |            ^             |
            3    3(3)4    7(9)8   11(5)12 13(6)14  17(10)20
                                                       |
            4                                      18(11)19

        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 8).one()
        node.move_before("4")
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,  10, 15, _level + 1,  1, 1),
                (5,  11, 12, _level + 2,  4, 1),
                (6,  13, 14, _level + 2,  4, 1),
                (7,  16, 21, _level + 1,  1, 1),
                (8,   6,  9, _level + 1,  1, 1),
                (9,   7,  8, _level + 2,  8, 1),
                (10, 17, 20, _level + 2,  7, 1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ],
            self.result.all())

    def test_move_one_tree_before_other_tree(self):
        self.fixture.add(
            self.model,
            os.path.join('fixtures', 'tree_3.json')
        )
        self.session.commit()
        self.maxDiff = None

        node = self.session.query(self.model).\
            filter(self.model.get_pk_column() == 12).one()
        node.move_before("1")
        _level = node.get_default_level()
        self.assertEqual(
            self.result.all(),
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 2),
                (2,   2,  5, _level + 1,  1, 2),
                (3,   3,  4, _level + 2,  2, 2),
                (4,   6, 11, _level + 1,  1, 2),
                (5,   7,  8, _level + 2,  4, 2),
                (6,   9, 10, _level + 2,  4, 2),
                (7,  12, 21, _level + 1,  1, 2),
                (8,  13, 16, _level + 2,  7, 2),
                (9,  14, 15, _level + 3,  8, 2),
                (10, 17, 20, _level + 2,  7, 2),
                (11, 18, 19, _level + 3, 10, 2),

                (12,  1, 22, _level + 0, None, 1),
                (13,  2,  5, _level + 1, 12, 1),
                (14,  3,  4, _level + 2, 13, 1),
                (15,  6, 11, _level + 1, 12, 1),
                (16,  7,  8, _level + 2, 15, 1),
                (17,  9, 10, _level + 2, 15, 1),
                (18, 12, 21, _level + 1, 12, 1),
                (19, 13, 16, _level + 2, 18, 1),
                (20, 14, 15, _level + 3, 19, 1),
                (21, 17, 20, _level + 2, 18, 1),
                (22, 18, 19, _level + 3, 21, 1),

                (23,  1, 22, _level + 0, None, 4),
                (24,  2,  5, _level + 1, 23, 4),
                (25,  3,  4, _level + 2, 24, 4),
                (26,  6, 11, _level + 1, 23, 4),
                (27,  7,  8, _level + 2, 26, 4),
                (28,  9, 10, _level + 2, 26, 4),
                (29, 12, 21, _level + 1, 23, 4),
                (30, 13, 16, _level + 2, 29, 4),
                (31, 14, 15, _level + 3, 30, 4),
                (32, 17, 20, _level + 2, 29, 4),
                (33, 18, 19, _level + 3, 32, 4)
            ]
        )

        node = self.session.query(self.model).\
            filter(self.model.get_pk_column() == 23).one()
        node.move_before("1")

        _level = node.get_default_level()
        self.assertEqual(
            self.result.all(),
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 3),
                (2,   2,  5, _level + 1,  1, 3),
                (3,   3,  4, _level + 2,  2, 3),
                (4,   6, 11, _level + 1,  1, 3),
                (5,   7,  8, _level + 2,  4, 3),
                (6,   9, 10, _level + 2,  4, 3),
                (7,  12, 21, _level + 1,  1, 3),
                (8,  13, 16, _level + 2,  7, 3),
                (9,  14, 15, _level + 3,  8, 3),
                (10, 17, 20, _level + 2,  7, 3),
                (11, 18, 19, _level + 3, 10, 3),

                (12,  1, 22, _level + 0, None, 1),
                (13,  2,  5, _level + 1, 12, 1),
                (14,  3,  4, _level + 2, 13, 1),
                (15,  6, 11, _level + 1, 12, 1),
                (16,  7,  8, _level + 2, 15, 1),
                (17,  9, 10, _level + 2, 15, 1),
                (18, 12, 21, _level + 1, 12, 1),
                (19, 13, 16, _level + 2, 18, 1),
                (20, 14, 15, _level + 3, 19, 1),
                (21, 17, 20, _level + 2, 18, 1),
                (22, 18, 19, _level + 3, 21, 1),

                (23,  1, 22, _level + 0, None, 2),
                (24,  2,  5, _level + 1, 23, 2),
                (25,  3,  4, _level + 2, 24, 2),
                (26,  6, 11, _level + 1, 23, 2),
                (27,  7,  8, _level + 2, 26, 2),
                (28,  9, 10, _level + 2, 26, 2),
                (29, 12, 21, _level + 1, 23, 2),
                (30, 13, 16, _level + 2, 29, 2),
                (31, 14, 15, _level + 3, 30, 2),
                (32, 17, 20, _level + 2, 29, 2),
                (33, 18, 19, _level + 3, 32, 2)
            ]
        )

        node = self.session.query(self.model).\
            filter(self.model.get_pk_column() == 1).one()
        node.move_before("12")

        _level = node.get_default_level()
        self.assertEqual(
            self.result.all(),
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,   6, 11, _level + 1,  1, 1),
                (5,   7,  8, _level + 2,  4, 1),
                (6,   9, 10, _level + 2,  4, 1),
                (7,  12, 21, _level + 1,  1, 1),
                (8,  13, 16, _level + 2,  7, 1),
                (9,  14, 15, _level + 3,  8, 1),
                (10, 17, 20, _level + 2,  7, 1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2),

                (23,  1, 22, _level + 0, None, 3),
                (24,  2,  5, _level + 1, 23, 3),
                (25,  3,  4, _level + 2, 24, 3),
                (26,  6, 11, _level + 1, 23, 3),
                (27,  7,  8, _level + 2, 26, 3),
                (28,  9, 10, _level + 2, 26, 3),
                (29, 12, 21, _level + 1, 23, 3),
                (30, 13, 16, _level + 2, 29, 3),
                (31, 14, 15, _level + 3, 30, 3),
                (32, 17, 20, _level + 2, 29, 3),
                (33, 18, 19, _level + 3, 32, 3)
            ]
        )

    def test_move_before_to_other_tree(self):
        """ For example move node(8) before node(15)

        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           Move 8 before 15
            1                    1(1)18
                     _______________|___________________
                    |               |                   |
            2     2(2)5           6(4)11             12(7)17
                    |               ^                   |
            3     3(3)4       7(5)8   9(6)10        13(10)16
                                                        |
            4                                       14(11)15

            level
            1                    1(12)26
                     _______________|______________________________
                    |         |               |                    |
            2    2(13)5     6(8)9         10(15)15             16(18)25
                    |         |               ^                    ^
            3    3(14)4     7(9)8    11(16)12  13(17)14   17(19)20   21(21)24
                                                              |          |
            4                                             18(20)19   22(22)23

        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 8).one()
        node.move_before("15")
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 18, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,   6, 11, _level + 1,  1, 1),
                (5,   7,  8, _level + 2,  4, 1),
                (6,   9, 10, _level + 2,  4, 1),
                (7,  12, 17, _level + 1,  1, 1),

                (8,   6,  9, _level + 1,  12, 2),
                (9,   7,  8, _level + 2,   8, 2),

                (10, 13, 16, _level + 2,  7, 1),
                (11, 14, 15, _level + 3, 10, 1),

                (12,  1, 26, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15, 10, 15, _level + 1, 12, 2),
                (16, 11, 12, _level + 2, 15, 2),
                (17, 13, 14, _level + 2, 15, 2),
                (18, 16, 25, _level + 1, 12, 2),
                (19, 17, 20, _level + 2, 18, 2),
                (20, 18, 19, _level + 3, 19, 2),
                (21, 21, 24, _level + 2, 18, 2),
                (22, 22, 23, _level + 3, 21, 2)
            ],
            self.result.all()
        )


class MoveAfter(object):

    def test_move_after_function(self):
        """ For example move node(8) after node(5)

        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

                level               Initial state
                    1                    1(1)22
                            _______________|___________________
                           |               |                   |
                    2    2(2)5           6(4)11             12(7)21
                           |               ^                   ^
                    3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                          |          |
                    4                                  14(9)15   18(11)19

                level               Move 8 after 5
                    1                     1(1)22
                            _______________|__________________
                           |               |                  |
                    2     2(2)5           6(4)15            16(7)21
                            |               ^                  |
                    3     3(3)4    7(5)8  9(8)12  13(6)14   17(10)20
                                            |                  |
                    4                    10(9)11            18(11)19

        """
        node = self.session.query(self.model)\
            .filter(self.model.get_pk_column() == 8).one()
        node.move_after("5")
        _level = node.get_default_level()
        self.assertEqual(
            [
                # id lft rgt lvl parent tree
                (1,   1, 22, _level + 0, None, 1),
                (2,   2,  5, _level + 1,  1, 1),
                (3,   3,  4, _level + 2,  2, 1),
                (4,   6, 15, _level + 1,  1, 1),
                (5,   7,  8, _level + 2,  4, 1),
                (6,  13, 14, _level + 2,  4, 1),
                (7,  16, 21, _level + 1,  1, 1),
                (8,   9, 12, _level + 2,  4, 1),
                (9,  10, 11, _level + 3,  8, 1),
                (10, 17, 20, _level + 2,  7, 1),
                (11, 18, 19, _level + 3, 10, 1),

                (12,  1, 22, _level + 0, None, 2),
                (13,  2,  5, _level + 1, 12, 2),
                (14,  3,  4, _level + 2, 13, 2),
                (15,  6, 11, _level + 1, 12, 2),
                (16,  7,  8, _level + 2, 15, 2),
                (17,  9, 10, _level + 2, 15, 2),
                (18, 12, 21, _level + 1, 12, 2),
                (19, 13, 16, _level + 2, 18, 2),
                (20, 14, 15, _level + 3, 19, 2),
                (21, 17, 20, _level + 2, 18, 2),
                (22, 18, 19, _level + 3, 21, 2)
            ],
            self.result.all()
        )

    def test_move_to_toplevel_where_much_trees_from_right_side(self):
        """ Move 20 after 1

        initial state of the tree :mod:`sqlalchemy_mptt.tests.add_mptt_tree`

        .. code::

            level           tree_id = 1
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

            level           tree_id = 2
            1                     1(15)6
                                     ^
            2                 2(16)3   4(17)5

            level           tree_id = 3
            1                    1(12)16
                     _______
Download .txt
gitextract_78avwcyy/

├── .coveragerc
├── .editorconfig
├── .flake8
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── codeql.yml
│       ├── publish.yml
│       └── run-tests.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── .rstcheck.cfg
├── CHANGES.rst
├── CHANGES_OLD.rst
├── CONTRIBUTORS.txt
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── RELEASING.rst
├── docs/
│   ├── CONTRIBUTING.rst
│   ├── Makefile
│   ├── conf.py
│   ├── crud.rst
│   ├── index.rst
│   ├── initialize.rst
│   ├── make.bat
│   ├── sqlalchemy_mptt.rst
│   └── tut_flask.rst
├── noxfile.py
├── pyproject.toml
├── requirements-doctest.txt
├── requirements-test.txt
├── requirements.txt
├── setup.py
├── sqlalchemy_mptt/
│   ├── __init__.py
│   ├── events.py
│   ├── mixins.py
│   ├── sqlalchemy_compat.py
│   └── tests/
│       ├── __init__.py
│       ├── cases/
│       │   ├── __init__.py
│       │   ├── edit_node.py
│       │   ├── get_node.py
│       │   ├── get_tree.py
│       │   ├── initialize.py
│       │   ├── integrity.py
│       │   └── move_node.py
│       ├── fixtures/
│       │   ├── tmp_tree.json
│       │   ├── tree.json
│       │   └── tree_3.json
│       ├── test_events.py
│       ├── test_inheritance.py
│       ├── test_mixins.py
│       └── test_stateful.py
└── test.sh
Download .txt
SYMBOL INDEX (180 symbols across 16 files)

FILE: noxfile.py
  function lint (line 52) | def lint(session):
  function parametrize_test_versions (line 61) | def parametrize_test_versions():
  function install_dependencies (line 99) | def install_dependencies(session, session_name, sqlalchemy_version):
  function test (line 110) | def test(session, sqlalchemy):
  function doctest (line 132) | def doctest(session, sqlalchemy):
  function dev (line 139) | def dev(session):
  function build (line 152) | def build(session):

FILE: setup.py
  function read (line 8) | def read(name):

FILE: sqlalchemy_mptt/events.py
  function _insert_subtree (line 25) | def _insert_subtree(
  function _get_tree_table (line 73) | def _get_tree_table(mapper):
  function mptt_before_insert (line 79) | def mptt_before_insert(mapper, connection, instance):
  function mptt_before_delete (line 135) | def mptt_before_delete(mapper, connection, instance, delete=True):
  function mptt_before_update (line 189) | def mptt_before_update(mapper, connection, instance):
  class _WeakDefaultDict (line 434) | class _WeakDefaultDict(weakref.WeakKeyDictionary):
    method __getitem__ (line 438) | def __getitem__(self, key):
  class TreesManager (line 446) | class TreesManager(object):
    method __init__ (line 450) | def __init__(self, base_class):
    method register_events (line 455) | def register_events(self, remove=False):
    method register_factory (line 468) | def register_factory(self, sessionmaker):
    method before_insert (line 509) | def before_insert(self, mapper, connection, instance):
    method before_update (line 514) | def before_update(self, mapper, connection, instance):
    method before_delete (line 519) | def before_delete(self, mapper, connection, instance):
    method after_flush_postexec (line 524) | def after_flush_postexec(self, session, context):
    method get_parent_value (line 548) | def get_parent_value(instance):
    method expire_session_for_children (line 552) | def expire_session_for_children(session, instance):

FILE: sqlalchemy_mptt/mixins.py
  class BaseNestedSets (line 30) | class BaseNestedSets(object):
    method __declare_first__ (line 77) | def __declare_first__(cls):
    method get_default_level (line 81) | def get_default_level(cls):
    method get_pk_name (line 89) | def get_pk_name(cls):
    method get_pk_column (line 93) | def get_pk_column(cls):
    method get_pk_value (line 96) | def get_pk_value(self):
    method tree_id (line 100) | def tree_id(cls):
    method parent_id (line 104) | def parent_id(cls):
    method parent (line 118) | def parent(self):
    method left (line 132) | def left(cls):
    method right (line 136) | def right(cls):
    method level (line 140) | def level(cls):
    method is_ancestor_of (line 144) | def is_ancestor_of(self, other, inclusive=False):
    method is_descendant_of (line 166) | def is_descendant_of(self, other, inclusive=False):
    method move_inside (line 177) | def move_inside(self, parent_id):
    method move_after (line 190) | def move_after(self, node_id):
    method move_before (line 200) | def move_before(self, node_id):
    method leftsibling_in_level (line 217) | def leftsibling_in_level(self):
    method _node_to_dict (line 238) | def _node_to_dict(cls, node, json, json_fields):
    method _base_query (line 252) | def _base_query(cls, session=None):
    method _base_query_obj (line 255) | def _base_query_obj(self, session=None):
    method _base_order (line 261) | def _base_order(cls, query, order=asc):
    method get_tree (line 269) | def get_tree(cls, session=None, json=False, json_fields=None, query=No...
    method _drilldown_query (line 329) | def _drilldown_query(self, nodes=None):
    method drilldown_tree (line 335) | def drilldown_tree(self, session=None, json=False, json_fields=None):
    method path_to_root (line 372) | def path_to_root(self, session=None, order=desc):
    method get_siblings (line 404) | def get_siblings(self, include_self=False, session=None):
    method get_children (line 445) | def get_children(self, session=None):
    method rebuild_tree (line 482) | def rebuild_tree(cls, session, tree_id):
    method rebuild (line 534) | def rebuild(cls, session, tree_id=None):

FILE: sqlalchemy_mptt/sqlalchemy_compat.py
  class LegacySQLAlchemyAPI (line 10) | class LegacySQLAlchemyAPI:
    method declarative_base (line 14) | def declarative_base(*args, **kwargs):
    method select (line 19) | def select(*args, **kwargs):
    method case (line 23) | def case(*args, **kwargs):
    method get (line 27) | def get(session, model, id):
  class ModernSQLAlchemyAPI (line 31) | class ModernSQLAlchemyAPI:
    method declarative_base (line 35) | def declarative_base(*args, **kwargs):
    method select (line 40) | def select(*args, **kwargs):
    method case (line 44) | def case(*args, **kwargs):
    method get (line 48) | def get(session, model, id):

FILE: sqlalchemy_mptt/tests/__init__.py
  function failures_expected_on (line 59) | def failures_expected_on(*, sqlalchemy_versions=[], python_versions=[]):
  class DatabaseSetupMixin (line 79) | class DatabaseSetupMixin(BaseType):
    method setUp (line 82) | def setUp(self):
    method tearDown (line 90) | def tearDown(self):
  class Fixtures (line 97) | class Fixtures(object):
    method __init__ (line 98) | def __init__(self, session):
    method add (line 101) | def add(self, model, fixtures):
  class TreeTestingMixin (line 112) | class TreeTestingMixin(
    method catch_queries (line 126) | def catch_queries(self, conn, cursor, statement, *args):
    method start_query_counter (line 129) | def start_query_counter(self):
    method stop_query_counter (line 135) | def stop_query_counter(self):
    method setUp (line 140) | def setUp(self):
    method test_session_expire_for_move_after_to_new_tree (line 156) | def test_session_expire_for_move_after_to_new_tree(self):

FILE: sqlalchemy_mptt/tests/cases/edit_node.py
  class Changes (line 1) | class Changes(object):
    method test_update_wo_move (line 3) | def test_update_wo_move(self):
    method test_update_wo_move_like_sacrud_save (line 53) | def test_update_wo_move_like_sacrud_save(self):
    method test_insert_node (line 100) | def test_insert_node(self):
    method test_insert_node_near_subtree (line 160) | def test_insert_node_near_subtree(self):
    method test_insert_after_node (line 220) | def test_insert_after_node(self):
    method test_delete_node (line 223) | def test_delete_node(self):
    method test_update_node (line 278) | def test_update_node(self):
    method test_rebuild (line 460) | def test_rebuild(self):

FILE: sqlalchemy_mptt/tests/cases/get_node.py
  class GetNodes (line 10) | class GetNodes(object):
    method test_get_siblings (line 11) | def test_get_siblings(self):
    method test_get_children (line 53) | def test_get_children(self):

FILE: sqlalchemy_mptt/tests/cases/get_tree.py
  function get_obj (line 4) | def get_obj(session, model, id):
  class Tree (line 8) | class Tree(object):
    method test_get_empty_tree (line 10) | def test_get_empty_tree(self):
    method test_get_empty_tree_with_custom_query (line 19) | def test_get_empty_tree_with_custom_query(self):
    method test_get_tree (line 27) | def test_get_tree(self):
    method test_get_tree_count_query (line 67) | def test_get_tree_count_query(self):
    method test_get_json_tree (line 85) | def test_get_json_tree(self):
    method test_get_json_tree_with_custom_field (line 126) | def test_get_json_tree_with_custom_field(self):
    method test_leftsibling_in_level (line 185) | def test_leftsibling_in_level(self):
    method test_drilldown_tree (line 236) | def test_drilldown_tree(self):
    method test_drilldown_tree_without_session (line 270) | def test_drilldown_tree_without_session(self):
    method test_path_to_root (line 288) | def test_path_to_root(self):

FILE: sqlalchemy_mptt/tests/cases/initialize.py
  class Initialize (line 4) | class Initialize(object):
    method test_tree_orm_initialize (line 6) | def test_tree_orm_initialize(self):
    method test_flush_with_transient_nodes_present (line 56) | def test_flush_with_transient_nodes_present(self):
    method test_tree_initialize (line 71) | def test_tree_initialize(self):

FILE: sqlalchemy_mptt/tests/cases/integrity.py
  class DataIntegrity (line 11) | class DataIntegrity(object):
    method test_left_is_always_less_than_right (line 13) | def test_left_is_always_less_than_right(self):
    method test_lowest_left_is_always_1 (line 26) | def test_lowest_left_is_always_1(self):
    method test_greatest_right_is_always_double_number_of_nodes (line 39) | def test_greatest_right_is_always_double_number_of_nodes(self):
    method test_right_minus_left_always_odd (line 55) | def test_right_minus_left_always_odd(self):
    method test_level_odd_when_left_odd_and_vice_versa (line 70) | def test_level_odd_when_left_odd_and_vice_versa(self):
    method test_left_and_right_always_unique_number (line 87) | def test_left_and_right_always_unique_number(self):
    method test_hierarchy_structure (line 96) | def test_hierarchy_structure(self):

FILE: sqlalchemy_mptt/tests/cases/move_node.py
  class MoveBefore (line 11) | class MoveBefore(object):
    method test_move_before_to_top_level (line 13) | def test_move_before_to_top_level(self):
    method test_move_one_tree_before_another (line 76) | def test_move_one_tree_before_another(self):
    method test_move_before_function (line 139) | def test_move_before_function(self):
    method test_move_one_tree_before_other_tree (line 200) | def test_move_one_tree_before_other_tree(self):
    method test_move_before_to_other_tree (line 348) | def test_move_before_to_other_tree(self):
  class MoveAfter (line 413) | class MoveAfter(object):
    method test_move_after_function (line 415) | def test_move_after_function(self):
    method test_move_to_toplevel_where_much_trees_from_right_side (line 477) | def test_move_to_toplevel_where_much_trees_from_right_side(self):
    method test_move_to_toplevel (line 611) | def test_move_to_toplevel(self):
    method test_move_to_toplevel2 (line 675) | def test_move_to_toplevel2(self):
    method test_move_to_toplevel_big_subtree (line 741) | def test_move_to_toplevel_big_subtree(self):
    method test_move_after_between_tree (line 804) | def test_move_after_between_tree(self):
  class MoveInside (line 867) | class MoveInside(object):
    method test_move_between_tree (line 869) | def test_move_between_tree(self):
    method test_move_tree_to_another_tree (line 934) | def test_move_tree_to_another_tree(self):
    method test_move_inside_function (line 992) | def test_move_inside_function(self):
    method test_tree_shorting (line 1056) | def test_tree_shorting(self):
    method test_move_inside_to_the_same_parent_function (line 1121) | def test_move_inside_to_the_same_parent_function(self):

FILE: sqlalchemy_mptt/tests/test_events.py
  class Tree (line 25) | class Tree(Base, BaseNestedSets):
    method __repr__ (line 31) | def __repr__(self):
  class TreeWithCustomId (line 35) | class TreeWithCustomId(Base, BaseNestedSets):
    method __repr__ (line 43) | def __repr__(self):
  class TreeWithCustomLevel (line 47) | class TreeWithCustomLevel(Base, BaseNestedSets):
    method __repr__ (line 55) | def __repr__(self):
  class TestTree (line 59) | class TestTree(TreeTestingMixin, unittest.TestCase):
  class TestTreeWithCustomId (line 64) | class TestTreeWithCustomId(TreeTestingMixin, unittest.TestCase):
  class TestTreeWithCustomLevel (line 69) | class TestTreeWithCustomLevel(TreeTestingMixin, unittest.TestCase):
  class Events (line 74) | class Events(unittest.TestCase):
    method test_register (line 76) | def test_register(self):
    method test_register_and_remove (line 101) | def test_register_and_remove(self):
    method test_remove (line 128) | def test_remove(self):
  class Tree0Id (line 155) | class Tree0Id(DatabaseSetupMixin, unittest.TestCase):
    method test (line 163) | def test(self):
  class InitialInsert (line 175) | class InitialInsert(DatabaseSetupMixin, unittest.TestCase):
    method test_documented_initial_insert (line 182) | def test_documented_initial_insert(self):

FILE: sqlalchemy_mptt/tests/test_inheritance.py
  class GenericTree (line 13) | class GenericTree(Base, BaseNestedSets):
    method __repr__ (line 27) | def __repr__(self):
  class SpecializedTree (line 31) | class SpecializedTree(GenericTree):
  class TestTree (line 47) | class TestTree(DatabaseSetupMixin, unittest.TestCase):
    method test_create_generic (line 51) | def test_create_generic(self):
    method test_create_spec (line 59) | def test_create_spec(self):
    method test_create_delete (line 67) | def test_create_delete(self):
  class TestGenericTree (line 96) | class TestGenericTree(TreeTestingMixin, unittest.TestCase):
  class TestSpecializedTree (line 101) | class TestSpecializedTree(TreeTestingMixin, unittest.TestCase):
    method test_rebuild (line 106) | def test_rebuild(self):
  class BaseInheritance (line 114) | class BaseInheritance(Base2):
    method __repr__ (line 126) | def __repr__(self):
  class InheritanceTree (line 130) | class InheritanceTree(BaseInheritance, BaseNestedSets):
  class TestInheritanceTree (line 142) | class TestInheritanceTree(TreeTestingMixin, unittest.TestCase):
    method test_rebuild (line 147) | def test_rebuild(self):

FILE: sqlalchemy_mptt/tests/test_mixins.py
  class Tree2 (line 23) | class Tree2(Base, BaseNestedSets):
  class TestMixin (line 29) | class TestMixin(unittest.TestCase):
    method test_mixin_parent_id (line 30) | def test_mixin_parent_id(self):

FILE: sqlalchemy_mptt/tests/test_stateful.py
  class Tree (line 22) | class Tree(Base, BaseNestedSets):
    method __repr__ (line 28) | def __repr__(self):
  class TreeStateMachine (line 32) | class TreeStateMachine(DatabaseSetupMixin, RuleBasedStateMachine):
    method __init__ (line 37) | def __init__(self):
    method teardown (line 41) | def teardown(self):
    method add_root_node (line 48) | def add_root_node(self, visible):
    method delete_node (line 56) | def delete_node(self, node):
    method add_child (line 69) | def add_child(self, node, visible):
    method check_get_tree_integrity (line 78) | def check_get_tree_integrity(self):
    method check_get_tree_with_custom_query (line 88) | def check_get_tree_with_custom_query(self):
  function validate_get_tree_node (line 100) | def validate_get_tree_node(node_response, level=1):
  function validate_get_tree_node_for_custom_query (line 113) | def validate_get_tree_node_for_custom_query(node_response):
Condensed preview — 53 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (254K chars).
[
  {
    "path": ".coveragerc",
    "chars": 58,
    "preview": "[run]\nrelative_files = True\n[report]\nomit =\n    */tests/*\n"
  },
  {
    "path": ".editorconfig",
    "chars": 77,
    "preview": "root = true\n\n[*]\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".flake8",
    "chars": 207,
    "preview": "[flake8]\nextend-exclude = .venv\nstatistics = True\ncount = True\nshow-source = True\n# The GitHub editor is 127 chars wide\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 525,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/codeql.yml",
    "chars": 4679,
    "preview": "# For most projects, this workflow file will not need changing; you simply need\n# to commit it to your repository.\n#\n# Y"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 2025,
    "preview": "# This workflow will upload a Python Package to PyPI when a release is created\n# For more information see: https://docs."
  },
  {
    "path": ".github/workflows/run-tests.yml",
    "chars": 1426,
    "preview": "name: Check code and run tests\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n\npermi"
  },
  {
    "path": ".gitignore",
    "chars": 333,
    "preview": "uv.lock\n.eggs\n.env\n*~\n*.swo\n*.swp\n.settings\n.project\n.pydevproject\nsqlalchemy_mptt/.coverage\nsqlalchemy_mptt/TODO\n*.pyc\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "chars": 674,
    "preview": "# See https://pre-commit.com for more information\n# See https://pre-commit.com/hooks.html for more hooks\nrepos:\n-   repo"
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 1077,
    "preview": "# Read the Docs configuration file for Sphinx projects\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html f"
  },
  {
    "path": ".rstcheck.cfg",
    "chars": 98,
    "preview": "[rstcheck]\nignore_directives=automodule,autofunction,autoclass,code-block\nignore_roles=mod,py:mod\n"
  },
  {
    "path": "CHANGES.rst",
    "chars": 1870,
    "preview": "Versions releases 0.2.x & above\n###############################\n\n0.6.0 (2025-11-29)\n==================\n\nsee issues #109,"
  },
  {
    "path": "CHANGES_OLD.rst",
    "chars": 2445,
    "preview": "Versions releases 0.1.x\n#######################\n\n0.1.9 (2015-09-24)\n==================\n\n- add option ``remove`` to ``sql"
  },
  {
    "path": "CONTRIBUTORS.txt",
    "chars": 296,
    "preview": "Contributors\n------------\n\n- Dmitry Svintsov (uralbash), 2014/04/16\n- Jonathan Stoppani, 2014/08/11\n- Fayaz Yusuf Khan, "
  },
  {
    "path": "LICENSE.txt",
    "chars": 1075,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2013 uralbash\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "MANIFEST.in",
    "chars": 86,
    "preview": "include requirements.txt\ninclude requirements-test.txt\ninclude README.rst CHANGES.rst\n"
  },
  {
    "path": "README.rst",
    "chars": 10393,
    "preview": "|PyPI Version| |PyPI Downloads| |PyPI Python Versions|\n|Build Status| |Coverage Status|\n\nLibrary for implementing Modifi"
  },
  {
    "path": "RELEASING.rst",
    "chars": 887,
    "preview": "Releasing\n=========\n\n1. Merge all intended and verified pull requests into the ``master`` branch.\n2. Create a local buil"
  },
  {
    "path": "docs/CONTRIBUTING.rst",
    "chars": 1708,
    "preview": "Contribution Guidelines\n=======================\n\nAll types of contributions are welcome: suggestions, ideas, commits\nwit"
  },
  {
    "path": "docs/Makefile",
    "chars": 6798,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD "
  },
  {
    "path": "docs/conf.py",
    "chars": 2556,
    "preview": "# -*- coding: utf-8 -*-\n#\n# sqlalchemy_mptt documentation build configuration file, created by\n# sphinx-quickstart on We"
  },
  {
    "path": "docs/crud.rst",
    "chars": 4392,
    "preview": "Usage\n=====\n\nINSERT\n------\n\nInsert node with parent_id==6\n\n.. testsetup::\n\n    Base = declarative_base()\n    engine = cr"
  },
  {
    "path": "docs/index.rst",
    "chars": 1311,
    "preview": ".. sqlalchemy_mptt documentation master file, created by\n   sphinx-quickstart on Wed Jun 25 14:00:12 2014.\n   You can ad"
  },
  {
    "path": "docs/initialize.rst",
    "chars": 5049,
    "preview": "Setup\n=====\n\nCreate model with MPTT mixin:\n\n.. testcode::\n\n    from sqlalchemy import Column, Integer, Boolean\n    from "
  },
  {
    "path": "docs/make.bat",
    "chars": 6719,
    "preview": "@ECHO OFF\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\n"
  },
  {
    "path": "docs/sqlalchemy_mptt.rst",
    "chars": 1697,
    "preview": ":mod:`sqlalchemy_mptt` package\n==============================\n\nEvents\n------\n\nBase events\n~~~~~~~~~~~\n\n.. automodule:: s"
  },
  {
    "path": "docs/tut_flask.rst",
    "chars": 13766,
    "preview": "Usage with Flask-SQLAlchemy\n===========================\n\nInitialize Flask app and sqlalchemy\n\n.. testsetup::\n\n    __name"
  },
  {
    "path": "noxfile.py",
    "chars": 5297,
    "preview": "# -*- coding: utf-8 -*-\n#\n# Copyright (c) 2025 Fayaz Yusuf Khan <fayaz.yusuf.khan@gmail.com>\n#\n# Distributed under terms"
  },
  {
    "path": "pyproject.toml",
    "chars": 323,
    "preview": "[tool.black]\nline-length = 79\ninclude = '\\.pyi?$'\nexclude = '''\n/(\n    \\.git\n  | \\.hg\n  | \\.mypy_cache\n  | \\.tox\n  | \\.v"
  },
  {
    "path": "requirements-doctest.txt",
    "chars": 24,
    "preview": "flask-sqlalchemy\nsphinx\n"
  },
  {
    "path": "requirements-test.txt",
    "chars": 29,
    "preview": "hypothesis\npytest\npytest-cov\n"
  },
  {
    "path": "requirements.txt",
    "chars": 23,
    "preview": "SQLAlchemy>=1.0.0,<3.0\n"
  },
  {
    "path": "setup.py",
    "chars": 1710,
    "preview": "import os\n\nfrom setuptools import setup\n\nthis = os.path.dirname(os.path.realpath(__file__))\n\n\ndef read(name):\n    with o"
  },
  {
    "path": "sqlalchemy_mptt/__init__.py",
    "chars": 438,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright (c) 2014 uralbash <root@uralbash.ru>\n#\n# D"
  },
  {
    "path": "sqlalchemy_mptt/events.py",
    "chars": 17473,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright © 2014 uralbash <root@uralbash.ru>\n# Copyr"
  },
  {
    "path": "sqlalchemy_mptt/mixins.py",
    "chars": 18605,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright © 2014 uralbash <root@uralbash.ru>\n# Copyr"
  },
  {
    "path": "sqlalchemy_mptt/sqlalchemy_compat.py",
    "chars": 1465,
    "preview": "# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright (c) 2025 Fayaz Yusuf Khan <fayaz.yusuf.khan@gmail.com>\n# Distribu"
  },
  {
    "path": "sqlalchemy_mptt/tests/__init__.py",
    "chars": 5867,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright © 2014 uralbash <root@uralbash.ru>\n#\n# Dis"
  },
  {
    "path": "sqlalchemy_mptt/tests/cases/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "sqlalchemy_mptt/tests/cases/edit_node.py",
    "chars": 24211,
    "preview": "class Changes(object):\n\n    def test_update_wo_move(self):\n        \"\"\" Update node w/o move\n        initial state of the"
  },
  {
    "path": "sqlalchemy_mptt/tests/cases/get_node.py",
    "chars": 2985,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright © 2015 uralbash <root@uralbash.ru>\n#\n# Dis"
  },
  {
    "path": "sqlalchemy_mptt/tests/cases/get_tree.py",
    "chars": 13177,
    "preview": "from sqlalchemy import asc\n\n\ndef get_obj(session, model, id):\n    return session.query(model).filter(model.get_pk_column"
  },
  {
    "path": "sqlalchemy_mptt/tests/cases/initialize.py",
    "chars": 4480,
    "preview": "from sqlalchemy.exc import IntegrityError\n\n\nclass Initialize(object):\n\n    def test_tree_orm_initialize(self):\n        p"
  },
  {
    "path": "sqlalchemy_mptt/tests/cases/integrity.py",
    "chars": 4819,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright © 2015 uralbash <root@uralbash.ru>\n#\n# Dis"
  },
  {
    "path": "sqlalchemy_mptt/tests/cases/move_node.py",
    "chars": 50375,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright © 2015 uralbash <root@uralbash.ru>\n#\n# Dis"
  },
  {
    "path": "sqlalchemy_mptt/tests/fixtures/tmp_tree.json",
    "chars": 809,
    "preview": "[\n    {\n        \"id\": \"1\",\n        \"parent_id\": null\n    },\n    {\n        \"id\": \"2\",\n        \"parent_id\": \"1\"\n    },\n   "
  },
  {
    "path": "sqlalchemy_mptt/tests/fixtures/tree.json",
    "chars": 1677,
    "preview": "[\n    {\n        \"parent_id\": null,\n        \"id\": \"1\"\n    },\n    {\n        \"parent_id\": \"1\",\n        \"id\": \"2\",\n        \""
  },
  {
    "path": "sqlalchemy_mptt/tests/fixtures/tree_3.json",
    "chars": 888,
    "preview": "[\n    {\n        \"parent_id\": null,\n        \"id\": 23,\n        \"tree_id\": \"3\"\n    },\n    {\n        \"parent_id\": \"23\",\n    "
  },
  {
    "path": "sqlalchemy_mptt/tests/test_events.py",
    "chars": 5011,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright © 2014 uralbash <root@uralbash.ru>\n#\n# Dis"
  },
  {
    "path": "sqlalchemy_mptt/tests/test_inheritance.py",
    "chars": 3900,
    "preview": "import unittest\n\nimport sqlalchemy as sa\n\nfrom sqlalchemy_mptt.mixins import BaseNestedSets\nfrom sqlalchemy_mptt.sqlalch"
  },
  {
    "path": "sqlalchemy_mptt/tests/test_mixins.py",
    "chars": 690,
    "preview": "#! /usr/bin/env python\n# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright © 2014 uralbash <root@uralbash.ru>\n#\n# Dis"
  },
  {
    "path": "sqlalchemy_mptt/tests/test_stateful.py",
    "chars": 4640,
    "preview": "# -*- coding: utf-8 -*-\n# vim:fenc=utf-8\n#\n# Copyright (c) 2025 Fayaz Yusuf Khan <fayaz.yusuf.khan@gmail.com>\n#\n# Distri"
  },
  {
    "path": "test.sh",
    "chars": 682,
    "preview": "#! /bin/bash\n#\n# test.sh\n# Copyright (C) 2015 uralbash <root@uralbash.ru>\n#\n# Distributed under terms of the MIT license"
  }
]

About this extraction

This page contains the full source code of the uralbash/sqlalchemy_mptt GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 53 files (236.2 KB), approximately 67.8k tokens, and a symbol index with 180 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!