django-fast-treenode 2.1.3__tar.gz → 2.1.5__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.
- {django_fast_treenode-2.1.3/django_fast_treenode.egg-info → django_fast_treenode-2.1.5}/PKG-INFO +10 -7
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/README.md +6 -4
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5/django_fast_treenode.egg-info}/PKG-INFO +10 -7
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/django_fast_treenode.egg-info/requires.txt +1 -1
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/pyproject.toml +3 -3
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/setup.cfg +2 -2
- django_fast_treenode-2.1.5/treenode/admin/__init__.py +9 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/admin/admin.py +1 -1
- django_fast_treenode-2.1.5/treenode/cache.py +352 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/managers/adjacency.py +6 -4
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/adjacency.py +40 -41
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/closure.py +2 -23
- django_fast_treenode-2.1.5/treenode/models/mixins/ancestors.py +48 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/children.py +2 -1
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/descendants.py +16 -22
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/node.py +4 -20
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/siblings.py +8 -8
- django_fast_treenode-2.1.5/treenode/static/.gitkeep +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/utils/exporter.py +1 -1
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/version.py +2 -2
- django_fast_treenode-2.1.3/treenode/__init__.py +0 -5
- django_fast_treenode-2.1.3/treenode/admin/__init__.py +0 -9
- django_fast_treenode-2.1.3/treenode/cache.py +0 -231
- django_fast_treenode-2.1.3/treenode/models/mixins/ancestors.py +0 -65
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/LICENSE +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/MANIFEST.in +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/django_fast_treenode.egg-info/SOURCES.txt +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/django_fast_treenode.egg-info/dependency_links.txt +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/django_fast_treenode.egg-info/top_level.txt +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/.gitignore +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/.nojekyll +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/about.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/admin.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/api.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/cache.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/import_export.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/index.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/installation.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/migration.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/models.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/requirements.txt +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/docs/roadmap.md +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/setup.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/tests/test_suite.py +0 -0
- /django_fast_treenode-2.1.3/treenode/static/.gitkeep → /django_fast_treenode-2.1.5/treenode/__init__.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/admin/changelist.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/admin/mixins.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/apps.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/forms.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/managers/__init__.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/managers/closure.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/__init__.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/classproperty.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/factory.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/__init__.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/family.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/logical.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/properties.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/roots.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/models/mixins/tree.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/signals.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/static/treenode/.gitkeep +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/static/treenode/css/.gitkeep +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/static/treenode/css/tree_widget.css +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/static/treenode/css/treenode_admin.css +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/static/treenode/js/.gitkeep +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/static/treenode/js/tree_widget.js +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/static/treenode/js/treenode_admin.js +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/.gitkeep +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/admin/.gitkeep +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/admin/export_success.html +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/admin/tree_node_changelist.html +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/admin/tree_node_export.html +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/admin/tree_node_import.html +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/admin/tree_node_import_report.html +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/admin/treenode_ajax_rows.html +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/widgets/tree_widget.css +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/templates/widgets/tree_widget.html +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/urls.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/utils/__init__.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/utils/aid.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/utils/base16.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/utils/base36.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/utils/db.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/utils/importer.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/utils/radix.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/views.py +0 -0
- {django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5}/treenode/widgets.py +0 -0
{django_fast_treenode-2.1.3/django_fast_treenode.egg-info → django_fast_treenode-2.1.5}/PKG-INFO
RENAMED
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 2.1.
|
3
|
+
Version: 2.1.5
|
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
|
@@ -53,22 +53,24 @@ Requires-Python: >=3.9
|
|
53
53
|
Description-Content-Type: text/markdown
|
54
54
|
License-File: LICENSE
|
55
55
|
Requires-Dist: Django>=4.0
|
56
|
-
Requires-Dist: pympler>=1.0
|
57
56
|
Requires-Dist: django-widget-tweaks>=1.5
|
57
|
+
Requires-Dist: msgpack>=1.1
|
58
58
|
Provides-Extra: import-export
|
59
59
|
Requires-Dist: openpyxl; extra == "import-export"
|
60
60
|
Requires-Dist: pyyaml; extra == "import-export"
|
61
61
|
Requires-Dist: xlsxwriter; extra == "import-export"
|
62
|
+
Dynamic: license-file
|
62
63
|
|
63
64
|
# Django-fast-treenode
|
64
|
-
**
|
65
|
+
**Hybrid Tree Storage**
|
65
66
|
|
66
67
|
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
67
68
|
[](https://django-fast-treenode.readthedocs.io/)
|
68
69
|
[](https://pypi.org/project/django-fast-treenode/)
|
69
70
|
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
71
|
+
[](https://github.com/sponsors/TimurKady)
|
70
72
|
|
71
|
-
**Django Fast TreeNode** is a high-performance Django application for working with tree structures
|
73
|
+
**Django Fast TreeNode** is a high-performance Django application for working with tree structures.
|
72
74
|
|
73
75
|
## Features
|
74
76
|
- **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
|
@@ -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.
|
@@ -87,7 +91,6 @@ Django Fast TreeNode is suitable for a wide range of applications, from simple d
|
|
87
91
|
- **Geographical data**: Represent administrative divisions, regions, and areas of influence.
|
88
92
|
- **Organizational and Business Structures**: Model company hierarchies, business processes, employees and departments.
|
89
93
|
|
90
|
-
In all applications, `django-fast-treenode` models show excellent performance and stability.
|
91
94
|
|
92
95
|
## Quick start
|
93
96
|
1. Run `pip install django-fast-treenode`.
|
@@ -159,4 +162,4 @@ Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/b
|
|
159
162
|
## Credits
|
160
163
|
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
164
|
|
162
|
-
Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies
|
165
|
+
Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies.
|
@@ -1,12 +1,13 @@
|
|
1
1
|
# Django-fast-treenode
|
2
|
-
**
|
2
|
+
**Hybrid Tree Storage**
|
3
3
|
|
4
4
|
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
5
5
|
[](https://django-fast-treenode.readthedocs.io/)
|
6
6
|
[](https://pypi.org/project/django-fast-treenode/)
|
7
7
|
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
8
|
+
[](https://github.com/sponsors/TimurKady)
|
8
9
|
|
9
|
-
**Django Fast TreeNode** is a high-performance Django application for working with tree structures
|
10
|
+
**Django Fast TreeNode** is a high-performance Django application for working with tree structures.
|
10
11
|
|
11
12
|
## Features
|
12
13
|
- **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
|
@@ -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.
|
@@ -25,7 +28,6 @@ Django Fast TreeNode is suitable for a wide range of applications, from simple d
|
|
25
28
|
- **Geographical data**: Represent administrative divisions, regions, and areas of influence.
|
26
29
|
- **Organizational and Business Structures**: Model company hierarchies, business processes, employees and departments.
|
27
30
|
|
28
|
-
In all applications, `django-fast-treenode` models show excellent performance and stability.
|
29
31
|
|
30
32
|
## Quick start
|
31
33
|
1. Run `pip install django-fast-treenode`.
|
@@ -97,4 +99,4 @@ Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/b
|
|
97
99
|
## Credits
|
98
100
|
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
101
|
|
100
|
-
Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies
|
102
|
+
Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies.
|
{django_fast_treenode-2.1.3 → django_fast_treenode-2.1.5/django_fast_treenode.egg-info}/PKG-INFO
RENAMED
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.4
|
2
2
|
Name: django-fast-treenode
|
3
|
-
Version: 2.1.
|
3
|
+
Version: 2.1.5
|
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
|
@@ -53,22 +53,24 @@ Requires-Python: >=3.9
|
|
53
53
|
Description-Content-Type: text/markdown
|
54
54
|
License-File: LICENSE
|
55
55
|
Requires-Dist: Django>=4.0
|
56
|
-
Requires-Dist: pympler>=1.0
|
57
56
|
Requires-Dist: django-widget-tweaks>=1.5
|
57
|
+
Requires-Dist: msgpack>=1.1
|
58
58
|
Provides-Extra: import-export
|
59
59
|
Requires-Dist: openpyxl; extra == "import-export"
|
60
60
|
Requires-Dist: pyyaml; extra == "import-export"
|
61
61
|
Requires-Dist: xlsxwriter; extra == "import-export"
|
62
|
+
Dynamic: license-file
|
62
63
|
|
63
64
|
# Django-fast-treenode
|
64
|
-
**
|
65
|
+
**Hybrid Tree Storage**
|
65
66
|
|
66
67
|
[](https://github.com/TimurKady/django-fast-treenode/actions/workflows/test.yaml)
|
67
68
|
[](https://django-fast-treenode.readthedocs.io/)
|
68
69
|
[](https://pypi.org/project/django-fast-treenode/)
|
69
70
|
[](https://djangopackages.org/packages/p/django-fast-treenode/)
|
71
|
+
[](https://github.com/sponsors/TimurKady)
|
70
72
|
|
71
|
-
**Django Fast TreeNode** is a high-performance Django application for working with tree structures
|
73
|
+
**Django Fast TreeNode** is a high-performance Django application for working with tree structures.
|
72
74
|
|
73
75
|
## Features
|
74
76
|
- **Hybrid storage model**: Combines Adjacency List and Closure Table for optimal performance.
|
@@ -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.
|
@@ -87,7 +91,6 @@ Django Fast TreeNode is suitable for a wide range of applications, from simple d
|
|
87
91
|
- **Geographical data**: Represent administrative divisions, regions, and areas of influence.
|
88
92
|
- **Organizational and Business Structures**: Model company hierarchies, business processes, employees and departments.
|
89
93
|
|
90
|
-
In all applications, `django-fast-treenode` models show excellent performance and stability.
|
91
94
|
|
92
95
|
## Quick start
|
93
96
|
1. Run `pip install django-fast-treenode`.
|
@@ -159,4 +162,4 @@ Released under [MIT License](https://github.com/TimurKady/django-fast-treenode/b
|
|
159
162
|
## Credits
|
160
163
|
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
164
|
|
162
|
-
Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies
|
165
|
+
Special thanks to [Fabio Caccamo](https://github.com/fabiocaccamo) for the idea behind creating a fast Django application for handling hierarchies.
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "django-fast-treenode"
|
7
|
-
version = "2.1.
|
7
|
+
version = "2.1.5"
|
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" }]
|
@@ -12,8 +12,8 @@ license = { file = "LICENSE" }
|
|
12
12
|
requires-python = ">=3.9"
|
13
13
|
dependencies = [
|
14
14
|
"Django >=4.0",
|
15
|
-
"
|
16
|
-
"
|
15
|
+
"django-widget-tweaks >= 1.5",
|
16
|
+
"msgpack >= 1.1"
|
17
17
|
]
|
18
18
|
classifiers = [
|
19
19
|
"Development Status :: 5 - Production/Stable",
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[metadata]
|
2
2
|
name = django-fast-treenode
|
3
|
-
version = 2.1.
|
3
|
+
version = 2.1.5
|
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
|
@@ -33,8 +33,8 @@ classifiers =
|
|
33
33
|
python_requires = >=3.9
|
34
34
|
install_requires =
|
35
35
|
Django >=4.0
|
36
|
-
pympler >=1.0
|
37
36
|
django-widget-tweaks >= 1.5
|
37
|
+
msgpack >= 1.1
|
38
38
|
|
39
39
|
[options.extras_require]
|
40
40
|
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
|
-
|
141
|
-
.
|
142
|
-
|
143
|
-
|
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
|
|