basic-memory 0.7.0__py3-none-any.whl → 0.17.4__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.
Potentially problematic release.
This version of basic-memory might be problematic. Click here for more details.
- basic_memory/__init__.py +5 -1
- basic_memory/alembic/alembic.ini +119 -0
- basic_memory/alembic/env.py +130 -20
- basic_memory/alembic/migrations.py +4 -9
- basic_memory/alembic/versions/314f1ea54dc4_add_postgres_full_text_search_support_.py +131 -0
- basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
- basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +120 -0
- basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +112 -0
- basic_memory/alembic/versions/6830751f5fb6_merge_multiple_heads.py +24 -0
- basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
- basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
- basic_memory/alembic/versions/a2b3c4d5e6f7_add_search_index_entity_cascade.py +56 -0
- basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
- basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +113 -0
- basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
- basic_memory/alembic/versions/f8a9b2c3d4e5_add_pg_trgm_for_fuzzy_link_resolution.py +239 -0
- basic_memory/alembic/versions/g9a0b3c4d5e6_add_external_id_to_project_and_entity.py +173 -0
- basic_memory/api/app.py +87 -20
- basic_memory/api/container.py +133 -0
- basic_memory/api/routers/__init__.py +4 -1
- basic_memory/api/routers/directory_router.py +84 -0
- basic_memory/api/routers/importer_router.py +152 -0
- basic_memory/api/routers/knowledge_router.py +180 -23
- basic_memory/api/routers/management_router.py +80 -0
- basic_memory/api/routers/memory_router.py +9 -64
- basic_memory/api/routers/project_router.py +460 -0
- basic_memory/api/routers/prompt_router.py +260 -0
- basic_memory/api/routers/resource_router.py +136 -11
- basic_memory/api/routers/search_router.py +5 -5
- basic_memory/api/routers/utils.py +169 -0
- basic_memory/api/template_loader.py +292 -0
- basic_memory/api/v2/__init__.py +35 -0
- basic_memory/api/v2/routers/__init__.py +21 -0
- basic_memory/api/v2/routers/directory_router.py +93 -0
- basic_memory/api/v2/routers/importer_router.py +181 -0
- basic_memory/api/v2/routers/knowledge_router.py +427 -0
- basic_memory/api/v2/routers/memory_router.py +130 -0
- basic_memory/api/v2/routers/project_router.py +359 -0
- basic_memory/api/v2/routers/prompt_router.py +269 -0
- basic_memory/api/v2/routers/resource_router.py +286 -0
- basic_memory/api/v2/routers/search_router.py +73 -0
- basic_memory/cli/app.py +80 -10
- basic_memory/cli/auth.py +300 -0
- basic_memory/cli/commands/__init__.py +15 -2
- basic_memory/cli/commands/cloud/__init__.py +6 -0
- basic_memory/cli/commands/cloud/api_client.py +127 -0
- basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
- basic_memory/cli/commands/cloud/cloud_utils.py +108 -0
- basic_memory/cli/commands/cloud/core_commands.py +195 -0
- basic_memory/cli/commands/cloud/rclone_commands.py +397 -0
- basic_memory/cli/commands/cloud/rclone_config.py +110 -0
- basic_memory/cli/commands/cloud/rclone_installer.py +263 -0
- basic_memory/cli/commands/cloud/upload.py +240 -0
- basic_memory/cli/commands/cloud/upload_command.py +124 -0
- basic_memory/cli/commands/command_utils.py +99 -0
- basic_memory/cli/commands/db.py +87 -12
- basic_memory/cli/commands/format.py +198 -0
- basic_memory/cli/commands/import_chatgpt.py +47 -223
- basic_memory/cli/commands/import_claude_conversations.py +48 -171
- basic_memory/cli/commands/import_claude_projects.py +53 -160
- basic_memory/cli/commands/import_memory_json.py +55 -111
- basic_memory/cli/commands/mcp.py +67 -11
- basic_memory/cli/commands/project.py +889 -0
- basic_memory/cli/commands/status.py +52 -34
- basic_memory/cli/commands/telemetry.py +81 -0
- basic_memory/cli/commands/tool.py +341 -0
- basic_memory/cli/container.py +84 -0
- basic_memory/cli/main.py +14 -6
- basic_memory/config.py +580 -26
- basic_memory/db.py +285 -28
- basic_memory/deps/__init__.py +293 -0
- basic_memory/deps/config.py +26 -0
- basic_memory/deps/db.py +56 -0
- basic_memory/deps/importers.py +200 -0
- basic_memory/deps/projects.py +238 -0
- basic_memory/deps/repositories.py +179 -0
- basic_memory/deps/services.py +480 -0
- basic_memory/deps.py +16 -185
- basic_memory/file_utils.py +318 -54
- basic_memory/ignore_utils.py +297 -0
- basic_memory/importers/__init__.py +27 -0
- basic_memory/importers/base.py +100 -0
- basic_memory/importers/chatgpt_importer.py +245 -0
- basic_memory/importers/claude_conversations_importer.py +192 -0
- basic_memory/importers/claude_projects_importer.py +184 -0
- basic_memory/importers/memory_json_importer.py +128 -0
- basic_memory/importers/utils.py +61 -0
- basic_memory/markdown/entity_parser.py +182 -23
- basic_memory/markdown/markdown_processor.py +70 -7
- basic_memory/markdown/plugins.py +43 -23
- basic_memory/markdown/schemas.py +1 -1
- basic_memory/markdown/utils.py +38 -14
- basic_memory/mcp/async_client.py +135 -4
- basic_memory/mcp/clients/__init__.py +28 -0
- basic_memory/mcp/clients/directory.py +70 -0
- basic_memory/mcp/clients/knowledge.py +176 -0
- basic_memory/mcp/clients/memory.py +120 -0
- basic_memory/mcp/clients/project.py +89 -0
- basic_memory/mcp/clients/resource.py +71 -0
- basic_memory/mcp/clients/search.py +65 -0
- basic_memory/mcp/container.py +110 -0
- basic_memory/mcp/project_context.py +155 -0
- basic_memory/mcp/prompts/__init__.py +19 -0
- basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
- basic_memory/mcp/prompts/continue_conversation.py +62 -0
- basic_memory/mcp/prompts/recent_activity.py +188 -0
- basic_memory/mcp/prompts/search.py +57 -0
- basic_memory/mcp/prompts/utils.py +162 -0
- basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
- basic_memory/mcp/resources/project_info.py +71 -0
- basic_memory/mcp/server.py +61 -9
- basic_memory/mcp/tools/__init__.py +33 -21
- basic_memory/mcp/tools/build_context.py +120 -0
- basic_memory/mcp/tools/canvas.py +152 -0
- basic_memory/mcp/tools/chatgpt_tools.py +190 -0
- basic_memory/mcp/tools/delete_note.py +249 -0
- basic_memory/mcp/tools/edit_note.py +325 -0
- basic_memory/mcp/tools/list_directory.py +157 -0
- basic_memory/mcp/tools/move_note.py +549 -0
- basic_memory/mcp/tools/project_management.py +204 -0
- basic_memory/mcp/tools/read_content.py +281 -0
- basic_memory/mcp/tools/read_note.py +265 -0
- basic_memory/mcp/tools/recent_activity.py +528 -0
- basic_memory/mcp/tools/search.py +377 -24
- basic_memory/mcp/tools/utils.py +402 -16
- basic_memory/mcp/tools/view_note.py +78 -0
- basic_memory/mcp/tools/write_note.py +230 -0
- basic_memory/models/__init__.py +3 -2
- basic_memory/models/knowledge.py +82 -17
- basic_memory/models/project.py +93 -0
- basic_memory/models/search.py +68 -8
- basic_memory/project_resolver.py +222 -0
- basic_memory/repository/__init__.py +2 -0
- basic_memory/repository/entity_repository.py +437 -8
- basic_memory/repository/observation_repository.py +36 -3
- basic_memory/repository/postgres_search_repository.py +451 -0
- basic_memory/repository/project_info_repository.py +10 -0
- basic_memory/repository/project_repository.py +140 -0
- basic_memory/repository/relation_repository.py +79 -4
- basic_memory/repository/repository.py +148 -29
- basic_memory/repository/search_index_row.py +95 -0
- basic_memory/repository/search_repository.py +79 -268
- basic_memory/repository/search_repository_base.py +241 -0
- basic_memory/repository/sqlite_search_repository.py +437 -0
- basic_memory/runtime.py +61 -0
- basic_memory/schemas/__init__.py +22 -9
- basic_memory/schemas/base.py +131 -12
- basic_memory/schemas/cloud.py +50 -0
- basic_memory/schemas/directory.py +31 -0
- basic_memory/schemas/importer.py +35 -0
- basic_memory/schemas/memory.py +194 -25
- basic_memory/schemas/project_info.py +213 -0
- basic_memory/schemas/prompt.py +90 -0
- basic_memory/schemas/request.py +56 -2
- basic_memory/schemas/response.py +85 -28
- basic_memory/schemas/search.py +36 -35
- basic_memory/schemas/sync_report.py +72 -0
- basic_memory/schemas/v2/__init__.py +27 -0
- basic_memory/schemas/v2/entity.py +133 -0
- basic_memory/schemas/v2/resource.py +47 -0
- basic_memory/services/__init__.py +2 -1
- basic_memory/services/context_service.py +451 -138
- basic_memory/services/directory_service.py +310 -0
- basic_memory/services/entity_service.py +636 -71
- basic_memory/services/exceptions.py +21 -0
- basic_memory/services/file_service.py +402 -33
- basic_memory/services/initialization.py +216 -0
- basic_memory/services/link_resolver.py +50 -56
- basic_memory/services/project_service.py +888 -0
- basic_memory/services/search_service.py +232 -37
- basic_memory/sync/__init__.py +4 -2
- basic_memory/sync/background_sync.py +26 -0
- basic_memory/sync/coordinator.py +160 -0
- basic_memory/sync/sync_service.py +1200 -109
- basic_memory/sync/watch_service.py +432 -135
- basic_memory/telemetry.py +249 -0
- basic_memory/templates/prompts/continue_conversation.hbs +110 -0
- basic_memory/templates/prompts/search.hbs +101 -0
- basic_memory/utils.py +407 -54
- basic_memory-0.17.4.dist-info/METADATA +617 -0
- basic_memory-0.17.4.dist-info/RECORD +193 -0
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/WHEEL +1 -1
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/entry_points.txt +1 -0
- basic_memory/alembic/README +0 -1
- basic_memory/cli/commands/sync.py +0 -206
- basic_memory/cli/commands/tools.py +0 -157
- basic_memory/mcp/tools/knowledge.py +0 -68
- basic_memory/mcp/tools/memory.py +0 -170
- basic_memory/mcp/tools/notes.py +0 -202
- basic_memory/schemas/discovery.py +0 -28
- basic_memory/sync/file_change_scanner.py +0 -158
- basic_memory/sync/utils.py +0 -31
- basic_memory-0.7.0.dist-info/METADATA +0 -378
- basic_memory-0.7.0.dist-info/RECORD +0 -82
- {basic_memory-0.7.0.dist-info → basic_memory-0.17.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""Directory service for managing file directories and tree structure."""
|
|
2
|
+
|
|
3
|
+
import fnmatch
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Dict, List, Optional, Sequence
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
from basic_memory.models import Entity
|
|
11
|
+
from basic_memory.repository import EntityRepository
|
|
12
|
+
from basic_memory.schemas.directory import DirectoryNode
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _mtime_to_datetime(entity: Entity) -> datetime:
|
|
18
|
+
"""Convert entity mtime (file modification time) to datetime.
|
|
19
|
+
|
|
20
|
+
Returns the file's actual modification time, falling back to updated_at
|
|
21
|
+
if mtime is not available.
|
|
22
|
+
"""
|
|
23
|
+
if entity.mtime: # pragma: no cover
|
|
24
|
+
return datetime.fromtimestamp(entity.mtime).astimezone() # pragma: no cover
|
|
25
|
+
return entity.updated_at
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class DirectoryService:
|
|
29
|
+
"""Service for working with directory trees."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, entity_repository: EntityRepository):
|
|
32
|
+
"""Initialize the directory service.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
entity_repository: Directory repository for data access.
|
|
36
|
+
"""
|
|
37
|
+
self.entity_repository = entity_repository
|
|
38
|
+
|
|
39
|
+
async def get_directory_tree(self) -> DirectoryNode:
|
|
40
|
+
"""Build a hierarchical directory tree from indexed files."""
|
|
41
|
+
|
|
42
|
+
# Get all files from DB (flat list)
|
|
43
|
+
entity_rows = await self.entity_repository.find_all()
|
|
44
|
+
|
|
45
|
+
# Create a root directory node
|
|
46
|
+
root_node = DirectoryNode(name="Root", directory_path="/", type="directory")
|
|
47
|
+
|
|
48
|
+
# Map to store directory nodes by path for easy lookup
|
|
49
|
+
dir_map: Dict[str, DirectoryNode] = {root_node.directory_path: root_node}
|
|
50
|
+
|
|
51
|
+
# First pass: create all directory nodes
|
|
52
|
+
for file in entity_rows:
|
|
53
|
+
# Process directory path components
|
|
54
|
+
parts = [p for p in file.file_path.split("/") if p]
|
|
55
|
+
|
|
56
|
+
# Create directory structure
|
|
57
|
+
current_path = "/"
|
|
58
|
+
for i, part in enumerate(parts[:-1]): # Skip the filename
|
|
59
|
+
parent_path = current_path
|
|
60
|
+
# Build the directory path
|
|
61
|
+
current_path = (
|
|
62
|
+
f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Create directory node if it doesn't exist
|
|
66
|
+
if current_path not in dir_map:
|
|
67
|
+
dir_node = DirectoryNode(
|
|
68
|
+
name=part, directory_path=current_path, type="directory"
|
|
69
|
+
)
|
|
70
|
+
dir_map[current_path] = dir_node
|
|
71
|
+
|
|
72
|
+
# Add to parent's children
|
|
73
|
+
if parent_path in dir_map:
|
|
74
|
+
dir_map[parent_path].children.append(dir_node)
|
|
75
|
+
|
|
76
|
+
# Second pass: add file nodes to their parent directories
|
|
77
|
+
for file in entity_rows:
|
|
78
|
+
file_name = os.path.basename(file.file_path)
|
|
79
|
+
parent_dir = os.path.dirname(file.file_path)
|
|
80
|
+
directory_path = "/" if parent_dir == "" else f"/{parent_dir}"
|
|
81
|
+
|
|
82
|
+
# Create file node
|
|
83
|
+
file_node = DirectoryNode(
|
|
84
|
+
name=file_name,
|
|
85
|
+
file_path=file.file_path, # Original path from DB (no leading slash)
|
|
86
|
+
directory_path=f"/{file.file_path}", # Path with leading slash
|
|
87
|
+
type="file",
|
|
88
|
+
title=file.title,
|
|
89
|
+
permalink=file.permalink,
|
|
90
|
+
external_id=file.external_id, # UUID for v2 API
|
|
91
|
+
entity_id=file.id,
|
|
92
|
+
entity_type=file.entity_type,
|
|
93
|
+
content_type=file.content_type,
|
|
94
|
+
updated_at=_mtime_to_datetime(file),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Add to parent directory's children
|
|
98
|
+
if directory_path in dir_map:
|
|
99
|
+
dir_map[directory_path].children.append(file_node)
|
|
100
|
+
else:
|
|
101
|
+
# If parent directory doesn't exist (should be rare), add to root
|
|
102
|
+
dir_map["/"].children.append(file_node) # pragma: no cover
|
|
103
|
+
|
|
104
|
+
# Return the root node with its children
|
|
105
|
+
return root_node
|
|
106
|
+
|
|
107
|
+
async def get_directory_structure(self) -> DirectoryNode:
|
|
108
|
+
"""Build a hierarchical directory structure without file details.
|
|
109
|
+
|
|
110
|
+
Optimized method for folder navigation that only returns directory nodes,
|
|
111
|
+
no file metadata. Much faster than get_directory_tree() for large knowledge bases.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
DirectoryNode tree containing only folders (type="directory")
|
|
115
|
+
"""
|
|
116
|
+
# Get unique directories without loading entities
|
|
117
|
+
directories = await self.entity_repository.get_distinct_directories()
|
|
118
|
+
|
|
119
|
+
# Create a root directory node
|
|
120
|
+
root_node = DirectoryNode(name="Root", directory_path="/", type="directory")
|
|
121
|
+
|
|
122
|
+
# Map to store directory nodes by path for easy lookup
|
|
123
|
+
dir_map: Dict[str, DirectoryNode] = {"/": root_node}
|
|
124
|
+
|
|
125
|
+
# Build tree with just folders
|
|
126
|
+
for dir_path in directories:
|
|
127
|
+
parts = [p for p in dir_path.split("/") if p]
|
|
128
|
+
current_path = "/"
|
|
129
|
+
|
|
130
|
+
for i, part in enumerate(parts):
|
|
131
|
+
parent_path = current_path
|
|
132
|
+
# Build the directory path
|
|
133
|
+
current_path = (
|
|
134
|
+
f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Create directory node if it doesn't exist
|
|
138
|
+
if current_path not in dir_map:
|
|
139
|
+
dir_node = DirectoryNode(
|
|
140
|
+
name=part, directory_path=current_path, type="directory"
|
|
141
|
+
)
|
|
142
|
+
dir_map[current_path] = dir_node
|
|
143
|
+
|
|
144
|
+
# Add to parent's children
|
|
145
|
+
if parent_path in dir_map:
|
|
146
|
+
dir_map[parent_path].children.append(dir_node)
|
|
147
|
+
|
|
148
|
+
return root_node
|
|
149
|
+
|
|
150
|
+
async def list_directory(
|
|
151
|
+
self,
|
|
152
|
+
dir_name: str = "/",
|
|
153
|
+
depth: int = 1,
|
|
154
|
+
file_name_glob: Optional[str] = None,
|
|
155
|
+
) -> List[DirectoryNode]:
|
|
156
|
+
"""List directory contents with filtering and depth control.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
dir_name: Directory path to list (default: root "/")
|
|
160
|
+
depth: Recursion depth (1 = immediate children only)
|
|
161
|
+
file_name_glob: Glob pattern for filtering file names
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
List of DirectoryNode objects matching the criteria
|
|
165
|
+
"""
|
|
166
|
+
# Normalize directory path
|
|
167
|
+
# Strip ./ prefix if present (handles relative path notation)
|
|
168
|
+
if dir_name.startswith("./"):
|
|
169
|
+
dir_name = dir_name[2:] # Remove "./" prefix
|
|
170
|
+
|
|
171
|
+
# Ensure path starts with "/"
|
|
172
|
+
if not dir_name.startswith("/"):
|
|
173
|
+
dir_name = f"/{dir_name}"
|
|
174
|
+
|
|
175
|
+
# Remove trailing slashes except for root
|
|
176
|
+
if dir_name != "/" and dir_name.endswith("/"):
|
|
177
|
+
dir_name = dir_name.rstrip("/")
|
|
178
|
+
|
|
179
|
+
# Optimize: Query only entities in the target directory
|
|
180
|
+
# instead of loading the entire tree
|
|
181
|
+
dir_prefix = dir_name.lstrip("/")
|
|
182
|
+
entity_rows = await self.entity_repository.find_by_directory_prefix(dir_prefix)
|
|
183
|
+
|
|
184
|
+
# Build a partial tree from only the relevant entities
|
|
185
|
+
root_tree = self._build_directory_tree_from_entities(entity_rows, dir_name)
|
|
186
|
+
|
|
187
|
+
# Find the target directory node
|
|
188
|
+
target_node = self._find_directory_node(root_tree, dir_name)
|
|
189
|
+
if not target_node:
|
|
190
|
+
return [] # pragma: no cover
|
|
191
|
+
|
|
192
|
+
# Collect nodes with depth and glob filtering
|
|
193
|
+
result = []
|
|
194
|
+
self._collect_nodes_recursive(target_node, result, depth, file_name_glob, 0)
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
def _build_directory_tree_from_entities(
|
|
199
|
+
self, entity_rows: Sequence[Entity], root_path: str
|
|
200
|
+
) -> DirectoryNode:
|
|
201
|
+
"""Build a directory tree from a subset of entities.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
entity_rows: Sequence of entity objects to build tree from
|
|
205
|
+
root_path: Root directory path for the tree
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
DirectoryNode representing the tree root
|
|
209
|
+
"""
|
|
210
|
+
# Create a root directory node
|
|
211
|
+
root_node = DirectoryNode(name="Root", directory_path=root_path, type="directory")
|
|
212
|
+
|
|
213
|
+
# Map to store directory nodes by path for easy lookup
|
|
214
|
+
dir_map: Dict[str, DirectoryNode] = {root_path: root_node}
|
|
215
|
+
|
|
216
|
+
# First pass: create all directory nodes
|
|
217
|
+
for file in entity_rows:
|
|
218
|
+
# Process directory path components
|
|
219
|
+
parts = [p for p in file.file_path.split("/") if p]
|
|
220
|
+
|
|
221
|
+
# Create directory structure
|
|
222
|
+
current_path = "/"
|
|
223
|
+
for i, part in enumerate(parts[:-1]): # Skip the filename
|
|
224
|
+
parent_path = current_path
|
|
225
|
+
# Build the directory path
|
|
226
|
+
current_path = (
|
|
227
|
+
f"{current_path}{part}" if current_path == "/" else f"{current_path}/{part}"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# Create directory node if it doesn't exist
|
|
231
|
+
if current_path not in dir_map:
|
|
232
|
+
dir_node = DirectoryNode(
|
|
233
|
+
name=part, directory_path=current_path, type="directory"
|
|
234
|
+
)
|
|
235
|
+
dir_map[current_path] = dir_node
|
|
236
|
+
|
|
237
|
+
# Add to parent's children
|
|
238
|
+
if parent_path in dir_map:
|
|
239
|
+
dir_map[parent_path].children.append(dir_node)
|
|
240
|
+
|
|
241
|
+
# Second pass: add file nodes to their parent directories
|
|
242
|
+
for file in entity_rows:
|
|
243
|
+
file_name = os.path.basename(file.file_path)
|
|
244
|
+
parent_dir = os.path.dirname(file.file_path)
|
|
245
|
+
directory_path = "/" if parent_dir == "" else f"/{parent_dir}"
|
|
246
|
+
|
|
247
|
+
# Create file node
|
|
248
|
+
file_node = DirectoryNode(
|
|
249
|
+
name=file_name,
|
|
250
|
+
file_path=file.file_path,
|
|
251
|
+
directory_path=f"/{file.file_path}",
|
|
252
|
+
type="file",
|
|
253
|
+
title=file.title,
|
|
254
|
+
permalink=file.permalink,
|
|
255
|
+
external_id=file.external_id, # UUID for v2 API
|
|
256
|
+
entity_id=file.id,
|
|
257
|
+
entity_type=file.entity_type,
|
|
258
|
+
content_type=file.content_type,
|
|
259
|
+
updated_at=_mtime_to_datetime(file),
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Add to parent directory's children
|
|
263
|
+
if directory_path in dir_map:
|
|
264
|
+
dir_map[directory_path].children.append(file_node)
|
|
265
|
+
elif root_path in dir_map: # pragma: no cover
|
|
266
|
+
# Fallback to root if parent not found
|
|
267
|
+
dir_map[root_path].children.append(file_node) # pragma: no cover
|
|
268
|
+
|
|
269
|
+
return root_node
|
|
270
|
+
|
|
271
|
+
def _find_directory_node(
|
|
272
|
+
self, root: DirectoryNode, target_path: str
|
|
273
|
+
) -> Optional[DirectoryNode]:
|
|
274
|
+
"""Find a directory node by path in the tree."""
|
|
275
|
+
if root.directory_path == target_path:
|
|
276
|
+
return root
|
|
277
|
+
|
|
278
|
+
for child in root.children: # pragma: no cover
|
|
279
|
+
if child.type == "directory": # pragma: no cover
|
|
280
|
+
found = self._find_directory_node(child, target_path) # pragma: no cover
|
|
281
|
+
if found: # pragma: no cover
|
|
282
|
+
return found # pragma: no cover
|
|
283
|
+
|
|
284
|
+
return None # pragma: no cover
|
|
285
|
+
|
|
286
|
+
def _collect_nodes_recursive(
|
|
287
|
+
self,
|
|
288
|
+
node: DirectoryNode,
|
|
289
|
+
result: List[DirectoryNode],
|
|
290
|
+
max_depth: int,
|
|
291
|
+
file_name_glob: Optional[str],
|
|
292
|
+
current_depth: int,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Recursively collect nodes with depth and glob filtering."""
|
|
295
|
+
if current_depth >= max_depth:
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
for child in node.children:
|
|
299
|
+
# Apply glob filtering
|
|
300
|
+
if file_name_glob and not fnmatch.fnmatch(child.name, file_name_glob):
|
|
301
|
+
continue
|
|
302
|
+
|
|
303
|
+
# Add the child to results
|
|
304
|
+
result.append(child)
|
|
305
|
+
|
|
306
|
+
# Recurse into subdirectories if we haven't reached max depth
|
|
307
|
+
if child.type == "directory" and current_depth < max_depth:
|
|
308
|
+
self._collect_nodes_recursive(
|
|
309
|
+
child, result, max_depth, file_name_glob, current_depth + 1
|
|
310
|
+
)
|