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
_______
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
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.