django-fast-treenode 2.1.3__tar.gz → 2.1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. {django_fast_treenode-2.1.3/django_fast_treenode.egg-info → django_fast_treenode-2.1.4}/PKG-INFO +9 -5
  2. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/README.md +7 -4
  3. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4/django_fast_treenode.egg-info}/PKG-INFO +9 -5
  4. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/django_fast_treenode.egg-info/requires.txt +1 -0
  5. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/pyproject.toml +3 -2
  6. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/setup.cfg +2 -1
  7. django_fast_treenode-2.1.4/treenode/cache.py +352 -0
  8. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/managers/adjacency.py +6 -4
  9. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/adjacency.py +40 -41
  10. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/closure.py +2 -23
  11. django_fast_treenode-2.1.4/treenode/models/mixins/ancestors.py +48 -0
  12. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/children.py +2 -1
  13. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/descendants.py +16 -22
  14. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/node.py +4 -20
  15. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/siblings.py +8 -8
  16. django_fast_treenode-2.1.4/treenode/static/.gitkeep +0 -0
  17. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/version.py +2 -2
  18. django_fast_treenode-2.1.3/treenode/__init__.py +0 -5
  19. django_fast_treenode-2.1.3/treenode/cache.py +0 -231
  20. django_fast_treenode-2.1.3/treenode/models/mixins/ancestors.py +0 -65
  21. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/LICENSE +0 -0
  22. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/MANIFEST.in +0 -0
  23. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/django_fast_treenode.egg-info/SOURCES.txt +0 -0
  24. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/django_fast_treenode.egg-info/dependency_links.txt +0 -0
  25. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/django_fast_treenode.egg-info/top_level.txt +0 -0
  26. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/.gitignore +0 -0
  27. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/.nojekyll +0 -0
  28. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/about.md +0 -0
  29. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/admin.md +0 -0
  30. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/api.md +0 -0
  31. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/cache.md +0 -0
  32. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/import_export.md +0 -0
  33. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/index.md +0 -0
  34. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/installation.md +0 -0
  35. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/migration.md +0 -0
  36. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/models.md +0 -0
  37. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/requirements.txt +0 -0
  38. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/docs/roadmap.md +0 -0
  39. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/setup.py +0 -0
  40. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/tests/test_suite.py +0 -0
  41. /django_fast_treenode-2.1.3/treenode/static/.gitkeep → /django_fast_treenode-2.1.4/treenode/__init__.py +0 -0
  42. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/admin/__init__.py +0 -0
  43. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/admin/admin.py +0 -0
  44. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/admin/changelist.py +0 -0
  45. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/admin/mixins.py +0 -0
  46. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/apps.py +0 -0
  47. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/forms.py +0 -0
  48. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/managers/__init__.py +0 -0
  49. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/managers/closure.py +0 -0
  50. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/__init__.py +0 -0
  51. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/classproperty.py +0 -0
  52. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/factory.py +0 -0
  53. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/__init__.py +0 -0
  54. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/family.py +0 -0
  55. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/logical.py +0 -0
  56. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/properties.py +0 -0
  57. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/roots.py +0 -0
  58. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/models/mixins/tree.py +0 -0
  59. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/signals.py +0 -0
  60. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/static/treenode/.gitkeep +0 -0
  61. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/static/treenode/css/.gitkeep +0 -0
  62. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/static/treenode/css/tree_widget.css +0 -0
  63. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/static/treenode/css/treenode_admin.css +0 -0
  64. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/static/treenode/js/.gitkeep +0 -0
  65. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/static/treenode/js/tree_widget.js +0 -0
  66. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/static/treenode/js/treenode_admin.js +0 -0
  67. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/.gitkeep +0 -0
  68. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/admin/.gitkeep +0 -0
  69. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/admin/export_success.html +0 -0
  70. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/admin/tree_node_changelist.html +0 -0
  71. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/admin/tree_node_export.html +0 -0
  72. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/admin/tree_node_import.html +0 -0
  73. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/admin/tree_node_import_report.html +0 -0
  74. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/admin/treenode_ajax_rows.html +0 -0
  75. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/widgets/tree_widget.css +0 -0
  76. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/templates/widgets/tree_widget.html +0 -0
  77. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/urls.py +0 -0
  78. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/utils/__init__.py +0 -0
  79. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/utils/aid.py +0 -0
  80. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/utils/base16.py +0 -0
  81. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/utils/base36.py +0 -0
  82. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/utils/db.py +0 -0
  83. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/utils/exporter.py +0 -0
  84. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/utils/importer.py +0 -0
  85. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/utils/radix.py +0 -0
  86. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/views.py +0 -0
  87. {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.4}/treenode/widgets.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: django-fast-treenode
3
- Version: 2.1.3
3
+ Version: 2.1.4
4
4
  Summary: Application for supporting tree (hierarchical) data structure in Django projects
5
5
  Home-page: https://django-fast-treenode.readthedocs.io/
6
6
  Author: Timur Kady
@@ -55,23 +55,25 @@ License-File: LICENSE
55
55
  Requires-Dist: Django>=4.0
56
56
  Requires-Dist: pympler>=1.0
57
57
  Requires-Dist: django-widget-tweaks>=1.5
58
+ Requires-Dist: msgpack>=1.1
58
59
  Provides-Extra: import-export
59
60
  Requires-Dist: openpyxl; extra == "import-export"
60
61
  Requires-Dist: pyyaml; extra == "import-export"
61
62
  Requires-Dist: xlsxwriter; extra == "import-export"
62
63
 
63
64
  # Django-fast-treenode
64
- **Combining Adjacency List and Closure Table for Optimal Performance**
65
+ **Hybrid Tree Storage**
65
66
 
66
67
  [![Tests](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
67
68
  [![Docs](https://readthedocs.org/projects/django-fast-treenode/badge/?version=latest)](https://django-fast-treenode.readthedocs.io/)
68
69
  [![PyPI](https://img.shields.io/pypi/v/django-fast-treenode.svg)](https://pypi.org/project/django-fast-treenode/)
69
70
  [![Published on Django Packages](https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26)](https://djangopackages.org/packages/p/django-fast-treenode/)
71
+ [![Sponsor](https://img.shields.io/github/sponsors/TimurKady)](https://github.com/sponsors/TimurKady)
70
72
 
71
- **Django Fast TreeNode** is a high-performance Django application for working with tree structures, combining **Adjacency List** and **Closure Table** models. Each **TreeNodeModel** instance maintains two synchronized tables, enabling most operations to be performed with a single database query.
73
+ **Django Fast TreeNode** is a high-performance Django application for working with tree structures.
72
74
 
73
75
  ## Features
74
- - **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
76
+ - **Hybrid storage model**: Combines Adjacency List and Materialized Path (versions 2.2 and above) Closure Table (versions 2.1 and earlier) for optimal performance.
75
77
  - **Custom caching system**: A built-in caching mechanism, specifically designed for this package, significantly boosts execution speed.
76
78
  - **Efficient queries**: Retrieve ancestors, descendants, breadcrumbs, and tree depth with only one SQL queriy.
77
79
  - **Bulk operations**: Supports fast insertion, movement, and deletion of nodes.
@@ -79,6 +81,8 @@ Requires-Dist: xlsxwriter; extra == "import-export"
79
81
  - **Admin panel integration**: Full compatibility with Django's admin panel, allowing intuitive management of tree structures.
80
82
  - **Import & Export functionality**: Built-in support for importing and exporting tree structures in multiple formats (CSV, JSON, XLSX, YAML, TSV), including integration with the Django admin panel.
81
83
 
84
+ It seems that django-fast-treenode is currently the most balanced and performant solution for most tasks, especially those related to dynamic hierarchical data structures. Check out the results of (comparison tests)[#] with other Django packages.
85
+
82
86
  ## Use Cases
83
87
  Django Fast TreeNode is suitable for a wide range of applications, from simple directories to complex systems with deep hierarchical structures:
84
88
  - **Categories and taxonomies**: Manage product categories, tags, and classification systems.
@@ -159,4 +163,4 @@ Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/b
159
163
  ## Credits
160
164
  Thanks to everyone who contributed to the development and testing of this package, as well as the Django community for their inspiration and support.
161
165
 
162
- Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies and [Mathieu Leplatre](https://github.com/leplatrem) for the advice used in writing this application.
166
+ Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies.
@@ -1,15 +1,16 @@
1
1
  # Django-fast-treenode
2
- **Combining Adjacency List and Closure Table for Optimal Performance**
2
+ **Hybrid Tree Storage**
3
3
 
4
4
  [![Tests](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
5
5
  [![Docs](https://readthedocs.org/projects/django-fast-treenode/badge/?version=latest)](https://django-fast-treenode.readthedocs.io/)
6
6
  [![PyPI](https://img.shields.io/pypi/v/django-fast-treenode.svg)](https://pypi.org/project/django-fast-treenode/)
7
7
  [![Published on Django Packages](https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26)](https://djangopackages.org/packages/p/django-fast-treenode/)
8
+ [![Sponsor](https://img.shields.io/github/sponsors/TimurKady)](https://github.com/sponsors/TimurKady)
8
9
 
9
- **Django Fast TreeNode** is a high-performance Django application for working with tree structures, combining **Adjacency List** and **Closure Table** models. Each **TreeNodeModel** instance maintains two synchronized tables, enabling most operations to be performed with a single database query.
10
+ **Django Fast TreeNode** is a high-performance Django application for working with tree structures.
10
11
 
11
12
  ## Features
12
- - **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
13
+ - **Hybrid storage model**: Combines Adjacency List and Materialized Path (versions 2.2 and above) Closure Table (versions 2.1 and earlier) for optimal performance.
13
14
  - **Custom caching system**: A built-in caching mechanism, specifically designed for this package, significantly boosts execution speed.
14
15
  - **Efficient queries**: Retrieve ancestors, descendants, breadcrumbs, and tree depth with only one SQL queriy.
15
16
  - **Bulk operations**: Supports fast insertion, movement, and deletion of nodes.
@@ -17,6 +18,8 @@
17
18
  - **Admin panel integration**: Full compatibility with Django's admin panel, allowing intuitive management of tree structures.
18
19
  - **Import & Export functionality**: Built-in support for importing and exporting tree structures in multiple formats (CSV, JSON, XLSX, YAML, TSV), including integration with the Django admin panel.
19
20
 
21
+ It seems that django-fast-treenode is currently the most balanced and performant solution for most tasks, especially those related to dynamic hierarchical data structures. Check out the results of (comparison tests)[#] with other Django packages.
22
+
20
23
  ## Use Cases
21
24
  Django Fast TreeNode is suitable for a wide range of applications, from simple directories to complex systems with deep hierarchical structures:
22
25
  - **Categories and taxonomies**: Manage product categories, tags, and classification systems.
@@ -97,4 +100,4 @@ Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/b
97
100
  ## Credits
98
101
  Thanks to everyone who contributed to the development and testing of this package, as well as the Django community for their inspiration and support.
99
102
 
100
- Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies and [Mathieu Leplatre](https://github.com/leplatrem) for the advice used in writing this application.
103
+ Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: django-fast-treenode
3
- Version: 2.1.3
3
+ Version: 2.1.4
4
4
  Summary: Application for supporting tree (hierarchical) data structure in Django projects
5
5
  Home-page: https://django-fast-treenode.readthedocs.io/
6
6
  Author: Timur Kady
@@ -55,23 +55,25 @@ License-File: LICENSE
55
55
  Requires-Dist: Django>=4.0
56
56
  Requires-Dist: pympler>=1.0
57
57
  Requires-Dist: django-widget-tweaks>=1.5
58
+ Requires-Dist: msgpack>=1.1
58
59
  Provides-Extra: import-export
59
60
  Requires-Dist: openpyxl; extra == "import-export"
60
61
  Requires-Dist: pyyaml; extra == "import-export"
61
62
  Requires-Dist: xlsxwriter; extra == "import-export"
62
63
 
63
64
  # Django-fast-treenode
64
- **Combining Adjacency List and Closure Table for Optimal Performance**
65
+ **Hybrid Tree Storage**
65
66
 
66
67
  [![Tests](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
67
68
  [![Docs](https://readthedocs.org/projects/django-fast-treenode/badge/?version=latest)](https://django-fast-treenode.readthedocs.io/)
68
69
  [![PyPI](https://img.shields.io/pypi/v/django-fast-treenode.svg)](https://pypi.org/project/django-fast-treenode/)
69
70
  [![Published on Django Packages](https://img.shields.io/badge/Published%20on-Django%20Packages-0c3c26)](https://djangopackages.org/packages/p/django-fast-treenode/)
71
+ [![Sponsor](https://img.shields.io/github/sponsors/TimurKady)](https://github.com/sponsors/TimurKady)
70
72
 
71
- **Django Fast TreeNode** is a high-performance Django application for working with tree structures, combining **Adjacency List** and **Closure Table** models. Each **TreeNodeModel** instance maintains two synchronized tables, enabling most operations to be performed with a single database query.
73
+ **Django Fast TreeNode** is a high-performance Django application for working with tree structures.
72
74
 
73
75
  ## Features
74
- - **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
76
+ - **Hybrid storage model**: Combines Adjacency List and Materialized Path (versions 2.2 and above) Closure Table (versions 2.1 and earlier) for optimal performance.
75
77
  - **Custom caching system**: A built-in caching mechanism, specifically designed for this package, significantly boosts execution speed.
76
78
  - **Efficient queries**: Retrieve ancestors, descendants, breadcrumbs, and tree depth with only one SQL queriy.
77
79
  - **Bulk operations**: Supports fast insertion, movement, and deletion of nodes.
@@ -79,6 +81,8 @@ Requires-Dist: xlsxwriter; extra == "import-export"
79
81
  - **Admin panel integration**: Full compatibility with Django's admin panel, allowing intuitive management of tree structures.
80
82
  - **Import & Export functionality**: Built-in support for importing and exporting tree structures in multiple formats (CSV, JSON, XLSX, YAML, TSV), including integration with the Django admin panel.
81
83
 
84
+ It seems that django-fast-treenode is currently the most balanced and performant solution for most tasks, especially those related to dynamic hierarchical data structures. Check out the results of (comparison tests)[#] with other Django packages.
85
+
82
86
  ## Use Cases
83
87
  Django Fast TreeNode is suitable for a wide range of applications, from simple directories to complex systems with deep hierarchical structures:
84
88
  - **Categories and taxonomies**: Manage product categories, tags, and classification systems.
@@ -159,4 +163,4 @@ Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/b
159
163
  ## Credits
160
164
  Thanks to everyone who contributed to the development and testing of this package, as well as the Django community for their inspiration and support.
161
165
 
162
- Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies and [Mathieu Leplatre](https://github.com/leplatrem) for the advice used in writing this application.
166
+ Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies.
@@ -1,6 +1,7 @@
1
1
  Django>=4.0
2
2
  pympler>=1.0
3
3
  django-widget-tweaks>=1.5
4
+ msgpack>=1.1
4
5
 
5
6
  [import_export]
6
7
  openpyxl
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "django-fast-treenode"
7
- version = "2.1.3"
7
+ version = "2.1.4"
8
8
  description = "Application for supporting tree (hierarchical) data structure in Django projects"
9
9
  readme = "README.md"
10
10
  authors = [{ name = "Timur Kady", email = "timurkady@yandex.com" }]
@@ -13,7 +13,8 @@ requires-python = ">=3.9"
13
13
  dependencies = [
14
14
  "Django >=4.0",
15
15
  "pympler >=1.0",
16
- "django-widget-tweaks >= 1.5"
16
+ "django-widget-tweaks >= 1.5",
17
+ "msgpack >= 1.1"
17
18
  ]
18
19
  classifiers = [
19
20
  "Development Status :: 5 - Production/Stable",
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = django-fast-treenode
3
- version = 2.1.3
3
+ version = 2.1.4
4
4
  description = Application for supporting tree (hierarchical) data structure in Django projects
5
5
  long_description = file: README.md
6
6
  long_description_content_type = text/markdown
@@ -35,6 +35,7 @@ install_requires =
35
35
  Django >=4.0
36
36
  pympler >=1.0
37
37
  django-widget-tweaks >= 1.5
38
+ msgpack >= 1.1
38
39
 
39
40
  [options.extras_require]
40
41
  import_export =
@@ -0,0 +1,352 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Cache Module
4
+
5
+ This module provides a singleton-based caching system for TreeNode models.
6
+ It includes optimized key generation, cache size tracking,
7
+ and an eviction mechanism to ensure efficient memory usage.
8
+
9
+ Features:
10
+ - Singleton cache instance to prevent redundant allocations.
11
+ - Custom cache key generation using function parameters.
12
+ - Automatic cache eviction when memory limits are exceeded.
13
+ - Decorator `@cached_method` for caching method results.
14
+
15
+ Version: 2.2.0
16
+ Author: Timur Kady
17
+ Email: timurkady@yandex.com
18
+ """
19
+
20
+ import hashlib
21
+ import msgpack
22
+ import sys
23
+ import threading
24
+ from collections import deque, defaultdict, OrderedDict
25
+ from django.conf import settings
26
+ from django.core.cache import caches
27
+ from functools import lru_cache
28
+ from functools import wraps
29
+
30
+
31
+ # ---------------------------------------------------
32
+ # Utilities
33
+ # ---------------------------------------------------
34
+
35
+ _DIGITS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
36
+ _CLEARINT_THESHOLD = 0.8
37
+ _EVICT_INTERVAL = 50
38
+
39
+
40
+ @lru_cache(maxsize=1000)
41
+ def to_base36(num):
42
+ """
43
+ Convert an integer to a base36 string.
44
+
45
+ For example: 10 -> 'A', 35 -> 'Z', 36 -> '10', etc.
46
+ """
47
+ if num == 0:
48
+ return '0'
49
+ sign = '-' if num < 0 else ''
50
+ num = abs(num)
51
+ result = []
52
+ while num:
53
+ num, rem = divmod(num, 36)
54
+ result.append(_DIGITS[rem])
55
+ return sign + ''.join(reversed(result))
56
+
57
+
58
+ # ---------------------------------------------------
59
+ # Caching
60
+ # ---------------------------------------------------
61
+
62
+ class TreeCache:
63
+ """Singleton class for managing the TreeNode cache."""
64
+
65
+ _instance = None
66
+ _instance_lock = threading.Lock()
67
+
68
+ def __new__(cls, *args, **kwargs):
69
+ """Singleton new."""
70
+ with cls._instance_lock:
71
+ if cls._instance is None:
72
+ cls._instance = super(TreeCache, cls).__new__(cls)
73
+ return cls._instance
74
+
75
+ def __init__(self, cache_limit=100 * 1024 * 1024):
76
+ """
77
+ Initialize the cache.
78
+
79
+ If the 'treenode' key is present in settings.CACHES, the corresponding
80
+ backend is used.
81
+ Otherwise, the custom dictionary is used.
82
+ The cache size (in bytes) is taken from
83
+ settings.TREENODE_CACHE_LIMIT (MB), by default 100 MB.
84
+ """
85
+ if hasattr(self, '_initialized') and self._initialized:
86
+ return
87
+
88
+ # Get the cache limit (MB), then convert to bytes.
89
+ cache_limit_mb = getattr(settings, 'TREENODE_CACHE_LIMIT', 100)
90
+ self.cache_limit = cache_limit_mb * 1024 * 1024
91
+
92
+ # Select backend: if there is 'treenode' in settings.CACHES, use it.
93
+ # Otherwise, use our own dictionary.
94
+ if hasattr(settings, 'CACHES') and 'treenode' in settings.CACHES:
95
+ self.cache = caches['treenode']
96
+ else:
97
+ # We use our dictionary as a backend.
98
+ self.cache = OrderedDict()
99
+
100
+ self.order = deque() # Queue for FIFO implementation.
101
+ self.total_size = 0 # Current cache size in bytes.
102
+ self.lock = threading.Lock() # Lock for thread safety.
103
+
104
+ # Additional index for fast search of keys by prefix
105
+ # Format: {prefix: {key1, key2, ...}}
106
+ self.prefix_index = defaultdict(set)
107
+ # Dictionary to store the sizes of each key (key -> size in bytes)
108
+ self.sizes = {}
109
+ # Dictionary to store the prefix for each key to avoid repeated
110
+ # splitting
111
+ self.key_prefix = {}
112
+
113
+ # Counter for number of set operations for periodic eviction
114
+ self._set_counter = 0
115
+ # Evict cache every _evict_interval set operations when using external
116
+ # backend
117
+ self._evict_interval = _EVICT_INTERVAL
118
+
119
+ self._initialized = True
120
+
121
+ def generate_cache_key(self, label, func_name, unique_id, *args, **kwargs):
122
+ """
123
+ Generate a cache key.
124
+
125
+ <label>_<func_name>_<unique_id>_<hash>
126
+ """
127
+ # If using custom dict backend, use simple key generation without
128
+ # serialization.
129
+ if isinstance(self.cache, dict):
130
+ sorted_kwargs = sorted(kwargs.items())
131
+ return f"{label}_{func_name}_{unique_id}_{args}_{sorted_kwargs}"
132
+ else:
133
+ try:
134
+ # Using msgpack for fast binary representation of arguments
135
+ sorted_kwargs = sorted(kwargs.items())
136
+ params_bytes = msgpack.packb(
137
+ (args, sorted_kwargs), use_bin_type=True)
138
+ except Exception:
139
+ params_bytes = repr((args, kwargs)).encode('utf-8')
140
+ # Using MD5 for speed (no cryptographic strength)
141
+ hash_value = hashlib.md5(params_bytes).hexdigest()
142
+ return f"{label}_{func_name}_{unique_id}_{hash_value}"
143
+
144
+ def get_obj_size(self, value):
145
+ """
146
+ Determine the size of the object in bytes.
147
+
148
+ If the value is already in bytes or bytearray, simply returns its
149
+ length. Otherwise, uses sys.getsizeof for an approximate estimate.
150
+ """
151
+ if isinstance(value, (bytes, bytearray)):
152
+ return len(value)
153
+ return sys.getsizeof(value)
154
+
155
+ def set(self, key, value):
156
+ """
157
+ Store the value in the cache.
158
+
159
+ Stores the value in the cache, updates the FIFO queue, prefix index,
160
+ size dictionary, and total cache size.
161
+ """
162
+ # Idea 1: Store raw object if using custom dict backend, otherwise
163
+ # serialize using msgpack.
164
+ if isinstance(self.cache, dict):
165
+ stored_value = value
166
+ else:
167
+ try:
168
+ stored_value = msgpack.packb(value, use_bin_type=True)
169
+ except Exception:
170
+ stored_value = value
171
+
172
+ # Calculate the size of the stored value
173
+ if isinstance(stored_value, (bytes, bytearray)):
174
+ size = len(stored_value)
175
+ else:
176
+ size = sys.getsizeof(stored_value)
177
+
178
+ # Store the value in the cache backend
179
+ if isinstance(self.cache, dict):
180
+ self.cache[key] = stored_value
181
+ else:
182
+ self.cache.set(key, stored_value)
183
+
184
+ # Update internal structures under lock
185
+ with self.lock:
186
+ if key in self.sizes:
187
+ # If the key already exists, adjust the total size
188
+ old_size = self.sizes[key]
189
+ self.total_size -= old_size
190
+ else:
191
+ # New key: add to FIFO queue
192
+ self.order.append(key)
193
+ # Compute prefix once and store it in key_prefix
194
+ if "_" in key:
195
+ prefix = key.split('_', 1)[0] + "_"
196
+ else:
197
+ prefix = key
198
+ self.key_prefix[key] = prefix
199
+ self.prefix_index[prefix].add(key)
200
+ # Save the size for this key and update total_size
201
+ self.sizes[key] = size
202
+ self.total_size += size
203
+
204
+ # Increment the set counter for periodic eviction
205
+ self._set_counter += 1
206
+
207
+ # Idea 3: If using external backend, evict cache every _evict_interval
208
+ # sets. Otherwise, always evict immediately.
209
+ if self._set_counter >= self._evict_interval:
210
+ with self.lock:
211
+ self._set_counter = 0
212
+ self._evict_cache()
213
+
214
+ def get(self, key):
215
+ """
216
+ Get a value from the cache by key.
217
+
218
+ Quickly retrieves a value from the cache by key.
219
+ Here we simply request a value from the backend (either a dictionary or
220
+ Django cache-backend) and return it without any additional operations.
221
+ """
222
+ if isinstance(self.cache, dict):
223
+ return self.cache.get(key)
224
+ else:
225
+ packed_value = self.cache.get(key)
226
+ if packed_value is None:
227
+ return None
228
+ try:
229
+ return msgpack.unpackb(packed_value, raw=False)
230
+ except Exception:
231
+ # If unpacking fails, return what we got
232
+ return packed_value
233
+
234
+ def invalidate(self, prefix):
235
+ """
236
+ Invalidate model cache.
237
+
238
+ Quickly removes all items from the cache whose keys start with prefix.
239
+ Uses prefix_index for instant access to keys.
240
+ When removing, each key's size is retrieved from self.sizes,
241
+ and total_size is reduced by the corresponding amount.
242
+ """
243
+ prefix += '_'
244
+ with self.lock:
245
+ keys_to_remove = self.prefix_index.get(prefix, set())
246
+ if not keys_to_remove:
247
+ return
248
+
249
+ # Remove keys from main cache and update total_size via sizes
250
+ # dictionary
251
+ if isinstance(self.cache, dict):
252
+ for key in keys_to_remove:
253
+ self.cache.pop(key, None)
254
+ size = self.sizes.pop(key, 0)
255
+ self.total_size -= size
256
+ # Remove key from key_prefix as well
257
+ self.key_prefix.pop(key, None)
258
+ else:
259
+ # If using Django backend
260
+ self.cache.delete_many(list(keys_to_remove))
261
+ for key in keys_to_remove:
262
+ size = self.sizes.pop(key, 0)
263
+ self.total_size -= size
264
+ self.key_prefix.pop(key, None)
265
+
266
+ # Remove prefix from index and update FIFO queue
267
+ del self.prefix_index[prefix]
268
+ self.order = deque(k for k in self.order if k not in keys_to_remove)
269
+
270
+ def clear(self):
271
+ """Clear cache completely."""
272
+ with self.lock:
273
+ if isinstance(self.cache, dict):
274
+ self.cache.clear()
275
+ else:
276
+ self.cache.clear()
277
+ self.order.clear()
278
+ self.prefix_index.clear()
279
+ self.sizes.clear()
280
+ self.key_prefix.clear()
281
+ self.total_size = 0
282
+
283
+ def _evict_cache(self):
284
+ """
285
+ Perform FIFO cache evacuation.
286
+
287
+ Removes old items until the total cache size is less than
288
+ _CLEARINT_THESHOLD of the limit.
289
+ """
290
+ with self.lock:
291
+ # Evict until total_size is below 80% of cache_limit
292
+ target_size = _CLEARINT_THESHOLD * self.cache_limit
293
+ while self.total_size > target_size and self.order:
294
+ # Extract the oldest key from the queue (FIFO)
295
+ key = self.order.popleft()
296
+
297
+ # Delete entry from backend cache
298
+ if isinstance(self.cache, dict):
299
+ self.cache.pop(key, None)
300
+ else:
301
+ self.cache.delete(key)
302
+
303
+ # Extract the size of the entry to be deleted and reduce
304
+ # the overall cache size
305
+ size = self.sizes.pop(key, 0)
306
+ self.total_size -= size
307
+
308
+ # Retrieve prefix from key_prefix without splitting
309
+ prefix = self.key_prefix.pop(key, None)
310
+ if prefix is not None:
311
+ self.prefix_index[prefix].discard(key)
312
+ if not self.prefix_index[prefix]:
313
+ del self.prefix_index[prefix]
314
+
315
+
316
+ # Global cache object (unique for the system)
317
+ treenode_cache = TreeCache()
318
+
319
+
320
+ # ---------------------------------------------------
321
+ # Decorator
322
+ # ---------------------------------------------------
323
+
324
+ def cached_method(func):
325
+ """Decorate instance or class methods."""
326
+ @wraps(func)
327
+ def wrapper(self, *args, **kwargs):
328
+ cache = treenode_cache
329
+
330
+ if isinstance(self, type):
331
+ unique_id = to_base36(id(self))
332
+ label = getattr(self._meta, 'label', self.__name__)
333
+ else:
334
+ unique_id = getattr(self, "pk", None) or to_base36(id(self))
335
+ label = self._meta.label
336
+
337
+ cache_key = cache.generate_cache_key(
338
+ label,
339
+ func.__name__,
340
+ unique_id,
341
+ *args,
342
+ **kwargs
343
+ )
344
+ value = cache.get(cache_key)
345
+ if value is None:
346
+ value = func(self, *args, **kwargs)
347
+ cache.set(cache_key, value)
348
+ return value
349
+ return wrapper
350
+
351
+
352
+ # The End
@@ -14,6 +14,7 @@ Email: timurkady@yandex.com
14
14
  from collections import deque, defaultdict
15
15
  from django.db import models, transaction
16
16
  from django.db import connection
17
+ from django.db.models import F
17
18
 
18
19
 
19
20
  class TreeNodeQuerySet(models.QuerySet):
@@ -137,10 +138,11 @@ class TreeNodeModelManager(models.Manager):
137
138
 
138
139
  def get_queryset(self):
139
140
  """Return a sorted QuerySet."""
140
- queryset = TreeNodeQuerySet(self.model, using=self._db)\
141
- .annotate(_depth_db=models.Max("parents_set__depth"))\
142
- .order_by("_depth_db", "tn_parent", "tn_priority")
143
- return queryset
141
+ return TreeNodeQuerySet(self.model, using=self._db)\
142
+ .order_by(
143
+ # F('tn_parent').asc(nulls_first=True),
144
+ 'tn_parent', 'tn_priority'
145
+ )
144
146
 
145
147
  # Service methods -------------------
146
148
 
@@ -74,8 +74,6 @@ class TreeNodeModel(
74
74
 
75
75
  abstract = True
76
76
  indexes = [
77
- models.Index(fields=["tn_parent"]),
78
- models.Index(fields=["tn_parent", "id"]),
79
77
  models.Index(fields=["tn_parent", "tn_priority"]),
80
78
  ]
81
79
 
@@ -151,43 +149,45 @@ class TreeNodeModel(
151
149
  using=self._state.db,
152
150
  update_fields=kwargs.get("update_fields", None)
153
151
  )
154
-
155
- # If the object already exists, get the old parent and priority values
156
- is_new = self.pk is None
157
- if not is_new:
158
- old_parent, old_priority = model.objects\
159
- .filter(pk=self.pk)\
160
- .values_list('tn_parent', 'tn_priority')\
161
- .first()
162
- is_move = (old_priority != self.tn_priority)
163
- else:
164
- force_insert = True
165
- is_move = False
166
- old_parent = None
167
-
168
- # Check if we are trying to move a node to a child
169
- if old_parent and old_parent != self.tn_parent and self.tn_parent:
170
- # Get pk of children via values_list to avoid creating full
171
- # set of objects
172
- if self.tn_parent.pk in self.get_descendants_pks():
173
- raise ValueError("You cannot move a node into its own child.")
174
-
175
- # Save the object and synchronize with the closing table
176
- # Disable signals
177
- with (disable_signals(pre_save, model),
178
- disable_signals(post_save, model)):
179
-
180
- if is_new or is_move:
181
- self._update_priority()
182
- super().save(force_insert=force_insert, *args, **kwargs)
183
- # Run synchronize
184
- if is_new:
185
- self.closure_model.insert_node(self)
186
- elif is_move:
187
- subtree_nodes = self.get_descendants(include_self=True)
188
- self.closure_model.move_node(subtree_nodes)
189
- # Update priorities among neighbors or clear cache if there was
190
- # no movement
152
+ with transaction.atomic():
153
+ # If the object already exists, get the old parent and priority
154
+ # values
155
+ is_new = self.pk is None
156
+ if not is_new:
157
+ old_parent, old_priority = model.objects\
158
+ .filter(pk=self.pk)\
159
+ .values_list('tn_parent', 'tn_priority')\
160
+ .first()
161
+ is_move = (old_priority != self.tn_priority)
162
+ else:
163
+ force_insert = True
164
+ is_move = False
165
+ old_parent = None
166
+
167
+ descendants = self.get_descendants(include_self=True)
168
+
169
+ # Check if we are trying to move a node to a child
170
+ if old_parent and old_parent != self.tn_parent and self.tn_parent:
171
+ # Get pk of children via values_list to avoid creating full
172
+ # set of objects
173
+ if self.tn_parent in descendants:
174
+ raise ValueError(
175
+ "You cannot move a node into its own child."
176
+ )
177
+
178
+ # Save the object and synchronize with the closing table
179
+ # Disable signals
180
+ with (disable_signals(pre_save, model),
181
+ disable_signals(post_save, model)):
182
+
183
+ if is_new or is_move:
184
+ self._update_priority()
185
+ super().save(force_insert=force_insert, *args, **kwargs)
186
+ # Run synchronize
187
+ if is_new:
188
+ self.closure_model.insert_node(self)
189
+ elif is_move:
190
+ self.closure_model.move_node(descendants)
191
191
 
192
192
  # Clear model cache
193
193
  model.clear_cache()
@@ -203,7 +203,7 @@ class TreeNodeModel(
203
203
 
204
204
  def _update_priority(self):
205
205
  """Update tn_priority field for siblings."""
206
- siblings = self.get_siblings()
206
+ siblings = self.get_siblings(include_self=False)
207
207
  siblings = sorted(siblings, key=lambda x: x.tn_priority)
208
208
  insert_pos = min(self.tn_priority, len(siblings))
209
209
  siblings.insert(insert_pos, self)
@@ -214,7 +214,6 @@ class TreeNodeModel(
214
214
  # Save changes
215
215
  model = self._meta.model
216
216
  model.objects.bulk_update(siblings, ['tn_priority'])
217
- model.clear_cache()
218
217
 
219
218
  @classmethod
220
219
  def _get_place(cls, target, position=0):