basic-memory 0.7.0__py3-none-any.whl → 0.16.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.

Potentially problematic release.


This version of basic-memory might be problematic. Click here for more details.

Files changed (150) hide show
  1. basic_memory/__init__.py +5 -1
  2. basic_memory/alembic/alembic.ini +119 -0
  3. basic_memory/alembic/env.py +27 -3
  4. basic_memory/alembic/migrations.py +4 -9
  5. basic_memory/alembic/versions/502b60eaa905_remove_required_from_entity_permalink.py +51 -0
  6. basic_memory/alembic/versions/5fe1ab1ccebe_add_projects_table.py +108 -0
  7. basic_memory/alembic/versions/647e7a75e2cd_project_constraint_fix.py +104 -0
  8. basic_memory/alembic/versions/9d9c1cb7d8f5_add_mtime_and_size_columns_to_entity_.py +49 -0
  9. basic_memory/alembic/versions/a1b2c3d4e5f6_fix_project_foreign_keys.py +49 -0
  10. basic_memory/alembic/versions/b3c3938bacdb_relation_to_name_unique_index.py +44 -0
  11. basic_memory/alembic/versions/cc7172b46608_update_search_index_schema.py +100 -0
  12. basic_memory/alembic/versions/e7e1f4367280_add_scan_watermark_tracking_to_project.py +37 -0
  13. basic_memory/api/app.py +64 -18
  14. basic_memory/api/routers/__init__.py +4 -1
  15. basic_memory/api/routers/directory_router.py +84 -0
  16. basic_memory/api/routers/importer_router.py +152 -0
  17. basic_memory/api/routers/knowledge_router.py +166 -21
  18. basic_memory/api/routers/management_router.py +80 -0
  19. basic_memory/api/routers/memory_router.py +9 -64
  20. basic_memory/api/routers/project_router.py +406 -0
  21. basic_memory/api/routers/prompt_router.py +260 -0
  22. basic_memory/api/routers/resource_router.py +119 -4
  23. basic_memory/api/routers/search_router.py +5 -5
  24. basic_memory/api/routers/utils.py +130 -0
  25. basic_memory/api/template_loader.py +292 -0
  26. basic_memory/cli/app.py +43 -9
  27. basic_memory/cli/auth.py +277 -0
  28. basic_memory/cli/commands/__init__.py +13 -2
  29. basic_memory/cli/commands/cloud/__init__.py +6 -0
  30. basic_memory/cli/commands/cloud/api_client.py +112 -0
  31. basic_memory/cli/commands/cloud/bisync_commands.py +110 -0
  32. basic_memory/cli/commands/cloud/cloud_utils.py +101 -0
  33. basic_memory/cli/commands/cloud/core_commands.py +195 -0
  34. basic_memory/cli/commands/cloud/rclone_commands.py +301 -0
  35. basic_memory/cli/commands/cloud/rclone_config.py +110 -0
  36. basic_memory/cli/commands/cloud/rclone_installer.py +249 -0
  37. basic_memory/cli/commands/cloud/upload.py +233 -0
  38. basic_memory/cli/commands/cloud/upload_command.py +124 -0
  39. basic_memory/cli/commands/command_utils.py +51 -0
  40. basic_memory/cli/commands/db.py +28 -12
  41. basic_memory/cli/commands/import_chatgpt.py +40 -220
  42. basic_memory/cli/commands/import_claude_conversations.py +41 -168
  43. basic_memory/cli/commands/import_claude_projects.py +46 -157
  44. basic_memory/cli/commands/import_memory_json.py +48 -108
  45. basic_memory/cli/commands/mcp.py +84 -10
  46. basic_memory/cli/commands/project.py +876 -0
  47. basic_memory/cli/commands/status.py +50 -33
  48. basic_memory/cli/commands/tool.py +341 -0
  49. basic_memory/cli/main.py +8 -7
  50. basic_memory/config.py +477 -23
  51. basic_memory/db.py +168 -17
  52. basic_memory/deps.py +251 -25
  53. basic_memory/file_utils.py +113 -58
  54. basic_memory/ignore_utils.py +297 -0
  55. basic_memory/importers/__init__.py +27 -0
  56. basic_memory/importers/base.py +79 -0
  57. basic_memory/importers/chatgpt_importer.py +232 -0
  58. basic_memory/importers/claude_conversations_importer.py +177 -0
  59. basic_memory/importers/claude_projects_importer.py +148 -0
  60. basic_memory/importers/memory_json_importer.py +108 -0
  61. basic_memory/importers/utils.py +58 -0
  62. basic_memory/markdown/entity_parser.py +143 -23
  63. basic_memory/markdown/markdown_processor.py +3 -3
  64. basic_memory/markdown/plugins.py +39 -21
  65. basic_memory/markdown/schemas.py +1 -1
  66. basic_memory/markdown/utils.py +28 -13
  67. basic_memory/mcp/async_client.py +134 -4
  68. basic_memory/mcp/project_context.py +141 -0
  69. basic_memory/mcp/prompts/__init__.py +19 -0
  70. basic_memory/mcp/prompts/ai_assistant_guide.py +70 -0
  71. basic_memory/mcp/prompts/continue_conversation.py +62 -0
  72. basic_memory/mcp/prompts/recent_activity.py +188 -0
  73. basic_memory/mcp/prompts/search.py +57 -0
  74. basic_memory/mcp/prompts/utils.py +162 -0
  75. basic_memory/mcp/resources/ai_assistant_guide.md +283 -0
  76. basic_memory/mcp/resources/project_info.py +71 -0
  77. basic_memory/mcp/server.py +7 -13
  78. basic_memory/mcp/tools/__init__.py +33 -21
  79. basic_memory/mcp/tools/build_context.py +120 -0
  80. basic_memory/mcp/tools/canvas.py +130 -0
  81. basic_memory/mcp/tools/chatgpt_tools.py +187 -0
  82. basic_memory/mcp/tools/delete_note.py +225 -0
  83. basic_memory/mcp/tools/edit_note.py +320 -0
  84. basic_memory/mcp/tools/list_directory.py +167 -0
  85. basic_memory/mcp/tools/move_note.py +545 -0
  86. basic_memory/mcp/tools/project_management.py +200 -0
  87. basic_memory/mcp/tools/read_content.py +271 -0
  88. basic_memory/mcp/tools/read_note.py +255 -0
  89. basic_memory/mcp/tools/recent_activity.py +534 -0
  90. basic_memory/mcp/tools/search.py +369 -23
  91. basic_memory/mcp/tools/utils.py +374 -16
  92. basic_memory/mcp/tools/view_note.py +77 -0
  93. basic_memory/mcp/tools/write_note.py +207 -0
  94. basic_memory/models/__init__.py +3 -2
  95. basic_memory/models/knowledge.py +67 -15
  96. basic_memory/models/project.py +87 -0
  97. basic_memory/models/search.py +10 -6
  98. basic_memory/repository/__init__.py +2 -0
  99. basic_memory/repository/entity_repository.py +229 -7
  100. basic_memory/repository/observation_repository.py +35 -3
  101. basic_memory/repository/project_info_repository.py +10 -0
  102. basic_memory/repository/project_repository.py +103 -0
  103. basic_memory/repository/relation_repository.py +21 -2
  104. basic_memory/repository/repository.py +147 -29
  105. basic_memory/repository/search_repository.py +411 -62
  106. basic_memory/schemas/__init__.py +22 -9
  107. basic_memory/schemas/base.py +97 -8
  108. basic_memory/schemas/cloud.py +50 -0
  109. basic_memory/schemas/directory.py +30 -0
  110. basic_memory/schemas/importer.py +35 -0
  111. basic_memory/schemas/memory.py +187 -25
  112. basic_memory/schemas/project_info.py +211 -0
  113. basic_memory/schemas/prompt.py +90 -0
  114. basic_memory/schemas/request.py +56 -2
  115. basic_memory/schemas/response.py +1 -1
  116. basic_memory/schemas/search.py +31 -35
  117. basic_memory/schemas/sync_report.py +72 -0
  118. basic_memory/services/__init__.py +2 -1
  119. basic_memory/services/context_service.py +241 -104
  120. basic_memory/services/directory_service.py +295 -0
  121. basic_memory/services/entity_service.py +590 -60
  122. basic_memory/services/exceptions.py +21 -0
  123. basic_memory/services/file_service.py +284 -30
  124. basic_memory/services/initialization.py +191 -0
  125. basic_memory/services/link_resolver.py +49 -56
  126. basic_memory/services/project_service.py +863 -0
  127. basic_memory/services/search_service.py +168 -32
  128. basic_memory/sync/__init__.py +3 -2
  129. basic_memory/sync/background_sync.py +26 -0
  130. basic_memory/sync/sync_service.py +1180 -109
  131. basic_memory/sync/watch_service.py +412 -135
  132. basic_memory/templates/prompts/continue_conversation.hbs +110 -0
  133. basic_memory/templates/prompts/search.hbs +101 -0
  134. basic_memory/utils.py +383 -51
  135. basic_memory-0.16.1.dist-info/METADATA +493 -0
  136. basic_memory-0.16.1.dist-info/RECORD +148 -0
  137. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/entry_points.txt +1 -0
  138. basic_memory/alembic/README +0 -1
  139. basic_memory/cli/commands/sync.py +0 -206
  140. basic_memory/cli/commands/tools.py +0 -157
  141. basic_memory/mcp/tools/knowledge.py +0 -68
  142. basic_memory/mcp/tools/memory.py +0 -170
  143. basic_memory/mcp/tools/notes.py +0 -202
  144. basic_memory/schemas/discovery.py +0 -28
  145. basic_memory/sync/file_change_scanner.py +0 -158
  146. basic_memory/sync/utils.py +0 -31
  147. basic_memory-0.7.0.dist-info/METADATA +0 -378
  148. basic_memory-0.7.0.dist-info/RECORD +0 -82
  149. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/WHEEL +0 -0
  150. {basic_memory-0.7.0.dist-info → basic_memory-0.16.1.dist-info}/licenses/LICENSE +0 -0
basic_memory/db.py CHANGED
@@ -1,16 +1,16 @@
1
1
  import asyncio
2
+ import os
2
3
  from contextlib import asynccontextmanager
3
4
  from enum import Enum, auto
4
5
  from pathlib import Path
5
6
  from typing import AsyncGenerator, Optional
6
7
 
7
-
8
- from basic_memory.config import ProjectConfig
8
+ from basic_memory.config import BasicMemoryConfig, ConfigManager
9
9
  from alembic import command
10
10
  from alembic.config import Config
11
11
 
12
12
  from loguru import logger
13
- from sqlalchemy import text
13
+ from sqlalchemy import text, event
14
14
  from sqlalchemy.ext.asyncio import (
15
15
  create_async_engine,
16
16
  async_sessionmaker,
@@ -18,12 +18,14 @@ from sqlalchemy.ext.asyncio import (
18
18
  AsyncEngine,
19
19
  async_scoped_session,
20
20
  )
21
+ from sqlalchemy.pool import NullPool
21
22
 
22
23
  from basic_memory.repository.search_repository import SearchRepository
23
24
 
24
25
  # Module level state
25
26
  _engine: Optional[AsyncEngine] = None
26
27
  _session_maker: Optional[async_sessionmaker[AsyncSession]] = None
28
+ _migrations_completed: bool = False
27
29
 
28
30
 
29
31
  class DatabaseType(Enum):
@@ -73,32 +75,119 @@ async def scoped_session(
73
75
  await factory.remove()
74
76
 
75
77
 
78
+ def _configure_sqlite_connection(dbapi_conn, enable_wal: bool = True) -> None:
79
+ """Configure SQLite connection with WAL mode and optimizations.
80
+
81
+ Args:
82
+ dbapi_conn: Database API connection object
83
+ enable_wal: Whether to enable WAL mode (should be False for in-memory databases)
84
+ """
85
+ cursor = dbapi_conn.cursor()
86
+ try:
87
+ # Enable WAL mode for better concurrency (not supported for in-memory databases)
88
+ if enable_wal:
89
+ cursor.execute("PRAGMA journal_mode=WAL")
90
+ # Set busy timeout to handle locked databases
91
+ cursor.execute("PRAGMA busy_timeout=10000") # 10 seconds
92
+ # Optimize for performance
93
+ cursor.execute("PRAGMA synchronous=NORMAL")
94
+ cursor.execute("PRAGMA cache_size=-64000") # 64MB cache
95
+ cursor.execute("PRAGMA temp_store=MEMORY")
96
+ # Windows-specific optimizations
97
+ if os.name == "nt":
98
+ cursor.execute("PRAGMA locking_mode=NORMAL") # Ensure normal locking on Windows
99
+ except Exception as e:
100
+ # Log but don't fail - some PRAGMAs may not be supported
101
+ logger.warning(f"Failed to configure SQLite connection: {e}")
102
+ finally:
103
+ cursor.close()
104
+
105
+
106
+ def _create_engine_and_session(
107
+ db_path: Path, db_type: DatabaseType = DatabaseType.FILESYSTEM
108
+ ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]:
109
+ """Internal helper to create engine and session maker."""
110
+ db_url = DatabaseType.get_db_url(db_path, db_type)
111
+ logger.debug(f"Creating engine for db_url: {db_url}")
112
+
113
+ # Configure connection args with Windows-specific settings
114
+ connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
115
+
116
+ # Add Windows-specific parameters to improve reliability
117
+ if os.name == "nt": # Windows
118
+ connect_args.update(
119
+ {
120
+ "timeout": 30.0, # Increase timeout to 30 seconds for Windows
121
+ "isolation_level": None, # Use autocommit mode
122
+ }
123
+ )
124
+ # Use NullPool for Windows filesystem databases to avoid connection pooling issues
125
+ # Important: Do NOT use NullPool for in-memory databases as it will destroy the database
126
+ # between connections
127
+ if db_type == DatabaseType.FILESYSTEM:
128
+ engine = create_async_engine(
129
+ db_url,
130
+ connect_args=connect_args,
131
+ poolclass=NullPool, # Disable connection pooling on Windows
132
+ echo=False,
133
+ )
134
+ else:
135
+ # In-memory databases need connection pooling to maintain state
136
+ engine = create_async_engine(db_url, connect_args=connect_args)
137
+ else:
138
+ engine = create_async_engine(db_url, connect_args=connect_args)
139
+
140
+ # Enable WAL mode for better concurrency and reliability
141
+ # Note: WAL mode is not supported for in-memory databases
142
+ enable_wal = db_type != DatabaseType.MEMORY
143
+
144
+ @event.listens_for(engine.sync_engine, "connect")
145
+ def enable_wal_mode(dbapi_conn, connection_record):
146
+ """Enable WAL mode on each connection."""
147
+ _configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
148
+
149
+ session_maker = async_sessionmaker(engine, expire_on_commit=False)
150
+ return engine, session_maker
151
+
152
+
76
153
  async def get_or_create_db(
77
154
  db_path: Path,
78
155
  db_type: DatabaseType = DatabaseType.FILESYSTEM,
156
+ ensure_migrations: bool = True,
79
157
  ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
80
158
  """Get or create database engine and session maker."""
81
159
  global _engine, _session_maker
82
160
 
83
161
  if _engine is None:
84
- db_url = DatabaseType.get_db_url(db_path, db_type)
85
- logger.debug(f"Creating engine for db_url: {db_url}")
86
- _engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
87
- _session_maker = async_sessionmaker(_engine, expire_on_commit=False)
162
+ _engine, _session_maker = _create_engine_and_session(db_path, db_type)
163
+
164
+ # Run migrations automatically unless explicitly disabled
165
+ if ensure_migrations:
166
+ app_config = ConfigManager().config
167
+ await run_migrations(app_config, db_type)
168
+
169
+ # These checks should never fail since we just created the engine and session maker
170
+ # if they were None, but we'll check anyway for the type checker
171
+ if _engine is None:
172
+ logger.error("Failed to create database engine", db_path=str(db_path))
173
+ raise RuntimeError("Database engine initialization failed")
174
+
175
+ if _session_maker is None:
176
+ logger.error("Failed to create session maker", db_path=str(db_path))
177
+ raise RuntimeError("Session maker initialization failed")
88
178
 
89
- assert _engine is not None # for type checker
90
- assert _session_maker is not None # for type checker
91
179
  return _engine, _session_maker
92
180
 
93
181
 
94
182
  async def shutdown_db() -> None: # pragma: no cover
95
183
  """Clean up database connections."""
96
- global _engine, _session_maker
184
+ global _engine, _session_maker, _migrations_completed
97
185
 
98
186
  if _engine:
99
187
  await _engine.dispose()
100
188
  _engine = None
101
189
  _session_maker = None
190
+ _migrations_completed = False
102
191
 
103
192
 
104
193
  @asynccontextmanager
@@ -112,27 +201,79 @@ async def engine_session_factory(
112
201
  for each test. For production use, use get_or_create_db() instead.
113
202
  """
114
203
 
115
- global _engine, _session_maker
204
+ global _engine, _session_maker, _migrations_completed
116
205
 
117
206
  db_url = DatabaseType.get_db_url(db_path, db_type)
118
207
  logger.debug(f"Creating engine for db_url: {db_url}")
119
208
 
120
- _engine = create_async_engine(db_url, connect_args={"check_same_thread": False})
209
+ # Configure connection args with Windows-specific settings
210
+ connect_args: dict[str, bool | float | None] = {"check_same_thread": False}
211
+
212
+ # Add Windows-specific parameters to improve reliability
213
+ if os.name == "nt": # Windows
214
+ connect_args.update(
215
+ {
216
+ "timeout": 30.0, # Increase timeout to 30 seconds for Windows
217
+ "isolation_level": None, # Use autocommit mode
218
+ }
219
+ )
220
+ # Use NullPool for Windows filesystem databases to avoid connection pooling issues
221
+ # Important: Do NOT use NullPool for in-memory databases as it will destroy the database
222
+ # between connections
223
+ if db_type == DatabaseType.FILESYSTEM:
224
+ _engine = create_async_engine(
225
+ db_url,
226
+ connect_args=connect_args,
227
+ poolclass=NullPool, # Disable connection pooling on Windows
228
+ echo=False,
229
+ )
230
+ else:
231
+ # In-memory databases need connection pooling to maintain state
232
+ _engine = create_async_engine(db_url, connect_args=connect_args)
233
+ else:
234
+ _engine = create_async_engine(db_url, connect_args=connect_args)
235
+
236
+ # Enable WAL mode for better concurrency and reliability
237
+ # Note: WAL mode is not supported for in-memory databases
238
+ enable_wal = db_type != DatabaseType.MEMORY
239
+
240
+ @event.listens_for(_engine.sync_engine, "connect")
241
+ def enable_wal_mode(dbapi_conn, connection_record):
242
+ """Enable WAL mode on each connection."""
243
+ _configure_sqlite_connection(dbapi_conn, enable_wal=enable_wal)
244
+
121
245
  try:
122
246
  _session_maker = async_sessionmaker(_engine, expire_on_commit=False)
123
247
 
124
- assert _engine is not None # for type checker
125
- assert _session_maker is not None # for type checker
248
+ # Verify that engine and session maker are initialized
249
+ if _engine is None: # pragma: no cover
250
+ logger.error("Database engine is None in engine_session_factory")
251
+ raise RuntimeError("Database engine initialization failed")
252
+
253
+ if _session_maker is None: # pragma: no cover
254
+ logger.error("Session maker is None in engine_session_factory")
255
+ raise RuntimeError("Session maker initialization failed")
256
+
126
257
  yield _engine, _session_maker
127
258
  finally:
128
259
  if _engine:
129
260
  await _engine.dispose()
130
261
  _engine = None
131
262
  _session_maker = None
263
+ _migrations_completed = False
132
264
 
133
265
 
134
- async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.FILESYSTEM):
266
+ async def run_migrations(
267
+ app_config: BasicMemoryConfig, database_type=DatabaseType.FILESYSTEM, force: bool = False
268
+ ): # pragma: no cover
135
269
  """Run any pending alembic migrations."""
270
+ global _migrations_completed
271
+
272
+ # Skip if migrations already completed unless forced
273
+ if _migrations_completed and not force:
274
+ logger.debug("Migrations already completed in this session, skipping")
275
+ return
276
+
136
277
  logger.info("Running database migrations...")
137
278
  try:
138
279
  # Get the absolute path to the alembic directory relative to this file
@@ -154,8 +295,18 @@ async def run_migrations(app_config: ProjectConfig, database_type=DatabaseType.F
154
295
  command.upgrade(config, "head")
155
296
  logger.info("Migrations completed successfully")
156
297
 
157
- _, session_maker = await get_or_create_db(app_config.database_path, database_type)
158
- await SearchRepository(session_maker).init_search_index()
298
+ # Get session maker - ensure we don't trigger recursive migration calls
299
+ if _session_maker is None:
300
+ _, session_maker = _create_engine_and_session(app_config.database_path, database_type)
301
+ else:
302
+ session_maker = _session_maker
303
+
304
+ # initialize the search Index schema
305
+ # the project_id is not used for init_search_index, so we pass a dummy value
306
+ await SearchRepository(session_maker, 1).init_search_index()
307
+
308
+ # Mark migrations as completed
309
+ _migrations_completed = True
159
310
  except Exception as e: # pragma: no cover
160
311
  logger.error(f"Error running migrations: {e}")
161
312
  raise
basic_memory/deps.py CHANGED
@@ -1,52 +1,104 @@
1
1
  """Dependency injection functions for basic-memory services."""
2
2
 
3
3
  from typing import Annotated
4
+ from loguru import logger
4
5
 
5
- import logfire
6
- from fastapi import Depends
6
+ from fastapi import Depends, HTTPException, Path, status, Request
7
7
  from sqlalchemy.ext.asyncio import (
8
8
  AsyncSession,
9
9
  AsyncEngine,
10
10
  async_sessionmaker,
11
11
  )
12
+ import pathlib
12
13
 
13
14
  from basic_memory import db
14
- from basic_memory.config import ProjectConfig, config
15
+ from basic_memory.config import ProjectConfig, BasicMemoryConfig, ConfigManager
16
+ from basic_memory.importers import (
17
+ ChatGPTImporter,
18
+ ClaudeConversationsImporter,
19
+ ClaudeProjectsImporter,
20
+ MemoryJsonImporter,
21
+ )
15
22
  from basic_memory.markdown import EntityParser
16
23
  from basic_memory.markdown.markdown_processor import MarkdownProcessor
17
24
  from basic_memory.repository.entity_repository import EntityRepository
18
25
  from basic_memory.repository.observation_repository import ObservationRepository
26
+ from basic_memory.repository.project_repository import ProjectRepository
19
27
  from basic_memory.repository.relation_repository import RelationRepository
20
28
  from basic_memory.repository.search_repository import SearchRepository
21
- from basic_memory.services import (
22
- EntityService,
23
- )
29
+ from basic_memory.services import EntityService, ProjectService
24
30
  from basic_memory.services.context_service import ContextService
31
+ from basic_memory.services.directory_service import DirectoryService
25
32
  from basic_memory.services.file_service import FileService
26
33
  from basic_memory.services.link_resolver import LinkResolver
27
34
  from basic_memory.services.search_service import SearchService
35
+ from basic_memory.sync import SyncService
36
+ from basic_memory.utils import generate_permalink
37
+
38
+
39
+ def get_app_config() -> BasicMemoryConfig: # pragma: no cover
40
+ app_config = ConfigManager().config
41
+ return app_config
42
+
43
+
44
+ AppConfigDep = Annotated[BasicMemoryConfig, Depends(get_app_config)] # pragma: no cover
28
45
 
29
46
 
30
47
  ## project
31
48
 
32
49
 
33
- def get_project_config() -> ProjectConfig: # pragma: no cover
34
- return config
50
+ async def get_project_config(
51
+ project: "ProjectPathDep", project_repository: "ProjectRepositoryDep"
52
+ ) -> ProjectConfig: # pragma: no cover
53
+ """Get the current project referenced from request state.
35
54
 
55
+ Args:
56
+ request: The current request object
57
+ project_repository: Repository for project operations
36
58
 
37
- ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)] # pragma: no cover
59
+ Returns:
60
+ The resolved project config
61
+
62
+ Raises:
63
+ HTTPException: If project is not found
64
+ """
65
+ # Convert project name to permalink for lookup
66
+ project_permalink = generate_permalink(str(project))
67
+ project_obj = await project_repository.get_by_permalink(project_permalink)
68
+ if project_obj:
69
+ return ProjectConfig(name=project_obj.name, home=pathlib.Path(project_obj.path))
38
70
 
71
+ # Not found
72
+ raise HTTPException( # pragma: no cover
73
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
74
+ )
75
+
76
+
77
+ ProjectConfigDep = Annotated[ProjectConfig, Depends(get_project_config)] # pragma: no cover
39
78
 
40
79
  ## sqlalchemy
41
80
 
42
81
 
43
82
  async def get_engine_factory(
44
- project_config: ProjectConfigDep,
83
+ request: Request,
45
84
  ) -> tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: # pragma: no cover
46
- """Get engine and session maker."""
47
- engine, session_maker = await db.get_or_create_db(project_config.database_path)
48
- if project_config.env != "test":
49
- logfire.instrument_sqlalchemy(engine=engine)
85
+ """Get cached engine and session maker from app state.
86
+
87
+ For API requests, returns cached connections from app.state for optimal performance.
88
+ For non-API contexts (CLI), falls back to direct database connection.
89
+ """
90
+ # Try to get cached connections from app state (API context)
91
+ if (
92
+ hasattr(request, "app")
93
+ and hasattr(request.app.state, "engine")
94
+ and hasattr(request.app.state, "session_maker")
95
+ ):
96
+ return request.app.state.engine, request.app.state.session_maker
97
+
98
+ # Fallback for non-API contexts (CLI)
99
+ logger.debug("Using fallback database connection for non-API context")
100
+ app_config = get_app_config()
101
+ engine, session_maker = await db.get_or_create_db(app_config.database_path)
50
102
  return engine, session_maker
51
103
 
52
104
 
@@ -67,11 +119,70 @@ SessionMakerDep = Annotated[async_sessionmaker, Depends(get_session_maker)]
67
119
  ## repositories
68
120
 
69
121
 
122
+ async def get_project_repository(
123
+ session_maker: SessionMakerDep,
124
+ ) -> ProjectRepository:
125
+ """Get the project repository."""
126
+ return ProjectRepository(session_maker)
127
+
128
+
129
+ ProjectRepositoryDep = Annotated[ProjectRepository, Depends(get_project_repository)]
130
+ ProjectPathDep = Annotated[str, Path()] # Use Path dependency to extract from URL
131
+
132
+
133
+ async def get_project_id(
134
+ project_repository: ProjectRepositoryDep,
135
+ project: ProjectPathDep,
136
+ ) -> int:
137
+ """Get the current project ID from request state.
138
+
139
+ When using sub-applications with /{project} mounting, the project value
140
+ is stored in request.state by middleware.
141
+
142
+ Args:
143
+ request: The current request object
144
+ project_repository: Repository for project operations
145
+
146
+ Returns:
147
+ The resolved project ID
148
+
149
+ Raises:
150
+ HTTPException: If project is not found
151
+ """
152
+ # Convert project name to permalink for lookup
153
+ project_permalink = generate_permalink(str(project))
154
+ project_obj = await project_repository.get_by_permalink(project_permalink)
155
+ if project_obj:
156
+ return project_obj.id
157
+
158
+ # Try by name if permalink lookup fails
159
+ project_obj = await project_repository.get_by_name(str(project)) # pragma: no cover
160
+ if project_obj: # pragma: no cover
161
+ return project_obj.id
162
+
163
+ # Not found
164
+ raise HTTPException( # pragma: no cover
165
+ status_code=status.HTTP_404_NOT_FOUND, detail=f"Project '{project}' not found."
166
+ )
167
+
168
+
169
+ """
170
+ The project_id dependency is used in the following:
171
+ - EntityRepository
172
+ - ObservationRepository
173
+ - RelationRepository
174
+ - SearchRepository
175
+ - ProjectInfoRepository
176
+ """
177
+ ProjectIdDep = Annotated[int, Depends(get_project_id)]
178
+
179
+
70
180
  async def get_entity_repository(
71
181
  session_maker: SessionMakerDep,
182
+ project_id: ProjectIdDep,
72
183
  ) -> EntityRepository:
73
- """Create an EntityRepository instance."""
74
- return EntityRepository(session_maker)
184
+ """Create an EntityRepository instance for the current project."""
185
+ return EntityRepository(session_maker, project_id=project_id)
75
186
 
76
187
 
77
188
  EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)]
@@ -79,9 +190,10 @@ EntityRepositoryDep = Annotated[EntityRepository, Depends(get_entity_repository)
79
190
 
80
191
  async def get_observation_repository(
81
192
  session_maker: SessionMakerDep,
193
+ project_id: ProjectIdDep,
82
194
  ) -> ObservationRepository:
83
- """Create an ObservationRepository instance."""
84
- return ObservationRepository(session_maker)
195
+ """Create an ObservationRepository instance for the current project."""
196
+ return ObservationRepository(session_maker, project_id=project_id)
85
197
 
86
198
 
87
199
  ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observation_repository)]
@@ -89,9 +201,10 @@ ObservationRepositoryDep = Annotated[ObservationRepository, Depends(get_observat
89
201
 
90
202
  async def get_relation_repository(
91
203
  session_maker: SessionMakerDep,
204
+ project_id: ProjectIdDep,
92
205
  ) -> RelationRepository:
93
- """Create a RelationRepository instance."""
94
- return RelationRepository(session_maker)
206
+ """Create a RelationRepository instance for the current project."""
207
+ return RelationRepository(session_maker, project_id=project_id)
95
208
 
96
209
 
97
210
  RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repository)]
@@ -99,14 +212,18 @@ RelationRepositoryDep = Annotated[RelationRepository, Depends(get_relation_repos
99
212
 
100
213
  async def get_search_repository(
101
214
  session_maker: SessionMakerDep,
215
+ project_id: ProjectIdDep,
102
216
  ) -> SearchRepository:
103
- """Create a SearchRepository instance."""
104
- return SearchRepository(session_maker)
217
+ """Create a SearchRepository instance for the current project."""
218
+ return SearchRepository(session_maker, project_id=project_id)
105
219
 
106
220
 
107
221
  SearchRepositoryDep = Annotated[SearchRepository, Depends(get_search_repository)]
108
222
 
109
223
 
224
+ # ProjectInfoRepository is deprecated and will be removed in a future version.
225
+ # Use ProjectRepository instead, which has the same functionality plus more project-specific operations.
226
+
110
227
  ## services
111
228
 
112
229
 
@@ -127,7 +244,12 @@ MarkdownProcessorDep = Annotated[MarkdownProcessor, Depends(get_markdown_process
127
244
  async def get_file_service(
128
245
  project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
129
246
  ) -> FileService:
130
- return FileService(project_config.home, markdown_processor)
247
+ logger.debug(
248
+ f"Creating FileService for project: {project_config.name}, base_path: {project_config.home}"
249
+ )
250
+ file_service = FileService(project_config.home, markdown_processor)
251
+ logger.debug(f"Created FileService for project: {file_service} ")
252
+ return file_service
131
253
 
132
254
 
133
255
  FileServiceDep = Annotated[FileService, Depends(get_file_service)]
@@ -140,6 +262,7 @@ async def get_entity_service(
140
262
  entity_parser: EntityParserDep,
141
263
  file_service: FileServiceDep,
142
264
  link_resolver: "LinkResolverDep",
265
+ app_config: AppConfigDep,
143
266
  ) -> EntityService:
144
267
  """Create EntityService with repository."""
145
268
  return EntityService(
@@ -149,6 +272,7 @@ async def get_entity_service(
149
272
  entity_parser=entity_parser,
150
273
  file_service=file_service,
151
274
  link_resolver=link_resolver,
275
+ app_config=app_config,
152
276
  )
153
277
 
154
278
 
@@ -177,9 +301,111 @@ LinkResolverDep = Annotated[LinkResolver, Depends(get_link_resolver)]
177
301
 
178
302
 
179
303
  async def get_context_service(
180
- search_repository: SearchRepositoryDep, entity_repository: EntityRepositoryDep
304
+ search_repository: SearchRepositoryDep,
305
+ entity_repository: EntityRepositoryDep,
306
+ observation_repository: ObservationRepositoryDep,
181
307
  ) -> ContextService:
182
- return ContextService(search_repository, entity_repository)
308
+ return ContextService(
309
+ search_repository=search_repository,
310
+ entity_repository=entity_repository,
311
+ observation_repository=observation_repository,
312
+ )
183
313
 
184
314
 
185
315
  ContextServiceDep = Annotated[ContextService, Depends(get_context_service)]
316
+
317
+
318
+ async def get_sync_service(
319
+ app_config: AppConfigDep,
320
+ entity_service: EntityServiceDep,
321
+ entity_parser: EntityParserDep,
322
+ entity_repository: EntityRepositoryDep,
323
+ relation_repository: RelationRepositoryDep,
324
+ project_repository: ProjectRepositoryDep,
325
+ search_service: SearchServiceDep,
326
+ file_service: FileServiceDep,
327
+ ) -> SyncService: # pragma: no cover
328
+ """
329
+
330
+ :rtype: object
331
+ """
332
+ return SyncService(
333
+ app_config=app_config,
334
+ entity_service=entity_service,
335
+ entity_parser=entity_parser,
336
+ entity_repository=entity_repository,
337
+ relation_repository=relation_repository,
338
+ project_repository=project_repository,
339
+ search_service=search_service,
340
+ file_service=file_service,
341
+ )
342
+
343
+
344
+ SyncServiceDep = Annotated[SyncService, Depends(get_sync_service)]
345
+
346
+
347
+ async def get_project_service(
348
+ project_repository: ProjectRepositoryDep,
349
+ ) -> ProjectService:
350
+ """Create ProjectService with repository."""
351
+ return ProjectService(repository=project_repository)
352
+
353
+
354
+ ProjectServiceDep = Annotated[ProjectService, Depends(get_project_service)]
355
+
356
+
357
+ async def get_directory_service(
358
+ entity_repository: EntityRepositoryDep,
359
+ ) -> DirectoryService:
360
+ """Create DirectoryService with dependencies."""
361
+ return DirectoryService(
362
+ entity_repository=entity_repository,
363
+ )
364
+
365
+
366
+ DirectoryServiceDep = Annotated[DirectoryService, Depends(get_directory_service)]
367
+
368
+
369
+ # Import
370
+
371
+
372
+ async def get_chatgpt_importer(
373
+ project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
374
+ ) -> ChatGPTImporter:
375
+ """Create ChatGPTImporter with dependencies."""
376
+ return ChatGPTImporter(project_config.home, markdown_processor)
377
+
378
+
379
+ ChatGPTImporterDep = Annotated[ChatGPTImporter, Depends(get_chatgpt_importer)]
380
+
381
+
382
+ async def get_claude_conversations_importer(
383
+ project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
384
+ ) -> ClaudeConversationsImporter:
385
+ """Create ChatGPTImporter with dependencies."""
386
+ return ClaudeConversationsImporter(project_config.home, markdown_processor)
387
+
388
+
389
+ ClaudeConversationsImporterDep = Annotated[
390
+ ClaudeConversationsImporter, Depends(get_claude_conversations_importer)
391
+ ]
392
+
393
+
394
+ async def get_claude_projects_importer(
395
+ project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
396
+ ) -> ClaudeProjectsImporter:
397
+ """Create ChatGPTImporter with dependencies."""
398
+ return ClaudeProjectsImporter(project_config.home, markdown_processor)
399
+
400
+
401
+ ClaudeProjectsImporterDep = Annotated[ClaudeProjectsImporter, Depends(get_claude_projects_importer)]
402
+
403
+
404
+ async def get_memory_json_importer(
405
+ project_config: ProjectConfigDep, markdown_processor: MarkdownProcessorDep
406
+ ) -> MemoryJsonImporter:
407
+ """Create ChatGPTImporter with dependencies."""
408
+ return MemoryJsonImporter(project_config.home, markdown_processor)
409
+
410
+
411
+ MemoryJsonImporterDep = Annotated[MemoryJsonImporter, Depends(get_memory_json_importer)]