msaas-folder-hierarchy 0.1.0__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.
- msaas_folder_hierarchy-0.1.0/.gitignore +23 -0
- msaas_folder_hierarchy-0.1.0/PKG-INFO +11 -0
- msaas_folder_hierarchy-0.1.0/pyproject.toml +38 -0
- msaas_folder_hierarchy-0.1.0/src/folder_hierarchy/__init__.py +19 -0
- msaas_folder_hierarchy-0.1.0/src/folder_hierarchy/models.py +59 -0
- msaas_folder_hierarchy-0.1.0/src/folder_hierarchy/service.py +216 -0
- msaas_folder_hierarchy-0.1.0/tests/__init__.py +0 -0
- msaas_folder_hierarchy-0.1.0/tests/test_service.py +113 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
.next/
|
|
4
|
+
.turbo/
|
|
5
|
+
*.pyc
|
|
6
|
+
__pycache__/
|
|
7
|
+
.venv/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.pytest_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.env
|
|
12
|
+
.env.*
|
|
13
|
+
!.env.example
|
|
14
|
+
!.env.*.example
|
|
15
|
+
!.env.*.template
|
|
16
|
+
.DS_Store
|
|
17
|
+
coverage/
|
|
18
|
+
|
|
19
|
+
# Runtime artifacts
|
|
20
|
+
logs_llm/
|
|
21
|
+
vectors.db
|
|
22
|
+
vectors.db-shm
|
|
23
|
+
vectors.db-wal
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: msaas-folder-hierarchy
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Materialized path tree structure for hierarchical folders with move, copy, and depth tracking
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: msaas-api-core
|
|
7
|
+
Requires-Dist: msaas-errors
|
|
8
|
+
Requires-Dist: pydantic>=2.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "msaas-folder-hierarchy"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Materialized path tree structure for hierarchical folders with move, copy, and depth tracking"
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"msaas-api-core",
|
|
12
|
+
"msaas-errors",
|
|
13
|
+
"pydantic>=2.0",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=8.0",
|
|
19
|
+
"pytest-asyncio>=0.24",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/folder_hierarchy"]
|
|
24
|
+
|
|
25
|
+
[tool.pytest.ini_options]
|
|
26
|
+
testpaths = ["tests"]
|
|
27
|
+
asyncio_mode = "auto"
|
|
28
|
+
|
|
29
|
+
[tool.ruff]
|
|
30
|
+
target-version = "py312"
|
|
31
|
+
line-length = 100
|
|
32
|
+
|
|
33
|
+
[tool.ruff.lint]
|
|
34
|
+
select = ["E", "F", "I", "N", "W", "UP", "B", "SIM", "TCH"]
|
|
35
|
+
|
|
36
|
+
[tool.uv.sources]
|
|
37
|
+
msaas-api-core = { workspace = true }
|
|
38
|
+
msaas-errors = { workspace = true }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Willian Folder Hierarchy -- Materialized path tree structure management."""
|
|
2
|
+
|
|
3
|
+
from folder_hierarchy.models import (
|
|
4
|
+
FolderCreate,
|
|
5
|
+
FolderMove,
|
|
6
|
+
FolderNode,
|
|
7
|
+
FolderTree,
|
|
8
|
+
FolderUpdate,
|
|
9
|
+
)
|
|
10
|
+
from folder_hierarchy.service import FolderHierarchyService
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"FolderCreate",
|
|
14
|
+
"FolderHierarchyService",
|
|
15
|
+
"FolderMove",
|
|
16
|
+
"FolderNode",
|
|
17
|
+
"FolderTree",
|
|
18
|
+
"FolderUpdate",
|
|
19
|
+
]
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Folder hierarchy data models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _utcnow() -> datetime:
|
|
9
|
+
return datetime.now(UTC)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from typing import Any
|
|
13
|
+
from uuid import uuid4
|
|
14
|
+
|
|
15
|
+
from pydantic import BaseModel, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FolderNode(BaseModel):
|
|
19
|
+
id: str = Field(default_factory=lambda: str(uuid4()))
|
|
20
|
+
name: str
|
|
21
|
+
path: str = "/"
|
|
22
|
+
parent_id: str | None = None
|
|
23
|
+
depth: int = 0
|
|
24
|
+
tenant_id: str | None = None
|
|
25
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
26
|
+
created_at: datetime = Field(default_factory=_utcnow)
|
|
27
|
+
updated_at: datetime = Field(default_factory=_utcnow)
|
|
28
|
+
children: list[FolderNode] = Field(default_factory=list)
|
|
29
|
+
item_count: int = 0
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_root(self) -> bool:
|
|
33
|
+
return self.parent_id is None
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def path_parts(self) -> list[str]:
|
|
37
|
+
return [p for p in self.path.split("/") if p]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class FolderCreate(BaseModel):
|
|
41
|
+
name: str = Field(min_length=1, max_length=255)
|
|
42
|
+
parent_id: str | None = None
|
|
43
|
+
tenant_id: str | None = None
|
|
44
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FolderUpdate(BaseModel):
|
|
48
|
+
name: str | None = Field(default=None, min_length=1, max_length=255)
|
|
49
|
+
metadata: dict[str, Any] | None = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class FolderMove(BaseModel):
|
|
53
|
+
target_parent_id: str | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class FolderTree(BaseModel):
|
|
57
|
+
roots: list[FolderNode] = Field(default_factory=list)
|
|
58
|
+
total_count: int = 0
|
|
59
|
+
max_depth: int = 0
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Folder hierarchy service with in-memory storage."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from datetime import UTC, datetime
|
|
6
|
+
|
|
7
|
+
from folder_hierarchy.models import (
|
|
8
|
+
FolderCreate,
|
|
9
|
+
FolderMove,
|
|
10
|
+
FolderNode,
|
|
11
|
+
FolderTree,
|
|
12
|
+
FolderUpdate,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class FolderHierarchyService:
|
|
17
|
+
"""Manages hierarchical folder structures using materialized paths."""
|
|
18
|
+
|
|
19
|
+
def __init__(self) -> None:
|
|
20
|
+
self._folders: dict[str, FolderNode] = {}
|
|
21
|
+
|
|
22
|
+
def create(self, params: FolderCreate) -> FolderNode:
|
|
23
|
+
if params.parent_id:
|
|
24
|
+
parent = self._get_folder(params.parent_id)
|
|
25
|
+
self._check_duplicate(params.name, params.parent_id, params.tenant_id)
|
|
26
|
+
path = f"{parent.path}{parent.name}/"
|
|
27
|
+
depth = parent.depth + 1
|
|
28
|
+
else:
|
|
29
|
+
self._check_duplicate(params.name, None, params.tenant_id)
|
|
30
|
+
path = "/"
|
|
31
|
+
depth = 0
|
|
32
|
+
|
|
33
|
+
folder = FolderNode(
|
|
34
|
+
name=params.name,
|
|
35
|
+
path=path,
|
|
36
|
+
parent_id=params.parent_id,
|
|
37
|
+
depth=depth,
|
|
38
|
+
tenant_id=params.tenant_id,
|
|
39
|
+
metadata=params.metadata,
|
|
40
|
+
)
|
|
41
|
+
self._folders[folder.id] = folder
|
|
42
|
+
return folder
|
|
43
|
+
|
|
44
|
+
def get(self, folder_id: str) -> FolderNode:
|
|
45
|
+
return self._get_folder(folder_id)
|
|
46
|
+
|
|
47
|
+
def update(self, folder_id: str, params: FolderUpdate) -> FolderNode:
|
|
48
|
+
folder = self._get_folder(folder_id)
|
|
49
|
+
if params.name is not None:
|
|
50
|
+
self._check_duplicate(
|
|
51
|
+
params.name, folder.parent_id, folder.tenant_id, exclude_id=folder_id
|
|
52
|
+
)
|
|
53
|
+
old_path_prefix = f"{folder.path}{folder.name}/"
|
|
54
|
+
folder.name = params.name
|
|
55
|
+
new_path_prefix = f"{folder.path}{folder.name}/"
|
|
56
|
+
self._update_children_paths(old_path_prefix, new_path_prefix)
|
|
57
|
+
if params.metadata is not None:
|
|
58
|
+
folder.metadata = params.metadata
|
|
59
|
+
folder.updated_at = datetime.now(UTC)
|
|
60
|
+
return folder
|
|
61
|
+
|
|
62
|
+
def delete(self, folder_id: str, recursive: bool = False) -> list[str]:
|
|
63
|
+
folder = self._get_folder(folder_id)
|
|
64
|
+
children = self._get_children(folder_id)
|
|
65
|
+
|
|
66
|
+
if children and not recursive:
|
|
67
|
+
from errors import BusinessLogicError
|
|
68
|
+
|
|
69
|
+
raise BusinessLogicError(
|
|
70
|
+
message=f"Folder '{folder.name}' has {len(children)} children. "
|
|
71
|
+
"Use recursive=True to delete."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
deleted_ids: list[str] = []
|
|
75
|
+
if recursive:
|
|
76
|
+
descendants = self._get_descendants(folder_id)
|
|
77
|
+
for d in reversed(descendants):
|
|
78
|
+
del self._folders[d.id]
|
|
79
|
+
deleted_ids.append(d.id)
|
|
80
|
+
|
|
81
|
+
del self._folders[folder_id]
|
|
82
|
+
deleted_ids.append(folder_id)
|
|
83
|
+
return deleted_ids
|
|
84
|
+
|
|
85
|
+
def move(self, folder_id: str, params: FolderMove) -> FolderNode:
|
|
86
|
+
folder = self._get_folder(folder_id)
|
|
87
|
+
|
|
88
|
+
if params.target_parent_id:
|
|
89
|
+
target = self._get_folder(params.target_parent_id)
|
|
90
|
+
if self._is_descendant(params.target_parent_id, folder_id):
|
|
91
|
+
from errors import BusinessLogicError
|
|
92
|
+
|
|
93
|
+
raise BusinessLogicError(message="Cannot move folder into its own descendant")
|
|
94
|
+
self._check_duplicate(
|
|
95
|
+
folder.name, params.target_parent_id, folder.tenant_id, exclude_id=folder_id
|
|
96
|
+
)
|
|
97
|
+
new_path = f"{target.path}{target.name}/"
|
|
98
|
+
new_depth = target.depth + 1
|
|
99
|
+
else:
|
|
100
|
+
self._check_duplicate(folder.name, None, folder.tenant_id, exclude_id=folder_id)
|
|
101
|
+
new_path = "/"
|
|
102
|
+
new_depth = 0
|
|
103
|
+
|
|
104
|
+
old_path_prefix = f"{folder.path}{folder.name}/"
|
|
105
|
+
folder.parent_id = params.target_parent_id
|
|
106
|
+
folder.path = new_path
|
|
107
|
+
folder.depth = new_depth
|
|
108
|
+
folder.updated_at = datetime.now(UTC)
|
|
109
|
+
|
|
110
|
+
new_path_prefix = f"{new_path}{folder.name}/"
|
|
111
|
+
self._update_children_paths(old_path_prefix, new_path_prefix)
|
|
112
|
+
self._update_children_depth(folder_id, new_depth + 1)
|
|
113
|
+
|
|
114
|
+
return folder
|
|
115
|
+
|
|
116
|
+
def get_tree(self, tenant_id: str | None = None) -> FolderTree:
|
|
117
|
+
roots = [
|
|
118
|
+
f
|
|
119
|
+
for f in self._folders.values()
|
|
120
|
+
if f.parent_id is None and (tenant_id is None or f.tenant_id == tenant_id)
|
|
121
|
+
]
|
|
122
|
+
for root in roots:
|
|
123
|
+
self._build_tree(root)
|
|
124
|
+
max_depth = max((f.depth for f in self._folders.values()), default=0)
|
|
125
|
+
return FolderTree(
|
|
126
|
+
roots=roots,
|
|
127
|
+
total_count=len(self._folders),
|
|
128
|
+
max_depth=max_depth,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def get_children(self, folder_id: str) -> list[FolderNode]:
|
|
132
|
+
self._get_folder(folder_id)
|
|
133
|
+
return self._get_children(folder_id)
|
|
134
|
+
|
|
135
|
+
def get_ancestors(self, folder_id: str) -> list[FolderNode]:
|
|
136
|
+
folder = self._get_folder(folder_id)
|
|
137
|
+
ancestors: list[FolderNode] = []
|
|
138
|
+
current = folder
|
|
139
|
+
while current.parent_id:
|
|
140
|
+
parent = self._folders.get(current.parent_id)
|
|
141
|
+
if parent is None:
|
|
142
|
+
break
|
|
143
|
+
ancestors.append(parent)
|
|
144
|
+
current = parent
|
|
145
|
+
ancestors.reverse()
|
|
146
|
+
return ancestors
|
|
147
|
+
|
|
148
|
+
def get_breadcrumb(self, folder_id: str) -> list[dict[str, str]]:
|
|
149
|
+
ancestors = self.get_ancestors(folder_id)
|
|
150
|
+
folder = self._get_folder(folder_id)
|
|
151
|
+
return [{"id": a.id, "name": a.name} for a in ancestors] + [
|
|
152
|
+
{"id": folder.id, "name": folder.name}
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
def _get_folder(self, folder_id: str) -> FolderNode:
|
|
156
|
+
folder = self._folders.get(folder_id)
|
|
157
|
+
if folder is None:
|
|
158
|
+
from errors import NotFoundError
|
|
159
|
+
|
|
160
|
+
raise NotFoundError(message=f"Folder {folder_id} not found")
|
|
161
|
+
return folder
|
|
162
|
+
|
|
163
|
+
def _get_children(self, parent_id: str) -> list[FolderNode]:
|
|
164
|
+
return [f for f in self._folders.values() if f.parent_id == parent_id]
|
|
165
|
+
|
|
166
|
+
def _get_descendants(self, folder_id: str) -> list[FolderNode]:
|
|
167
|
+
descendants: list[FolderNode] = []
|
|
168
|
+
children = self._get_children(folder_id)
|
|
169
|
+
for child in children:
|
|
170
|
+
descendants.append(child)
|
|
171
|
+
descendants.extend(self._get_descendants(child.id))
|
|
172
|
+
return descendants
|
|
173
|
+
|
|
174
|
+
def _is_descendant(self, folder_id: str, ancestor_id: str) -> bool:
|
|
175
|
+
current = self._folders.get(folder_id)
|
|
176
|
+
while current:
|
|
177
|
+
if current.id == ancestor_id:
|
|
178
|
+
return True
|
|
179
|
+
if current.parent_id is None:
|
|
180
|
+
break
|
|
181
|
+
current = self._folders.get(current.parent_id)
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
def _check_duplicate(
|
|
185
|
+
self,
|
|
186
|
+
name: str,
|
|
187
|
+
parent_id: str | None,
|
|
188
|
+
tenant_id: str | None,
|
|
189
|
+
exclude_id: str | None = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
for f in self._folders.values():
|
|
192
|
+
if (
|
|
193
|
+
f.name == name
|
|
194
|
+
and f.parent_id == parent_id
|
|
195
|
+
and f.tenant_id == tenant_id
|
|
196
|
+
and f.id != exclude_id
|
|
197
|
+
):
|
|
198
|
+
from errors import ConflictError
|
|
199
|
+
|
|
200
|
+
raise ConflictError(message=f"Folder '{name}' already exists in this location")
|
|
201
|
+
|
|
202
|
+
def _build_tree(self, node: FolderNode) -> None:
|
|
203
|
+
node.children = self._get_children(node.id)
|
|
204
|
+
for child in node.children:
|
|
205
|
+
self._build_tree(child)
|
|
206
|
+
|
|
207
|
+
def _update_children_paths(self, old_prefix: str, new_prefix: str) -> None:
|
|
208
|
+
for f in self._folders.values():
|
|
209
|
+
if f.path.startswith(old_prefix):
|
|
210
|
+
f.path = new_prefix + f.path[len(old_prefix) :]
|
|
211
|
+
|
|
212
|
+
def _update_children_depth(self, parent_id: str, depth: int) -> None:
|
|
213
|
+
children = self._get_children(parent_id)
|
|
214
|
+
for child in children:
|
|
215
|
+
child.depth = depth
|
|
216
|
+
self._update_children_depth(child.id, depth + 1)
|
|
File without changes
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Tests for folder hierarchy service."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from folder_hierarchy.models import FolderCreate, FolderMove, FolderUpdate
|
|
8
|
+
from folder_hierarchy.service import FolderHierarchyService
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestFolderHierarchyService:
|
|
12
|
+
def setup_method(self) -> None:
|
|
13
|
+
self.service = FolderHierarchyService()
|
|
14
|
+
|
|
15
|
+
def test_create_root_folder(self) -> None:
|
|
16
|
+
folder = self.service.create(FolderCreate(name="Documents"))
|
|
17
|
+
assert folder.name == "Documents"
|
|
18
|
+
assert folder.path == "/"
|
|
19
|
+
assert folder.depth == 0
|
|
20
|
+
assert folder.is_root is True
|
|
21
|
+
|
|
22
|
+
def test_create_nested_folder(self) -> None:
|
|
23
|
+
root = self.service.create(FolderCreate(name="Documents"))
|
|
24
|
+
child = self.service.create(FolderCreate(name="Reports", parent_id=root.id))
|
|
25
|
+
assert child.path == "/Documents/"
|
|
26
|
+
assert child.depth == 1
|
|
27
|
+
assert child.parent_id == root.id
|
|
28
|
+
|
|
29
|
+
def test_create_deeply_nested(self) -> None:
|
|
30
|
+
root = self.service.create(FolderCreate(name="A"))
|
|
31
|
+
b = self.service.create(FolderCreate(name="B", parent_id=root.id))
|
|
32
|
+
c = self.service.create(FolderCreate(name="C", parent_id=b.id))
|
|
33
|
+
assert c.path == "/A/B/"
|
|
34
|
+
assert c.depth == 2
|
|
35
|
+
|
|
36
|
+
def test_duplicate_name_same_parent_fails(self) -> None:
|
|
37
|
+
self.service.create(FolderCreate(name="Docs"))
|
|
38
|
+
with pytest.raises(Exception, match="already exists"):
|
|
39
|
+
self.service.create(FolderCreate(name="Docs"))
|
|
40
|
+
|
|
41
|
+
def test_same_name_different_parent_ok(self) -> None:
|
|
42
|
+
root1 = self.service.create(FolderCreate(name="A"))
|
|
43
|
+
root2 = self.service.create(FolderCreate(name="B"))
|
|
44
|
+
f1 = self.service.create(FolderCreate(name="Docs", parent_id=root1.id))
|
|
45
|
+
f2 = self.service.create(FolderCreate(name="Docs", parent_id=root2.id))
|
|
46
|
+
assert f1.id != f2.id
|
|
47
|
+
|
|
48
|
+
def test_update_name(self) -> None:
|
|
49
|
+
folder = self.service.create(FolderCreate(name="Old"))
|
|
50
|
+
updated = self.service.update(folder.id, FolderUpdate(name="New"))
|
|
51
|
+
assert updated.name == "New"
|
|
52
|
+
|
|
53
|
+
def test_delete_empty_folder(self) -> None:
|
|
54
|
+
folder = self.service.create(FolderCreate(name="Temp"))
|
|
55
|
+
deleted = self.service.delete(folder.id)
|
|
56
|
+
assert folder.id in deleted
|
|
57
|
+
|
|
58
|
+
def test_delete_with_children_fails(self) -> None:
|
|
59
|
+
root = self.service.create(FolderCreate(name="Root"))
|
|
60
|
+
self.service.create(FolderCreate(name="Child", parent_id=root.id))
|
|
61
|
+
with pytest.raises(Exception, match="children"):
|
|
62
|
+
self.service.delete(root.id)
|
|
63
|
+
|
|
64
|
+
def test_delete_recursive(self) -> None:
|
|
65
|
+
root = self.service.create(FolderCreate(name="Root"))
|
|
66
|
+
child = self.service.create(FolderCreate(name="Child", parent_id=root.id))
|
|
67
|
+
deleted = self.service.delete(root.id, recursive=True)
|
|
68
|
+
assert len(deleted) == 2
|
|
69
|
+
|
|
70
|
+
def test_move_folder(self) -> None:
|
|
71
|
+
a = self.service.create(FolderCreate(name="A"))
|
|
72
|
+
b = self.service.create(FolderCreate(name="B"))
|
|
73
|
+
child = self.service.create(FolderCreate(name="X", parent_id=a.id))
|
|
74
|
+
moved = self.service.move(child.id, FolderMove(target_parent_id=b.id))
|
|
75
|
+
assert moved.parent_id == b.id
|
|
76
|
+
assert moved.path == "/B/"
|
|
77
|
+
|
|
78
|
+
def test_move_into_descendant_fails(self) -> None:
|
|
79
|
+
root = self.service.create(FolderCreate(name="Root"))
|
|
80
|
+
child = self.service.create(FolderCreate(name="Child", parent_id=root.id))
|
|
81
|
+
with pytest.raises(Exception, match="descendant"):
|
|
82
|
+
self.service.move(root.id, FolderMove(target_parent_id=child.id))
|
|
83
|
+
|
|
84
|
+
def test_get_tree(self) -> None:
|
|
85
|
+
root = self.service.create(FolderCreate(name="Root"))
|
|
86
|
+
self.service.create(FolderCreate(name="Child", parent_id=root.id))
|
|
87
|
+
tree = self.service.get_tree()
|
|
88
|
+
assert tree.total_count == 2
|
|
89
|
+
assert len(tree.roots) == 1
|
|
90
|
+
assert len(tree.roots[0].children) == 1
|
|
91
|
+
|
|
92
|
+
def test_get_ancestors(self) -> None:
|
|
93
|
+
a = self.service.create(FolderCreate(name="A"))
|
|
94
|
+
b = self.service.create(FolderCreate(name="B", parent_id=a.id))
|
|
95
|
+
c = self.service.create(FolderCreate(name="C", parent_id=b.id))
|
|
96
|
+
ancestors = self.service.get_ancestors(c.id)
|
|
97
|
+
assert len(ancestors) == 2
|
|
98
|
+
assert ancestors[0].name == "A"
|
|
99
|
+
assert ancestors[1].name == "B"
|
|
100
|
+
|
|
101
|
+
def test_get_breadcrumb(self) -> None:
|
|
102
|
+
a = self.service.create(FolderCreate(name="A"))
|
|
103
|
+
b = self.service.create(FolderCreate(name="B", parent_id=a.id))
|
|
104
|
+
crumbs = self.service.get_breadcrumb(b.id)
|
|
105
|
+
assert len(crumbs) == 2
|
|
106
|
+
assert crumbs[0]["name"] == "A"
|
|
107
|
+
assert crumbs[1]["name"] == "B"
|
|
108
|
+
|
|
109
|
+
def test_tenant_isolation(self) -> None:
|
|
110
|
+
self.service.create(FolderCreate(name="X", tenant_id="t1"))
|
|
111
|
+
self.service.create(FolderCreate(name="X", tenant_id="t2"))
|
|
112
|
+
tree_t1 = self.service.get_tree(tenant_id="t1")
|
|
113
|
+
assert len(tree_t1.roots) == 1
|