django-fast-treenode 3.2.0__py3-none-any.whl → 3.2.1__py3-none-any.whl

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-fast-treenode
3
- Version: 3.2.0
3
+ Version: 3.2.1
4
4
  Summary: Treenode Framework for supporting tree (hierarchical) data structure in Django projects
5
5
  Home-page: https://django-fast-treenode.readthedocs.io/
6
6
  Author: Timur Kady
@@ -1,4 +1,4 @@
1
- django_fast_treenode-3.2.0.dist-info/licenses/LICENSE,sha256=SSYqS84FCnAW7tAxmjBKU8qAa8Jv4VGPuSSGeHwWtJE,1095
1
+ django_fast_treenode-3.2.1.dist-info/licenses/LICENSE,sha256=SSYqS84FCnAW7tAxmjBKU8qAa8Jv4VGPuSSGeHwWtJE,1095
2
2
  treenode/__init__.py,sha256=3z1hWpHyy4wg6uz7HCmRi9FaXYeN5CfANVpa77UIoPw,53
3
3
  treenode/apps.py,sha256=QlwjNDM9rkUoWB8Vm8-OkS6lNx0-aTByuGZlu9wrQMs,1832
4
4
  treenode/cache.py,sha256=2jUiiecfFxwB7QFukpU4u0FnDzGH6hNRfo6KAYvs6vM,8447
@@ -7,7 +7,7 @@ treenode/settings.py,sha256=FRGK7hl_Tnxp4sGbUNxJgEQP9niDJjLhcNBtmloOvHk,741
7
7
  treenode/signals.py,sha256=ERrlKjGqhYaPYVKKRk1JBBlPFOmJKpJ6bXsJavcTlo0,518
8
8
  treenode/tests.py,sha256=2uDafv3Ns6f7Vy1ekUtgYxCZEi1KRyesZDTAFhYcX-E,63
9
9
  treenode/urls.py,sha256=krHvVigc_dxC0z5hEd2rgeH6th8jW7qJY3Qbia-419Y,240
10
- treenode/version.py,sha256=7TedO4TEmiuyAVChT2JQ953v0toWdhTtfA4d5VIsuCE,220
10
+ treenode/version.py,sha256=2xptvV9MUEglYmGGtVaEX6WiMnY6GLv_F951eiBLmJk,220
11
11
  treenode/widgets.py,sha256=3kSby7v-gpyUHmAIFZCENM8hzT1xcHJFB4op8XU6YEQ,4035
12
12
  treenode/admin/__init__.py,sha256=XNEYHdF5lKb0vpdlVxdR2fxj5oUgzyx1YyCwsv0gxHw,100
13
13
  treenode/admin/admin.py,sha256=cbiqlzXwK-W73DTJqvXo_abtnK1JwEwyBEOl0bmgsa4,7507
@@ -18,21 +18,22 @@ treenode/admin/mixin.py,sha256=7hcjoh8W2_R2in0EorNjVTXakYP3I1mMZKIr65UsU-g,10677
18
18
  treenode/managers/__init__.py,sha256=c7F9Ku9489Hv6lTpUY2nbyBlWFCXBWAkNBm4xTKcjL8,186
19
19
  treenode/managers/managers.py,sha256=G1dayrqaEX5nQiKsHS_2y6o3iXKIAn66RKArHttN-kU,7174
20
20
  treenode/managers/queries.py,sha256=Kepax8SDn7G5tOlPRWBCp5Oyp49O5iMITCMBoNCm_Ak,10655
21
- treenode/managers/tasks.py,sha256=nVlGDxNnFlZS1-oU8UH_yTXJERcGrAZ9kCmKwKO5WuY,7138
21
+ treenode/managers/tasks.py,sha256=XGV7zei6LQJqQx6Ozg5lWC2A7770wn6rvWJ5jyJ_8pw,7796
22
22
  treenode/models/__init__.py,sha256=iR4ksCKoayvkIWWgGk6OUGHZC3D0mzAtgdBcS2vQPBw,188
23
23
  treenode/models/decorators.py,sha256=N2dcnWqSCiEXDcYCf0zVijrbGUC8kYlqOLi_GKFmECU,1457
24
24
  treenode/models/factory.py,sha256=sPUSrvo1za-r6ny3B8ptwevyjO8-iUpPNrT0eSD2kvI,1786
25
- treenode/models/models.py,sha256=93AxBPQseV3TaKhYoX173HMgV1wIFDjwGg9YVBvtPNQ,12335
26
- treenode/models/mixins/__init__.py,sha256=aALVKMGAWbgMAeKWS6s-NF3L5FmRX96mQxtpthOX-Ec,805
25
+ treenode/models/models.py,sha256=ClY1LaxAikqgdLIOiuKfb8srTpj5gUcSGmB50oA6REA,12969
26
+ treenode/models/mixins/__init__.py,sha256=CAef4rue_uYeBdcswPZAOIEwsKqGLhL_XFco7JwcvdI,909
27
27
  treenode/models/mixins/ancestors.py,sha256=9g-0nPHoiF_SX2kN4uDLdbWyw-TDCz1YqxLJngwTZOQ,1971
28
- treenode/models/mixins/children.py,sha256=H9iMqgucSmwLX-3O3QUj1a2PUQTmmWZ4GPPjRZ9a5E4,2399
29
- treenode/models/mixins/descendants.py,sha256=RKowr29JUUO3E_UDvbLbMLhbn5IUkY2eh3AXfU8XSE8,2165
28
+ treenode/models/mixins/children.py,sha256=H0faiH24ihXCUEWGjTcjoIAKl26E8RQcQaTTkgKFz00,2397
29
+ treenode/models/mixins/descendants.py,sha256=5xoeIdRhZ94K-EMrEjGgD7KW668gyXxp8JyB3t3mFAU,2163
30
30
  treenode/models/mixins/family.py,sha256=MB5kWRVvxU_xmSgCekveTP5Vhj4wJki8bU7hzn9RNLE,1673
31
31
  treenode/models/mixins/logical.py,sha256=gh5wv5XZDs5GWarU6g9zKXWNwji7SE3zSVNIpywDWjw,2190
32
32
  treenode/models/mixins/node.py,sha256=D3lwWsN2eRJeS99O7CaVAhabTYWA_NSY8z60zAhFAs0,8576
33
33
  treenode/models/mixins/properties.py,sha256=1O6p2tfvOesBooZeOeuXi8yfEO_o-5gn_agtKqMxU-s,3945
34
- treenode/models/mixins/roots.py,sha256=2lDjUH22NZFUMVbVZgsdtEqDoUyHhGTiaoeo9h3KRfk,4102
35
- treenode/models/mixins/siblings.py,sha256=4XvQS7WkgolzEZdnURhIClo-VUcpuqQ-Sc7PDYKGmFw,3069
34
+ treenode/models/mixins/roots.py,sha256=wfuf6jdkcfkEUa42n0bHD_bQDE78AEFK2ZVCeBh7FZw,4100
35
+ treenode/models/mixins/search.py,sha256=ZURelSNFT3wK3A0SnYvfHb2WnfrN_c7BIatp6ffGfsY,1652
36
+ treenode/models/mixins/siblings.py,sha256=o0Iorfe35V1A9iAKwV8CqALPuTjjcQcnEAjAdPE6haI,3067
36
37
  treenode/models/mixins/tree.py,sha256=5Cew5c-NtTmz15S_ueDZ-fojHtQKZEKNxmp3W6qlC-8,14512
37
38
  treenode/models/mixins/update.py,sha256=oCZkMnfT23n2n3_mnokDrtTLS_jO_lJRwG3ENkH_DE4,4948
38
39
  treenode/static/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -79,7 +80,7 @@ treenode/templatetags/treenode_admin.py,sha256=5f5oqAS4zC_f0kkJRsm5MqXHjsKJXn8Gs
79
80
  treenode/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
80
81
  treenode/utils/jwt_auth.py,sha256=LSTMuBFbH1DPJ0pVUnD_T1owb6GurpkJcCxpsXDL4HQ,845
81
82
  treenode/utils/db/__init__.py,sha256=RwicAcJSI1nhIPWLdT7j9TFsgOc9834VDn9lVn54GlY,247
82
- treenode/utils/db/compiler.py,sha256=PgD9ybS5H8OUHw1gkFBQHhnrf5HiCx8QXUMRhydwh7o,3824
83
+ treenode/utils/db/compiler.py,sha256=2evMIg-bqDNbRqZub6ePNlQi9jnS_YbLe3CmOa_4XC0,2519
83
84
  treenode/utils/db/db_vendor.py,sha256=4SyEHl51jVCDB3is4omHCf2bTB_QV3RvemUQYxJP5m0,930
84
85
  treenode/utils/db/service.py,sha256=PF85Yhz2xUWFFCzpLYotmiNTZXXEH61rhswslSxEUds,2640
85
86
  treenode/utils/db/sqlcompat.py,sha256=K71ggkKIvpdTtHQ6Y4qcbo6cj2eYiEfy6DlVBr8Po1E,4460
@@ -91,7 +92,7 @@ treenode/views/children.py,sha256=seO0SNKriRY2FJOO1oZgX42iKolXMf3EF76GrfdJZLQ,11
91
92
  treenode/views/common.py,sha256=kUN3IgMZCdNdJ2haxB9MTGcn2rctyFUAHNagzcu9wXk,594
92
93
  treenode/views/crud.py,sha256=RI5rdyD4hZTszjZFThByxi_lkAeJlqbDCXFkD8iyzKE,7424
93
94
  treenode/views/search.py,sha256=c_GyooT3jyoNa96bBxfoWruRN1wIw-ZGYvwGKkGojTs,1501
94
- django_fast_treenode-3.2.0.dist-info/METADATA,sha256=q3lF9MZCoD6ZJO9DuApkCEwIEMqPCr75zjvUWOpZakw,10377
95
- django_fast_treenode-3.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
96
- django_fast_treenode-3.2.0.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
97
- django_fast_treenode-3.2.0.dist-info/RECORD,,
95
+ django_fast_treenode-3.2.1.dist-info/METADATA,sha256=EOaUVLtr2Go1Shmy1qFlVMJ2MKrjr4nkM3VPecw5dgQ,10377
96
+ django_fast_treenode-3.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
+ django_fast_treenode-3.2.1.dist-info/top_level.txt,sha256=fmgxHbXyx1O2MPi_9kjx8aL9L-8TmV0gre4Go8XgqFk,9
98
+ django_fast_treenode-3.2.1.dist-info/RECORD,,
@@ -137,7 +137,25 @@ class TreeTaskQueue:
137
137
  if not merged:
138
138
  result_set.add(current)
139
139
 
140
- return [{"mode": "update", "parent_id": pk} for pk in sorted(result_set)]
140
+ if not result_set:
141
+ return []
142
+
143
+ depth_lookup = {}
144
+ if result_set:
145
+ with connection.cursor() as cursor:
146
+ format_ids = ', '.join(['%s'] * len(result_set))
147
+ sql = f"""
148
+ SELECT id, _depth
149
+ FROM {self.model._meta.db_table}
150
+ WHERE id IN ({format_ids})
151
+ """
152
+ cursor.execute(sql, list(result_set))
153
+ for node_id, depth in cursor.fetchall():
154
+ depth_lookup[node_id] = depth
155
+
156
+
157
+ ordered_pks = sorted(result_set, key=lambda pk: (depth_lookup.get(pk, 0), pk))
158
+ return [{"mode": "update", "parent_id": pk} for pk in ordered_pks]
141
159
 
142
160
  def _get_root_ids(self):
143
161
  """Return root node IDs."""
@@ -7,6 +7,7 @@ from .family import TreeNodeFamilyMixin
7
7
  from .logical import TreeNodeLogicalMixin
8
8
  from .node import TreeNodeNodeMixin
9
9
  from .properties import TreeNodePropertiesMixin
10
+ from .search import TreeNodeSearchMixin
10
11
  from .roots import TreeNodeRootsMixin
11
12
  from .siblings import TreeNodeSiblingsMixin
12
13
  from .tree import TreeNodeTreeMixin
@@ -16,7 +17,8 @@ from .update import RawSQLMixin
16
17
  __all__ = [
17
18
  "TreeNodeAncestorsMixin", "TreeNodeChildrenMixin", "TreeNodeFamilyMixin",
18
19
  "TreeNodeDescendantsMixin", "TreeNodeLogicalMixin", "TreeNodeNodeMixin",
19
- "TreeNodePropertiesMixin", "TreeNodeRootsMixin", "TreeNodeSiblingsMixin",
20
+ "TreeNodeSearchMixin", "TreeNodePropertiesMixin", "TreeNodeRootsMixin",
21
+ "TreeNodeSiblingsMixin", "TreeNodeTreeMixin", "RawSQLMixin"
20
22
  "TreeNodeTreeMixin", "RawSQLMixin"
21
23
  ]
22
24
 
@@ -86,4 +86,4 @@ class TreeNodeChildrenMixin(models.Model):
86
86
  """Get the last child node or None if it has no children."""
87
87
  return self.get_children_queryset().last()
88
88
 
89
- # The End
89
+ # The End
@@ -74,4 +74,4 @@ class TreeNodeDescendantsMixin(models.Model):
74
74
  )
75
75
 
76
76
 
77
- # The End
77
+ # The End
@@ -137,4 +137,4 @@ class TreeNodeRootsMixin(models.Model):
137
137
  cursor.execute(query, params)
138
138
 
139
139
 
140
- # The End
140
+ # The End
@@ -0,0 +1,55 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ TreeNode Search Mixin
4
+
5
+ Version: 3.2.1
6
+ Author: Timur Kady
7
+ Email: timurkady@yandex.com
8
+ """
9
+
10
+ from django.db import models
11
+
12
+
13
+ class TreeNodeSearchMixin(models.Model):
14
+ """Mixin that provides helper methods for quick node lookup."""
15
+
16
+ class Meta:
17
+ """Mixin Meta Class."""
18
+ abstract = True
19
+
20
+ @classmethod
21
+ def find_by_path(cls, path, attr="id", delimiter="/"):
22
+ """Return a node referenced by breadcrumbs path.
23
+
24
+ The ``path`` argument may be a string or an iterable of breadcrumb
25
+ values previously produced by :py:meth:`get_breadcrumbs`.
26
+ If the path cannot be resolved, ``None`` is returned.
27
+ """
28
+ if path is None:
29
+ return None
30
+
31
+ if not isinstance(path, (list, tuple)):
32
+ path = [p for p in str(path).strip(delimiter).split(delimiter) if p]
33
+
34
+ parent = None
35
+ for token in path:
36
+ lookup = {attr: token}
37
+ if parent is None:
38
+ qs = cls.objects.filter(parent_id__isnull=True, **lookup)
39
+ else:
40
+ qs = cls.objects.filter(parent=parent, **lookup)
41
+ parent = qs.first()
42
+ if parent is None:
43
+ return None
44
+ return parent
45
+
46
+ @classmethod
47
+ def find_in_subtree(cls, parent, value, attr="id"):
48
+ """Search ``parent`` descendants for attribute ``attr`` equal to ``value``."""
49
+ if parent is None:
50
+ return None
51
+ prefix = parent.get_order() + "."
52
+ return cls.objects.filter(_path__startswith=prefix, **{attr: value}).first()
53
+
54
+
55
+ # The End
@@ -99,4 +99,4 @@ class TreeNodeSiblingsMixin(models.Model):
99
99
  qs = self._meta.model.objects.filter(parent_id=self._parent_id)
100
100
  return qs.last()
101
101
 
102
- # The End
102
+ # The End
treenode/models/models.py CHANGED
@@ -45,8 +45,9 @@ class TreeNodeModel(
45
45
  mx.TreeNodeAncestorsMixin, mx.TreeNodeChildrenMixin,
46
46
  mx.TreeNodeFamilyMixin, mx.TreeNodeDescendantsMixin,
47
47
  mx.TreeNodeLogicalMixin, mx.TreeNodeNodeMixin,
48
- mx.TreeNodePropertiesMixin, mx.TreeNodeRootsMixin,
49
- mx.TreeNodeSiblingsMixin, mx.TreeNodeTreeMixin, mx.RawSQLMixin,
48
+ mx.TreeNodeSearchMixin, mx.TreeNodePropertiesMixin,
49
+ mx.TreeNodeRootsMixin, mx.TreeNodeSiblingsMixin,
50
+ mx.TreeNodeTreeMixin, mx.RawSQLMixin,
50
51
  models.Model, metaclass=TreeNodeModelBase):
51
52
  """
52
53
  Abstract tree node model.
@@ -186,6 +187,7 @@ class TreeNodeModel(
186
187
  is_new = False
187
188
  is_shift = False
188
189
  is_move = False
190
+ sorting_changed = False
189
191
 
190
192
  if self.pk:
191
193
  state = self.get_db_state()
@@ -194,6 +196,14 @@ class TreeNodeModel(
194
196
  is_move = self.parent_id != state["parent_id"]
195
197
  if is_move:
196
198
  self._meta.model.tasks.add("update", state["parent_id"])
199
+
200
+ if self.sorting_field != "priority":
201
+ old_value = self.__class__.objects.filter(
202
+ pk=self.pk
203
+ ).values_list(self.sorting_field, flat=True).first()
204
+ sorting_changed = old_value != getattr(
205
+ self, self.sorting_field
206
+ )
197
207
  else:
198
208
  logger.error(
199
209
  "TreeNodeModel save error: object with pk %s not found in DB",
@@ -231,12 +241,14 @@ class TreeNodeModel(
231
241
  pass
232
242
  """
233
243
 
234
- if is_new or is_move or is_shift:
244
+ if is_new or is_move or is_shift or sorting_changed:
235
245
  # Step 1: Shift siblings
236
246
  if (is_new or is_move) and (self.priority is not None):
237
247
  self._shift_siblings_forward()
238
248
  # Step 2: Update paths for the new parent -> sqlq
239
- self._meta.model.tasks.add("update", self.parent_id)
249
+ path_ids = self.query.get_relative_pks(objects="ancestors", include_self=True)
250
+ update_root_id = path_ids[0] if path_ids else self.parent_id
251
+ self._meta.model.tasks.add("update", update_root_id)
240
252
  # Step 3: Clear model cache
241
253
  self.clear_cache()
242
254
 
@@ -245,7 +257,7 @@ class TreeNodeModel(
245
257
  disable_signals(post_save, model)):
246
258
  super().save(*args, **kwargs)
247
259
 
248
- if is_new or is_move or is_shift:
260
+ if is_new or is_move or is_shift or sorting_changed:
249
261
  # Run sql
250
262
  # self.sqlq.flush()
251
263
  setattr(model, 'is_dry', True)
@@ -26,88 +26,47 @@ class TreePathCompiler:
26
26
 
27
27
  @classmethod
28
28
  def update_path(cls, model, parent_id=None):
29
- """
30
- Rebuild subtree starting from parent_id.
31
-
32
- If parent_id=None, then the whole tree is rebuilt.
33
- Uses only fields: parent_id and id. All others (priority, _path,
34
- _depth) are recalculated.
35
- """
36
- db_table = model._meta.db_table
37
- # Will eliminate the risk if the user names the model order or user.
38
- qname = connection.ops.quote_name(db_table)
39
-
40
- sorting_field = model.sorting_field
41
- sorting_fields = ["priority", "id"] if sorting_field == "priority" else [sorting_field] # noqa: D5017
42
- sort_expr = ", ".join([
43
- f"c.{field}" if "." not in field else field
44
- for field in sorting_fields
45
- ])
46
-
47
- cte_header = "(id, parent_id, new_priority, new_path, new_depth)"
48
-
49
- row_number_expr = f"ROW_NUMBER() OVER (ORDER BY {sort_expr}) - 1"
50
- hex_expr = SQLCompat.to_hex(row_number_expr)
51
- lpad_expr = SQLCompat.lpad(hex_expr, SEGMENT_LENGTH, "'0'")
29
+ """Rebuild subtree using BFS so parents update before children."""
30
+ table = model._meta.db_table
31
+ sort_field = model.sorting_field
32
+
33
+ def fetch_children(pid):
34
+ if pid is None:
35
+ where = "parent_id IS NULL"
36
+ params = []
37
+ else:
38
+ where = "parent_id = %s"
39
+ params = [pid]
40
+ with connection.cursor() as cursor:
41
+ cursor.execute(
42
+ f"SELECT id FROM {table} WHERE {where} ORDER BY {sort_field}, id",
43
+ params,
44
+ )
45
+ return [row[0] for row in cursor.fetchall()]
46
+
47
+ queue = []
52
48
 
53
49
  if parent_id is None:
54
- new_path_expr = lpad_expr
55
- base_sql = f"""
56
- SELECT
57
- c.id,
58
- c.parent_id,
59
- {row_number_expr} AS new_priority,
60
- {new_path_expr} AS new_path,
61
- 0 AS new_depth
62
- FROM {qname} AS c
63
- WHERE c.parent_id IS NULL
64
- """
65
- params = []
50
+ for idx, node_id in enumerate(fetch_children(None)):
51
+ queue.append((node_id, "", 0, idx))
66
52
  else:
67
- path_expr = SQLCompat.concat("p._path", "'.'", lpad_expr)
68
- base_sql = f"""
69
- SELECT
70
- c.id,
71
- c.parent_id,
72
- {row_number_expr} AS new_priority,
73
- {path_expr} AS new_path,
74
- p._depth + 1 AS new_depth
75
- FROM {qname} c
76
- JOIN {qname} p ON c.parent_id = p.id
77
- WHERE p.id = %s
78
- """
79
- params = [parent_id]
80
-
81
- recursive_row_number_expr = f"ROW_NUMBER() OVER (PARTITION BY c.parent_id ORDER BY {sort_expr}) - 1"
82
- recursive_hex_expr = SQLCompat.to_hex(recursive_row_number_expr)
83
- recursive_lpad_expr = SQLCompat.lpad(
84
- recursive_hex_expr, SEGMENT_LENGTH, "'0'")
85
- recursive_path_expr = SQLCompat.concat(
86
- "t.new_path", "'.'", recursive_lpad_expr)
87
-
88
- recursive_sql = f"""
89
- SELECT
90
- c.id,
91
- c.parent_id,
92
- {recursive_row_number_expr} AS new_priority,
93
- {recursive_path_expr} AS new_path,
94
- t.new_depth + 1 AS new_depth
95
- FROM {qname} c
96
- JOIN tree_cte t ON c.parent_id = t.id
97
- """
98
-
99
- final_sql = SQLCompat.update_from(
100
- db_table=db_table,
101
- cte_header=cte_header,
102
- base_sql=base_sql,
103
- recursive_sql=recursive_sql,
104
- update_fields=["priority", "_path", "_depth"]
105
- )
106
-
107
- with connection.cursor() as cursor:
108
- # Make params read-only
109
- params = tuple(params)
110
- cursor.execute(final_sql, params)
53
+ parent_data = model.objects.filter(pk=parent_id).values("_path", "_depth").first()
54
+ parent_path = parent_data["_path"] if parent_data else ""
55
+ depth = (parent_data["_depth"] + 1) if parent_data else 0
56
+ for idx, node_id in enumerate(fetch_children(parent_id)):
57
+ queue.append((node_id, parent_path, depth, idx))
58
+
59
+ while queue:
60
+ node_id, base_path, depth, index = queue.pop(0)
61
+ segment = f"{index:0{SEGMENT_LENGTH}X}"
62
+ path = segment if base_path == "" else f"{base_path}.{segment}"
63
+ with connection.cursor() as cursor:
64
+ cursor.execute(
65
+ f"UPDATE {table} SET priority=%s, _path=%s, _depth=%s WHERE id=%s",
66
+ [index, path, depth, node_id],
67
+ )
68
+ for child_idx, child_id in enumerate(fetch_children(node_id)):
69
+ queue.append((child_id, path, depth + 1, child_idx))
111
70
 
112
71
 
113
72
  # The End
treenode/version.py CHANGED
@@ -4,9 +4,9 @@ TreeNode Version Module
4
4
 
5
5
  This module defines the current version of the TreeNode package.
6
6
 
7
- Version: 3.2.0
7
+ Version: 3.2.1
8
8
  Author: Timur Kady
9
9
  Email: timurkady@yandex.com
10
10
  """
11
11
 
12
- __version__ = '3.2.0'
12
+ __version__ = '3.2.1'