basic-memory 0.12.3__py3-none-any.whl → 0.13.0b2__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.

Files changed (107) hide show
  1. basic_memory/__init__.py +7 -1
  2. basic_memory/alembic/env.py +1 -1
  3. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  4. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +0 -5
  5. basic_memory/api/app.py +43 -13
  6. basic_memory/api/routers/__init__.py +4 -2
  7. basic_memory/api/routers/directory_router.py +63 -0
  8. basic_memory/api/routers/importer_router.py +152 -0
  9. basic_memory/api/routers/knowledge_router.py +127 -38
  10. basic_memory/api/routers/management_router.py +78 -0
  11. basic_memory/api/routers/memory_router.py +4 -59
  12. basic_memory/api/routers/project_router.py +230 -0
  13. basic_memory/api/routers/prompt_router.py +260 -0
  14. basic_memory/api/routers/search_router.py +3 -21
  15. basic_memory/api/routers/utils.py +130 -0
  16. basic_memory/api/template_loader.py +292 -0
  17. basic_memory/cli/app.py +20 -21
  18. basic_memory/cli/commands/__init__.py +2 -1
  19. basic_memory/cli/commands/auth.py +136 -0
  20. basic_memory/cli/commands/db.py +3 -3
  21. basic_memory/cli/commands/import_chatgpt.py +31 -207
  22. basic_memory/cli/commands/import_claude_conversations.py +16 -142
  23. basic_memory/cli/commands/import_claude_projects.py +33 -143
  24. basic_memory/cli/commands/import_memory_json.py +26 -83
  25. basic_memory/cli/commands/mcp.py +71 -18
  26. basic_memory/cli/commands/project.py +99 -67
  27. basic_memory/cli/commands/status.py +19 -9
  28. basic_memory/cli/commands/sync.py +44 -58
  29. basic_memory/cli/main.py +1 -5
  30. basic_memory/config.py +144 -88
  31. basic_memory/db.py +6 -4
  32. basic_memory/deps.py +227 -30
  33. basic_memory/importers/__init__.py +27 -0
  34. basic_memory/importers/base.py +79 -0
  35. basic_memory/importers/chatgpt_importer.py +222 -0
  36. basic_memory/importers/claude_conversations_importer.py +172 -0
  37. basic_memory/importers/claude_projects_importer.py +148 -0
  38. basic_memory/importers/memory_json_importer.py +93 -0
  39. basic_memory/importers/utils.py +58 -0
  40. basic_memory/markdown/entity_parser.py +5 -2
  41. basic_memory/mcp/auth_provider.py +270 -0
  42. basic_memory/mcp/external_auth_provider.py +321 -0
  43. basic_memory/mcp/project_session.py +103 -0
  44. basic_memory/mcp/prompts/continue_conversation.py +18 -68
  45. basic_memory/mcp/prompts/recent_activity.py +19 -3
  46. basic_memory/mcp/prompts/search.py +14 -140
  47. basic_memory/mcp/prompts/utils.py +3 -3
  48. basic_memory/mcp/{tools → resources}/project_info.py +6 -2
  49. basic_memory/mcp/server.py +82 -8
  50. basic_memory/mcp/supabase_auth_provider.py +463 -0
  51. basic_memory/mcp/tools/__init__.py +20 -0
  52. basic_memory/mcp/tools/build_context.py +11 -1
  53. basic_memory/mcp/tools/canvas.py +15 -2
  54. basic_memory/mcp/tools/delete_note.py +12 -4
  55. basic_memory/mcp/tools/edit_note.py +297 -0
  56. basic_memory/mcp/tools/list_directory.py +154 -0
  57. basic_memory/mcp/tools/move_note.py +87 -0
  58. basic_memory/mcp/tools/project_management.py +300 -0
  59. basic_memory/mcp/tools/read_content.py +15 -6
  60. basic_memory/mcp/tools/read_note.py +17 -5
  61. basic_memory/mcp/tools/recent_activity.py +11 -2
  62. basic_memory/mcp/tools/search.py +10 -1
  63. basic_memory/mcp/tools/utils.py +137 -12
  64. basic_memory/mcp/tools/write_note.py +11 -15
  65. basic_memory/models/__init__.py +3 -2
  66. basic_memory/models/knowledge.py +16 -4
  67. basic_memory/models/project.py +80 -0
  68. basic_memory/models/search.py +8 -5
  69. basic_memory/repository/__init__.py +2 -0
  70. basic_memory/repository/entity_repository.py +8 -3
  71. basic_memory/repository/observation_repository.py +35 -3
  72. basic_memory/repository/project_info_repository.py +3 -2
  73. basic_memory/repository/project_repository.py +85 -0
  74. basic_memory/repository/relation_repository.py +8 -2
  75. basic_memory/repository/repository.py +107 -15
  76. basic_memory/repository/search_repository.py +87 -27
  77. basic_memory/schemas/__init__.py +6 -0
  78. basic_memory/schemas/directory.py +30 -0
  79. basic_memory/schemas/importer.py +34 -0
  80. basic_memory/schemas/memory.py +26 -12
  81. basic_memory/schemas/project_info.py +112 -2
  82. basic_memory/schemas/prompt.py +90 -0
  83. basic_memory/schemas/request.py +56 -2
  84. basic_memory/schemas/search.py +1 -1
  85. basic_memory/services/__init__.py +2 -1
  86. basic_memory/services/context_service.py +208 -95
  87. basic_memory/services/directory_service.py +167 -0
  88. basic_memory/services/entity_service.py +385 -5
  89. basic_memory/services/exceptions.py +6 -0
  90. basic_memory/services/file_service.py +14 -15
  91. basic_memory/services/initialization.py +144 -67
  92. basic_memory/services/link_resolver.py +16 -8
  93. basic_memory/services/project_service.py +548 -0
  94. basic_memory/services/search_service.py +77 -2
  95. basic_memory/sync/background_sync.py +25 -0
  96. basic_memory/sync/sync_service.py +10 -9
  97. basic_memory/sync/watch_service.py +63 -39
  98. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  99. basic_memory/templates/prompts/search.hbs +101 -0
  100. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/METADATA +23 -1
  101. basic_memory-0.13.0b2.dist-info/RECORD +132 -0
  102. basic_memory/api/routers/project_info_router.py +0 -274
  103. basic_memory/mcp/main.py +0 -24
  104. basic_memory-0.12.3.dist-info/RECORD +0 -100
  105. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/WHEEL +0 -0
  106. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/entry_points.txt +0 -0
  107. {basic_memory-0.12.3.dist-info → basic_memory-0.13.0b2.dist-info}/licenses/LICENSE +0 -0
basic_memory/__init__.py CHANGED
@@ -1,3 +1,9 @@
1
1
  """basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
2
2
 
3
- __version__ = "0.12.3"
3
+ try:
4
+ from importlib.metadata import version
5
+
6
+ __version__ = version("basic-memory")
7
+ except Exception: # pragma: no cover
8
+ # Fallback if package not installed (e.g., during development)
9
+ __version__ = "0.0.0" # pragma: no cover
@@ -13,7 +13,7 @@ from basic_memory.models import Base
13
13
  # set config.env to "test" for pytest to prevent logging to file in utils.setup_logging()
14
14
  os.environ["BASIC_MEMORY_ENV"] = "test"
15
15
 
16
- from basic_memory.config import config as app_config
16
+ from basic_memory.config import app_config
17
17
 
18
18
  # this is the Alembic Config object, which provides
19
19
  # access to the values within the .ini file in use.
@@ -0,0 +1,108 @@
1
+ """add projects table
2
+
3
+ Revision ID: 5fe1ab1ccebe
4
+ Revises: cc7172b46608
5
+ Create Date: 2025-05-14 09:05:18.214357
6
+
7
+ """
8
+
9
+ from typing import Sequence, Union
10
+
11
+ from alembic import op
12
+ import sqlalchemy as sa
13
+
14
+
15
+ # revision identifiers, used by Alembic.
16
+ revision: str = "5fe1ab1ccebe"
17
+ down_revision: Union[str, None] = "cc7172b46608"
18
+ branch_labels: Union[str, Sequence[str], None] = None
19
+ depends_on: Union[str, Sequence[str], None] = None
20
+
21
+
22
+ def upgrade() -> None:
23
+ # ### commands auto generated by Alembic - please adjust! ###
24
+ op.create_table(
25
+ "project",
26
+ sa.Column("id", sa.Integer(), nullable=False),
27
+ sa.Column("name", sa.String(), nullable=False),
28
+ sa.Column("description", sa.Text(), nullable=True),
29
+ sa.Column("permalink", sa.String(), nullable=False),
30
+ sa.Column("path", sa.String(), nullable=False),
31
+ sa.Column("is_active", sa.Boolean(), nullable=False),
32
+ sa.Column("is_default", sa.Boolean(), nullable=True),
33
+ sa.Column("created_at", sa.DateTime(), nullable=False),
34
+ sa.Column("updated_at", sa.DateTime(), nullable=False),
35
+ sa.PrimaryKeyConstraint("id"),
36
+ sa.UniqueConstraint("is_default"),
37
+ sa.UniqueConstraint("name"),
38
+ sa.UniqueConstraint("permalink"),
39
+ if_not_exists=True,
40
+ )
41
+ with op.batch_alter_table("project", schema=None) as batch_op:
42
+ batch_op.create_index(
43
+ "ix_project_created_at", ["created_at"], unique=False, if_not_exists=True
44
+ )
45
+ batch_op.create_index("ix_project_name", ["name"], unique=True, if_not_exists=True)
46
+ batch_op.create_index("ix_project_path", ["path"], unique=False, if_not_exists=True)
47
+ batch_op.create_index(
48
+ "ix_project_permalink", ["permalink"], unique=True, if_not_exists=True
49
+ )
50
+ batch_op.create_index(
51
+ "ix_project_updated_at", ["updated_at"], unique=False, if_not_exists=True
52
+ )
53
+
54
+ with op.batch_alter_table("entity", schema=None) as batch_op:
55
+ batch_op.add_column(sa.Column("project_id", sa.Integer(), nullable=False))
56
+ batch_op.drop_index(
57
+ "uix_entity_permalink",
58
+ sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
59
+ )
60
+ batch_op.drop_index("ix_entity_file_path")
61
+ batch_op.create_index(batch_op.f("ix_entity_file_path"), ["file_path"], unique=False)
62
+ batch_op.create_index("ix_entity_project_id", ["project_id"], unique=False)
63
+ batch_op.create_index(
64
+ "uix_entity_file_path_project", ["file_path", "project_id"], unique=True
65
+ )
66
+ batch_op.create_index(
67
+ "uix_entity_permalink_project",
68
+ ["permalink", "project_id"],
69
+ unique=True,
70
+ sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
71
+ )
72
+ batch_op.create_foreign_key("fk_entity_project_id", "project", ["project_id"], ["id"])
73
+
74
+ # drop the search index table. it will be recreated
75
+ op.drop_table("search_index")
76
+
77
+ # ### end Alembic commands ###
78
+
79
+
80
+ def downgrade() -> None:
81
+ # ### commands auto generated by Alembic - please adjust! ###
82
+ with op.batch_alter_table("entity", schema=None) as batch_op:
83
+ batch_op.drop_constraint("fk_entity_project_id", type_="foreignkey")
84
+ batch_op.drop_index(
85
+ "uix_entity_permalink_project",
86
+ sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
87
+ )
88
+ batch_op.drop_index("uix_entity_file_path_project")
89
+ batch_op.drop_index("ix_entity_project_id")
90
+ batch_op.drop_index(batch_op.f("ix_entity_file_path"))
91
+ batch_op.create_index("ix_entity_file_path", ["file_path"], unique=1)
92
+ batch_op.create_index(
93
+ "uix_entity_permalink",
94
+ ["permalink"],
95
+ unique=1,
96
+ sqlite_where=sa.text("content_type = 'text/markdown' AND permalink IS NOT NULL"),
97
+ )
98
+ batch_op.drop_column("project_id")
99
+
100
+ with op.batch_alter_table("project", schema=None) as batch_op:
101
+ batch_op.drop_index("ix_project_updated_at")
102
+ batch_op.drop_index("ix_project_permalink")
103
+ batch_op.drop_index("ix_project_path")
104
+ batch_op.drop_index("ix_project_name")
105
+ batch_op.drop_index("ix_project_created_at")
106
+
107
+ op.drop_table("project")
108
+ # ### end Alembic commands ###
@@ -56,11 +56,6 @@ def upgrade() -> None:
56
56
  );
57
57
  """)
58
58
 
59
- # Print instruction to manually reindex after migration
60
- print("\n------------------------------------------------------------------")
61
- print("IMPORTANT: After migration completes, manually run the reindex command:")
62
- print("basic-memory sync")
63
- print("------------------------------------------------------------------\n")
64
59
 
65
60
 
66
61
  def downgrade() -> None:
basic_memory/api/app.py CHANGED
@@ -1,29 +1,50 @@
1
1
  """FastAPI application for basic-memory knowledge graph API."""
2
2
 
3
+ import asyncio
3
4
  from contextlib import asynccontextmanager
4
5
 
5
6
  from fastapi import FastAPI, HTTPException
6
7
  from fastapi.exception_handlers import http_exception_handler
7
8
  from loguru import logger
8
9
 
10
+ from basic_memory import __version__ as version
9
11
  from basic_memory import db
10
- from basic_memory.api.routers import knowledge, memory, project_info, resource, search
11
- from basic_memory.config import config as project_config
12
- from basic_memory.services.initialization import initialize_app
12
+ from basic_memory.api.routers import (
13
+ directory_router,
14
+ importer_router,
15
+ knowledge,
16
+ management,
17
+ memory,
18
+ project,
19
+ resource,
20
+ search,
21
+ prompt_router,
22
+ )
23
+ from basic_memory.config import app_config
24
+ from basic_memory.services.initialization import initialize_app, initialize_file_sync
13
25
 
14
26
 
15
27
  @asynccontextmanager
16
28
  async def lifespan(app: FastAPI): # pragma: no cover
17
29
  """Lifecycle manager for the FastAPI app."""
18
- # Initialize database and file sync services
19
- watch_task = await initialize_app(project_config)
30
+ # Initialize app and database
31
+ logger.info("Starting Basic Memory API")
32
+ await initialize_app(app_config)
33
+
34
+ logger.info(f"Sync changes enabled: {app_config.sync_changes}")
35
+ if app_config.sync_changes:
36
+ # start file sync task in background
37
+ app.state.sync_task = asyncio.create_task(initialize_file_sync(app_config))
38
+ else:
39
+ logger.info("Sync changes disabled. Skipping file sync service.")
20
40
 
21
41
  # proceed with startup
22
42
  yield
23
43
 
24
44
  logger.info("Shutting down Basic Memory API")
25
- if watch_task:
26
- watch_task.cancel()
45
+ if app.state.sync_task:
46
+ logger.info("Stopping sync...")
47
+ app.state.sync_task.cancel() # pyright: ignore
27
48
 
28
49
  await db.shutdown_db()
29
50
 
@@ -32,17 +53,26 @@ async def lifespan(app: FastAPI): # pragma: no cover
32
53
  app = FastAPI(
33
54
  title="Basic Memory API",
34
55
  description="Knowledge graph API for basic-memory",
35
- version="0.1.0",
56
+ version=version,
36
57
  lifespan=lifespan,
37
58
  )
38
59
 
39
60
 
40
61
  # Include routers
41
- app.include_router(knowledge.router)
42
- app.include_router(search.router)
43
- app.include_router(memory.router)
44
- app.include_router(resource.router)
45
- app.include_router(project_info.router)
62
+ app.include_router(knowledge.router, prefix="/{project}")
63
+ app.include_router(memory.router, prefix="/{project}")
64
+ app.include_router(resource.router, prefix="/{project}")
65
+ app.include_router(search.router, prefix="/{project}")
66
+ app.include_router(project.project_router, prefix="/{project}")
67
+ app.include_router(directory_router.router, prefix="/{project}")
68
+ app.include_router(prompt_router.router, prefix="/{project}")
69
+ app.include_router(importer_router.router, prefix="/{project}")
70
+
71
+ # Project resource router works accross projects
72
+ app.include_router(project.project_resource_router)
73
+ app.include_router(management.router)
74
+
75
+ # Auth routes are handled by FastMCP automatically when auth is enabled
46
76
 
47
77
 
48
78
  @app.exception_handler(Exception)
@@ -1,9 +1,11 @@
1
1
  """API routers."""
2
2
 
3
3
  from . import knowledge_router as knowledge
4
+ from . import management_router as management
4
5
  from . import memory_router as memory
6
+ from . import project_router as project
5
7
  from . import resource_router as resource
6
8
  from . import search_router as search
7
- from . import project_info_router as project_info
9
+ from . import prompt_router as prompt
8
10
 
9
- __all__ = ["knowledge", "memory", "resource", "search", "project_info"]
11
+ __all__ = ["knowledge", "management", "memory", "project", "resource", "search", "prompt"]
@@ -0,0 +1,63 @@
1
+ """Router for directory tree operations."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from fastapi import APIRouter, Query
6
+
7
+ from basic_memory.deps import DirectoryServiceDep, ProjectIdDep
8
+ from basic_memory.schemas.directory import DirectoryNode
9
+
10
+ router = APIRouter(prefix="/directory", tags=["directory"])
11
+
12
+
13
+ @router.get("/tree", response_model=DirectoryNode)
14
+ async def get_directory_tree(
15
+ directory_service: DirectoryServiceDep,
16
+ project_id: ProjectIdDep,
17
+ ):
18
+ """Get hierarchical directory structure from the knowledge base.
19
+
20
+ Args:
21
+ directory_service: Service for directory operations
22
+ project_id: ID of the current project
23
+
24
+ Returns:
25
+ DirectoryNode representing the root of the hierarchical tree structure
26
+ """
27
+ # Get a hierarchical directory tree for the specific project
28
+ tree = await directory_service.get_directory_tree()
29
+
30
+ # Return the hierarchical tree
31
+ return tree
32
+
33
+
34
+ @router.get("/list", response_model=List[DirectoryNode])
35
+ async def list_directory(
36
+ directory_service: DirectoryServiceDep,
37
+ project_id: ProjectIdDep,
38
+ dir_name: str = Query("/", description="Directory path to list"),
39
+ depth: int = Query(1, ge=1, le=10, description="Recursion depth (1-10)"),
40
+ file_name_glob: Optional[str] = Query(
41
+ None, description="Glob pattern for filtering file names"
42
+ ),
43
+ ):
44
+ """List directory contents with filtering and depth control.
45
+
46
+ Args:
47
+ directory_service: Service for directory operations
48
+ project_id: ID of the current project
49
+ dir_name: Directory path to list (default: root "/")
50
+ depth: Recursion depth (1-10, default: 1 for immediate children only)
51
+ file_name_glob: Optional glob pattern for filtering file names (e.g., "*.md", "*meeting*")
52
+
53
+ Returns:
54
+ List of DirectoryNode objects matching the criteria
55
+ """
56
+ # Get directory listing with filtering
57
+ nodes = await directory_service.list_directory(
58
+ dir_name=dir_name,
59
+ depth=depth,
60
+ file_name_glob=file_name_glob,
61
+ )
62
+
63
+ return nodes
@@ -0,0 +1,152 @@
1
+ """Import router for Basic Memory API."""
2
+
3
+ import json
4
+ import logging
5
+
6
+ from fastapi import APIRouter, Form, HTTPException, UploadFile, status
7
+
8
+ from basic_memory.deps import (
9
+ ChatGPTImporterDep,
10
+ ClaudeConversationsImporterDep,
11
+ ClaudeProjectsImporterDep,
12
+ MemoryJsonImporterDep,
13
+ )
14
+ from basic_memory.importers import Importer
15
+ from basic_memory.schemas.importer import (
16
+ ChatImportResult,
17
+ EntityImportResult,
18
+ ProjectImportResult,
19
+ )
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ router = APIRouter(prefix="/import", tags=["import"])
24
+
25
+
26
+ @router.post("/chatgpt", response_model=ChatImportResult)
27
+ async def import_chatgpt(
28
+ importer: ChatGPTImporterDep,
29
+ file: UploadFile,
30
+ folder: str = Form("conversations"),
31
+ ) -> ChatImportResult:
32
+ """Import conversations from ChatGPT JSON export.
33
+
34
+ Args:
35
+ file: The ChatGPT conversations.json file.
36
+ folder: The folder to place the files in.
37
+ markdown_processor: MarkdownProcessor instance.
38
+
39
+ Returns:
40
+ ChatImportResult with import statistics.
41
+
42
+ Raises:
43
+ HTTPException: If import fails.
44
+ """
45
+ return await import_file(importer, file, folder)
46
+
47
+
48
+ @router.post("/claude/conversations", response_model=ChatImportResult)
49
+ async def import_claude_conversations(
50
+ importer: ClaudeConversationsImporterDep,
51
+ file: UploadFile,
52
+ folder: str = Form("conversations"),
53
+ ) -> ChatImportResult:
54
+ """Import conversations from Claude conversations.json export.
55
+
56
+ Args:
57
+ file: The Claude conversations.json file.
58
+ folder: The folder to place the files in.
59
+ markdown_processor: MarkdownProcessor instance.
60
+
61
+ Returns:
62
+ ChatImportResult with import statistics.
63
+
64
+ Raises:
65
+ HTTPException: If import fails.
66
+ """
67
+ return await import_file(importer, file, folder)
68
+
69
+
70
+ @router.post("/claude/projects", response_model=ProjectImportResult)
71
+ async def import_claude_projects(
72
+ importer: ClaudeProjectsImporterDep,
73
+ file: UploadFile,
74
+ folder: str = Form("projects"),
75
+ ) -> ProjectImportResult:
76
+ """Import projects from Claude projects.json export.
77
+
78
+ Args:
79
+ file: The Claude projects.json file.
80
+ base_folder: The base folder to place the files in.
81
+ markdown_processor: MarkdownProcessor instance.
82
+
83
+ Returns:
84
+ ProjectImportResult with import statistics.
85
+
86
+ Raises:
87
+ HTTPException: If import fails.
88
+ """
89
+ return await import_file(importer, file, folder)
90
+
91
+
92
+ @router.post("/memory-json", response_model=EntityImportResult)
93
+ async def import_memory_json(
94
+ importer: MemoryJsonImporterDep,
95
+ file: UploadFile,
96
+ folder: str = Form("conversations"),
97
+ ) -> EntityImportResult:
98
+ """Import entities and relations from a memory.json file.
99
+
100
+ Args:
101
+ file: The memory.json file.
102
+ destination_folder: Optional destination folder within the project.
103
+ markdown_processor: MarkdownProcessor instance.
104
+
105
+ Returns:
106
+ EntityImportResult with import statistics.
107
+
108
+ Raises:
109
+ HTTPException: If import fails.
110
+ """
111
+ try:
112
+ file_data = []
113
+ file_bytes = await file.read()
114
+ file_str = file_bytes.decode("utf-8")
115
+ for line in file_str.splitlines():
116
+ json_data = json.loads(line)
117
+ file_data.append(json_data)
118
+
119
+ result = await importer.import_data(file_data, folder)
120
+ if not result.success: # pragma: no cover
121
+ raise HTTPException(
122
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
123
+ detail=result.error_message or "Import failed",
124
+ )
125
+ except Exception as e:
126
+ logger.exception("Import failed")
127
+ raise HTTPException(
128
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
129
+ detail=f"Import failed: {str(e)}",
130
+ )
131
+ return result
132
+
133
+
134
+ async def import_file(importer: Importer, file: UploadFile, destination_folder: str):
135
+ try:
136
+ # Process file
137
+ json_data = json.load(file.file)
138
+ result = await importer.import_data(json_data, destination_folder)
139
+ if not result.success: # pragma: no cover
140
+ raise HTTPException(
141
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
142
+ detail=result.error_message or "Import failed",
143
+ )
144
+
145
+ return result
146
+
147
+ except Exception as e:
148
+ logger.exception("Import failed")
149
+ raise HTTPException(
150
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
151
+ detail=f"Import failed: {str(e)}",
152
+ )